Skip to content

About

This project is the codebase for the Tau Testnet Alpha Blockchain

Resources

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

 

History

365 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The TauNet logo

Tau Testnet Alpha Blockchain

A live, working blockchain whose state transitions and consensus rules are governed by Tau formal logic. A Python host handles networking, storage, and cryptography (BLS12-381 signatures); a separate Tau logic program — run via native bindings or the bundled Docker image — is the ultimate arbiter of block validity, proposer eligibility, transfers, and fees.

  • Tau-driven consensus — block validity (o6) and proposer eligibility (o7) come from living, governance-votable Tau rules.
  • On-chain PoA validator set — proposers must be in the active validator set; the set changes only through governance with a configurable vote quorum.
  • Native fee model — fees are emitted by the consensus rules (o9 + optional user o8), charged on inclusion, credited to the block proposer.
  • Multi-node ready — libp2p P2P, header/block sync, fork choice + reorgs, genesis-hash handshake gate, NAT-friendly announce addresses.

Status: Alpha. Under active development, for testing and experimentation.

⚠️ This alpha network has no economic finality, no slashing, and limited DoS protection, and it may reorg. Do not use it with real funds. Specifically: tokens have no real value; there is no finality (PoA with possible deep reorgs); no validator slashing; the cryptography and consensus are not audited; sustained Tau use can hit a native segfault that stops the node process; and a bad governance update can halt the chain until corrected. See Security & risk model.

Platforms: Linux and macOS run the node natively. Windows is supported via Docker Desktop or WSL2 — the node is not supported on native Windows (it relies on Unix-only process control and a bash/cmake Tau build). See Windows.


Quickstart (Docker, ~2 commands)

The standalone Docker image bundles the node and the Tau engine — no separate Tau install needed.

# 1. Build (compiles tau-lang inside the image; one-time, slow)
docker build -f Dockerfile.standalone -t tau-testnet-standalone .

# 2. Run a local node (isolated, no mining) with persistent data in ./data
docker run --rm -it \
  -p 65432:65432 -p 65433:65433 -p 4001:4001 \
  -v "$(pwd)/data:/data" \
  tau-testnet-standalone

The node exposes three ports: 65432 (RPC/TCP + CLI), 65433 (WebSocket, for the web wallet), 4001 (libp2p P2P). The -v mount keeps the database, identity, and keys across restarts.

⚠️ Bind RPC/WS to localhost, not all interfaces. -p 65432:65432 publishes the port on every host interface (0.0.0.0). The RPC/WS API has no authentication beyond a loopback gate on createblock; anyone who can reach the port can read chain state, submit transactions, and (over plaintext WS) interact with the wallet. P2P (4001) is meant to be public; RPC (65432) and WS (65433) are not. For local use, bind them to loopback:

docker run --rm -it \
  -p 127.0.0.1:65432:65432 -p 127.0.0.1:65433:65433 -p 4001:4001 \
  -v "$(pwd)/data:/data" tau-testnet-standalone

To expose RPC/WS deliberately, put them behind a TLS-terminating reverse proxy and set TAU_WS_ALLOWED_ORIGINS. See RPC / WebSocket exposure.

Talk to it from another terminal:

pip install -e .                       # installs the `tau-testnet` CLI
tau-testnet status                     # node health + chain tip
tau-testnet keys new                   # generate a BLS keypair

To join a network instead of running isolated, jump to Join the Tau Testnet. To run without Docker, see Run from source.


Ways to run a node

Path Best for Tau engine
Docker standalone (above) Fastest start, operators Bundled (compiled in image)
From source + native bindings Development, contributors You build tau-lang locally
tau-testnet node run Either of the above, ergonomic flags Whatever the env provides

Node roles

  • Read / RPC node — syncs the chain, serves queries, relays gossip. No keys, mining off. Anyone can run one.
  • Validator — additionally proposes blocks. Needs a BLS keypair and membership in the on-chain active validator set (granted at genesis or via governance).

Windows

The node does not run on native Windows: the watchdog uses SIGKILL/pgrep, the Tau engine reads /proc and loads a .so, and tau-lang builds with bash/cmake/clang. Use one of:

  • Docker Desktop (recommended). Run the exact same image as everyone else. The only difference from the Linux/macOS commands is the volume-mount path. In PowerShell:

    docker build -f Dockerfile.standalone -t tau-testnet-standalone .
    docker run --rm -it -p 65432:65432 -p 65433:65433 -p 4001:4001 -v "${PWD}/data:/data" tau-testnet-standalone

    In cmd.exe use -v "%cd%/data:/data". PowerShell has no \ line continuation — keep docker run on one line, or use a backtick ` to continue.

  • WSL2 (for development / from source). Install WSL2 with a Linux distro (Ubuntu), open the Linux shell, and follow the Run from source steps verbatim — inside WSL it is Linux, so every bash command works as written.

The tau-testnet CLI is pure Python and runs under native Windows Python to query a remote node (tau-testnet --host <ip> --port 65432 status); only running a node needs Docker/WSL2.


Run from source

On Windows, run these inside WSL2 (see Windows). On Linux/macOS, run them directly.

Prerequisites: Python 3.14+.

python3 -m venv venv && source venv/bin/activate
pip install -e .                       # installs deps + the `tau-testnet` CLI

Option A — quick mock node (no Tau build). --test sets TAU_ENV=test + TAU_FORCE_TEST=1 and implies --miner --isolated: a self-mining single node using the deterministic mock validator path. Great for trying the CLI/API without compiling tau-lang.

tau-testnet node run --test

Option B — real Tau engine. Build the tau-lang native bindings once, then run without --test (real evaluation is the default — see Tau execution mode):

# clone tau-lang as a SIBLING of this repo (../tau-lang is auto-discovered)
git clone https://github.com/IDNI/tau-lang.git ../tau-lang
git -C ../tau-lang checkout c3ea334cd1e6612930ea9f404933211858f0009f   # pinned; matches Dockerfile.standalone
( cd ../tau-lang && ./dev dep-cvc5 && ./dev dep-boost \
  && TAU_PYTHON=/path/to/venv/bin/python ./dev build Release -DTAU_BUILD_BINDING_PYTHON_NANOBIND=ON -DTAU_PYTHON_VERSION=3.14 )   # → build-Release/bindings/python/nanobind/tau*.so
# set -DTAU_PYTHON_VERSION to the major.minor of that python (default 3.14, as in the Docker image)

# only needed if auto-discovery of ../tau-lang fails:
export PYTHONPATH=../tau-lang/build-Release/bindings/python/nanobind

tau-testnet node run --miner --isolated        # real-Tau isolated dev chain

tau-testnet node run wraps server.py (you can also run python server.py directly with the same TAU_* env vars). Useful flags: --miner/--no-miner, --isolated/--no-isolated, --listen <multiaddr|host:port>, --ephemeral-identity, --fresh, --open-governance (isolated dev only), --port, --host.


Join the Tau Testnet

The public network is tau-testnet-v2. Everything needed to join is already committed to this repo under networks/tau-testnet-v2/ (genesis.json, the bootnode address, and the network id); genesis.tau and genesis_consensus.tau sit at the repo root. You do not need to obtain anything from the operator.

Every node must advertise the same genesis_hash and TAU_NETWORK_ID or it is rejected at the P2P handshake (fork protection). Using the committed profile guarantees a match.

Pick the path for your machine. Docker is the quickest and works the same on all three OSes; from source is for development.

Option 1 — Docker · Windows / macOS / Linux (no toolchain to build)

Install Docker Desktop (Windows, macOS) or Docker Engine (Linux). The image compiles tau-lang for you — nothing else to set up.

# 1. clone, then build the image once (slow: compiles tau-lang inside the image)
git clone https://github.com/IDNI/tau-testnet.git && cd tau-testnet
docker build -f Dockerfile.standalone -t tau-testnet-standalone .

# 2. join — loads the committed genesis profile; chain data persists in the `tau_data` volume
docker run --rm -it -p 65432:65432 -p 65433:65433 -p 4001:4001 \
  -v tau_data:/data \
  -e TAU_NETWORK_ID=tau-testnet-v2 \
  -e TAU_BOOTSTRAP_PEERS='[{"peer_id":"12D3KooWDpWEYxBy8y84AssrPSLaq9DxC7Lncmn5wERJnAWZFnYC","addrs":["/ip4/34.251.82.246/tcp/4001"]}]' \
  -e TAU_MINING_ENABLED=false \
  tau-testnet-standalone \
  bash -c 'mkdir -p data && cp networks/tau-testnet-v2/genesis.json data/genesis.json && python server.py'

Windows: run the above in PowerShell or Git Bash. In PowerShell, put the docker run on one line (or break lines with a backtick ` instead of \); keep the single quotes around the TAU_BOOTSTRAP_PEERS and bash -c values.

RPC is exposed on localhost:65432, WebSocket on 65433.

Option 2 — From source · macOS / Linux / Windows-WSL2

For development, or to run without Docker. On Windows, run these inside WSL2 — the node is Unix-only.

# 1. clone + install the CLI (Python 3.14+)
git clone https://github.com/IDNI/tau-testnet.git && cd tau-testnet
python3 -m venv venv && source venv/bin/activate
pip install -e .

# 2. build the tau-lang native engine once (prerequisites: see "Run from source")
git clone https://github.com/IDNI/tau-lang.git ../tau-lang
git -C ../tau-lang checkout c3ea334cd1e6612930ea9f404933211858f0009f   # pinned; matches Dockerfile.standalone
( cd ../tau-lang && ./dev dep-cvc5 && ./dev dep-boost \
  && TAU_PYTHON=/path/to/venv/bin/python ./dev build Release -DTAU_BUILD_BINDING_PYTHON_NANOBIND=ON -DTAU_PYTHON_VERSION=3.14 )
# only if auto-discovery of ../tau-lang fails:
# export PYTHONPATH=../tau-lang/build-Release/bindings/python/nanobind

# 3. join — copies the genesis profile into data/, loads the network env, starts the node
./networks/tau-testnet-v2/join.sh

join.sh simply does:

cp networks/tau-testnet-v2/genesis.json data/genesis.json
set -a; . networks/tau-testnet-v2/env; set +a   # TAU_NETWORK_ID + bootnode + listen addr
tau-testnet node run --no-isolated

The node dials the bootnode (with retry/backoff), handshakes, syncs headers + block bodies, and rebuilds state by replaying from genesis. Known peers persist across restarts. Remote clients cannot trigger block production (createblock is loopback-only unless TAU_ALLOW_REMOTE_CREATEBLOCK=true).

Verify you joined

Both join commands run the node in the foreground, so sync progress (Header sync …, Added block #N …) streams live in your terminal. To query state, use the CLI from another terminal (it talks to the node's TCP port 65432):

tau-testnet status                       # node started from source
tau-testnet --host localhost status      # node started via Docker (CLI installed on the host)

The Docker image ships only the node, not the tau-testnet CLI. To query a Docker node, install the client on the host with pip install -e . (no tau-lang build needed — the CLI is a thin RPC client), or just read the container's foreground logs.

A rising head_number means you are syncing. If it stays at 0, the handshake was rejected — confirm TAU_NETWORK_ID=tau-testnet-v2 and that data/genesis.json matches the committed profile (block_0.hash = 35faee3b…f1a7). The node logs the exact reason for any mismatch.

Run as a validator

A validator additionally proposes blocks and must already be in the active set (granted at genesis or via governance — see Operating a network). Add a BLS keypair on top of either path above:

export TAU_MINING_ENABLED=true
export TAU_MINER_PUBKEY=<96-hex-pubkey>
export TAU_MINER_PRIVKEY=<64-hex-privkey>     # or TAU_MINER_PRIVKEY_PATH=/path/to/key
tau-testnet node run --no-isolated

For a Docker node, pass the same as -e flags (and mount the key, e.g. -v "$(pwd)/test_miner.key:/data/test_miner.key:ro" with -e TAU_MINER_PRIVKEY_PATH=/data/test_miner.key).

A validator whose key is removed from the active set stops proposing, and its blocks are rejected by peers.

Behind NAT / on a public host

Advertise a reachable address (the node still binds TAU_NETWORK_LISTEN locally):

export TAU_ANNOUNCE_ADDRS='/ip4/<YOUR_PUBLIC_IP>/tcp/4001'   # Docker: -e TAU_ANNOUNCE_ADDRS=...

Must be identical on every node, or the network forks: TAU_NETWORK_ID and the three genesis artifacts (which pin the validator set, the vote_quorum policy, and the base fee). Keep TAU_GOVERNANCE_OPEN_ADMISSION=false on a public network. The vote_quorum policy is read from genesis, not per-node config, and is bound into the state hash — a misconfigured node surfaces as a hash mismatch, not a silent fork.

Rebuilding the Docker image against a specific tau-lang

TAU_LANG_REF=$(git ls-remote https://github.com/IDNI/tau-lang.git refs/heads/main | awk '{print $1}')
docker build --pull -f Dockerfile.standalone \
  --build-arg TAU_LANG_REF="$TAU_LANG_REF" --build-arg TAU_BUILD_JOBS=4 \
  -t tau-testnet-standalone:latest .

The image is pinned to the SHA in Dockerfile.standalone (TAU_LANG_REF); overriding it (e.g. to tau-lang main as above) builds an untested engine, and every node on a network must run the same engine.

TAU_LANG_REF accepts a branch, tag, or commit SHA. Keep TAU_BUILD_JOBS ≥ 1.


Using the node

The tau-testnet CLI is the primary interface; it speaks the RPC over TCP (default 127.0.0.1:65432). Use --host/--port to target a remote node and --json for machine-readable output.

# keys (stored under ~/.tau-testnet/keys, chmod 0600)
tau-testnet keys new                              # print a fresh keypair
tau-testnet keys save --name alice                # generate + save under a name
tau-testnet keys list

# query chain state
tau-testnet status
tau-testnet balance <pubkey>
tau-testnet sequence <pubkey>
tau-testnet history <pubkey>
tau-testnet blocks --limit 10
tau-testnet mempool
tau-testnet governance

# send a transfer (default fee 10, expiry 600s)
tau-testnet tx send --key alice --to <recipient_pubkey> --amount 10
tau-testnet tx send --key alice --transfer <pk1>:5 --transfer <pk2>:3 --fee 10

# governance (sender must be an active validator; default fee 0)
tau-testnet gov propose --key alice --file consensus_update.json
tau-testnet gov vote --key alice --update-id <update_id_hex>

# rule sharing (send a rule to another user; they review, then accept or reject)
tau-testnet rule offer --key alice --to <recipient_pubkey> --rule-file policy.tau
tau-testnet rule list --key bob --role in
tau-testnet rule show <offer_id>
tau-testnet rule check <offer_id>
tau-testnet rule accept --key bob <offer_id>
tau-testnet rule reject --key bob <offer_id>

# retractable transfers (escrowed until a release point; the sender can take it back first)
tau-testnet retractable send --key alice --to <recipient_pubkey> --amount 10 --release-in 20
tau-testnet retractable list --key alice --role out
tau-testnet retractable show <retractable_id>
tau-testnet retractable retract --key alice <retractable_id>

# target a remote node
tau-testnet --host testnet.tau.net --port 65432 status

Full CLI reference, validator-setup recipe, and the governance update lifecycle: docs/developer_cli.md — which also covers the raw TCP/WebSocket command grammar and JSON envelopes under Raw protocol.

Web wallet

A browser wallet lives in web-wallet/ (static client; connects to the node's WebSocket port, 65433 by default). It is not auto-served — open it with a static server:

cd web-wallet && python3 -m http.server 8000
# then browse http://localhost:8000 and point the wallet at ws://<node-host>:65433

Console wallet (legacy)

python wallet.py provides the original command-line wallet (new, balance, history, send); see WALLET_USAGE.md. The tau-testnet CLI above is the preferred path.


Operating a network (genesis, keys, validators)

A network is defined by its genesis artifacts. Generate them once and distribute the identical files to every node.

# 1. validator keypair (writes test_miner.key + test_miner.pub)
python scripts/generate_miner_keys.py        # or: tau-testnet keys new

# 2. genesis (pre-funds accounts, pins validator set + vote quorum + base fee)
python scripts/gen_genesis.py \
  --validator-key <96-hex-pubkey> \
  --account <addr1>:1000000 --account <addr2>:500000 \
  --network-id tau-testnet-v2 \
  --base-fee 10 \
  --genesis-rules-path genesis.tau \
  --genesis-consensus-path genesis_consensus.tau \
  --out data/genesis.json
  • Validator and account addresses are 96 lowercase hex chars (48-byte BLS pubkeys, no 0x). Private keys are 64 hex chars (32 bytes). Use --validator-privkey to derive the pubkey automatically.
  • No auto-faucet — unknown accounts have balance 0. Fund accounts at genesis with repeatable --account ADDR:BALANCE, or by transfer afterward.
  • Transfer amounts are bv[24] → capped at 16,777,215 per step; --base-fee likewise (0–16777215).
  • Adding a validator after launch: an existing validator proposes a consensus_rule_update with host_contract_patch.validator_additions; once it reaches the vote quorum (supermajority = ⌈2N/3⌉ by default, or majority) it activates. Removal works symmetrically.

Configuration reference (TAU_* env vars)

Defaults shown; set via environment or tau-testnet node run flags. Bold vars must match across all peers on a network.

Core

Var Default Meaning
TAU_ENV development Profile: development / test / production.
TAU_NETWORK_ID tau-local Consensus network identifier.
TAU_HOST 127.0.0.1 TCP API bind address.
TAU_PORT 65432 TCP API port (WebSocket = +1 = 65433).
TAU_DB_PATH node.db SQLite database path.
TAU_PROGRAM_FILE genesis.tau Application Tau rules program.
TAU_FORCE_TEST 0 1 = mock Tau (test only; refused in production).
TAU_USE_DIRECT_BINDINGS — 1 = use native tau-lang bindings (default in Docker).

Networking

Var Default Meaning
TAU_NETWORK_LISTEN /ip4/0.0.0.0/tcp/0 libp2p bind multiaddr(s); use a fixed port (e.g. /ip4/0.0.0.0/tcp/4001) to be reachable.
TAU_ANNOUNCE_ADDRS (empty) Multiaddrs advertised to peers (set to your public addr behind NAT).
TAU_BOOTSTRAP_PEERS [] JSON [{"peer_id":…,"addrs":[…]}] for discovery.
TAU_IDENTITY_KEY_PATH data/identity.key Persistent libp2p identity (--ephemeral-identity to skip).
TAU_PEERSTORE_PATH data/peerstore Persisted known-peer store.

Mining / validator

Var Default Meaning
TAU_MINING_ENABLED true Run the internal block proposer. Set false for read nodes. The standalone Docker image overrides this to false, so the quickstart node does not mine; pass -e TAU_MINING_ENABLED=true to mine.
TAU_MINER_PUBKEY (test key) 96-hex validator public key.
TAU_MINER_PRIVKEY / _PATH — / data/test_miner.key 64-hex signing key (direct value wins over path).
TAU_ALLOW_REMOTE_CREATEBLOCK false Allow non-loopback createblock RPC (leave off).

Governance / consensus (network-wide)

Var Default Meaning
TAU_VALIDATOR_VOTE_QUORUM supermajority Default vote_quorum baked into genesis by gen_genesis.py (supermajority = ⌈2N/3⌉, or majority = N/2+1). Runtime nodes read the policy from genesis, not this var. Set it at genesis with gen_genesis.py --vote-quorum.
TAU_BLOCK_SIGNATURE_SCHEME bls_g2 Block signature algorithm.
TAU_GOVERNANCE_OPEN_ADMISSION false Allow non-validators into governance (keep off in public).

Operational extras: TAU_MAX_MEMPOOL_TXS (5000), TAU_MEMPOOL_RULE_LANE_MAX_FRACTION (0.1; slow-lane share of the mempool), TAU_BLOCK_MAX_RULE_TXS (8; proposer-side rule budget, never above the consensus ceiling), TAU_MAX_CONNECTIONS (200), TAU_RATE_LIMIT_PER_PEER (2.0/s), TAU_COMM_TIMEOUT (60s; watchdog kills a stalled in-process interpreter; also the limit on each advisory worker request), TAU_MINING_BUDGET (60s; per Tau request while this node builds a block; an overrun defers that transaction), TAU_VALIDATION_CEILING (600s; per Tau request while applying a received block; an overrun leaves the block for a later retry), TAU_WS_ALLOWED_ORIGINS, TAU_WS_CERT_PATH/TAU_WS_KEY_PATH (WSS), TAU_FORCE_FRESH_START (crash recovery). See config.py for the full list.


Tau execution mode

Real Tau evaluation is enabled by default. Setting TAU_FORCE_TEST=1 bypasses the real engine and uses a deterministic mock validator path — for development/CI only (the test suite sets it automatically). Booting with TAU_FORCE_TEST=1 and TAU_ENV=production is refused.

When native bindings are active (TAU_USE_DIRECT_BINDINGS=1, default in Docker), the node holds a single tau::interpreter instance and reads state updates from its output streams. The interpreter is process-global and Tau calls are serialized; a watchdog (TAU_COMM_TIMEOUT) kills a stalled in-process evaluation, while block evaluation runs in worker processes bounded per request by TAU_MINING_BUDGET and TAU_VALIDATION_CEILING. A native segfault inside a block-evaluation worker is contained: the worker is lost and the attempt is operational, never a verdict on a transaction or a block. The in-process interpreter, which holds the consensus rules and answers the header and eligibility fallbacks, can still take the node process down (see Security & risk model).


Consensus boundary

"Tau-driven" means the Tau program is the arbiter of the consensus verdict, but the Python host performs all the cryptography, encoding, and state mechanics the verdict depends on. The split is fixed:

Tau decides (verdict output streams):

  • o6 — block validity. The genesis rule is o6 = i10 — "valid iff the host's cryptographic proof check passed" (see below).
  • o7 — proposer eligibility.
  • o9 — consensus base fee (strict); o8 — optional user fee (lenient).
  • o5 — user policy on a transfer (0 = block, 1/absent = allow). User-deployed, sender-scoped rules; consensus-enforced at both admission and block apply. A block on any transfer rejects the whole user_tx. Lets users express recipient whitelists (i4), time-locks (i5), and spending limits (i1). See WALLET_USAGE.md. Tiered co-signature approvals build on o5: a sender requires an approver's signature above an amount threshold, and the transfer parks on-chain until the rule is satisfied. See docs/approvals.md. Sender scope is enforced at admission (UNSCOPED_USER_RULE): o5/o8 are shared streams composed across every deployed rule, so rule text writing them must reference i12 (sender pubkey) or i3 (from address). Without that, one always (o5[t] = 0) deploy would block every account on the network. The screen is textual — a reference is not proof the rule is genuinely gated by it — and applies at admission only, so rules already on chain are unaffected. User rules reference [t] only (TEMPORAL_REFERENCE): i1[t-1] or i1[0] is refused at admission and again at block apply, on every stream and for rules offered, accepted or routed as clauses. One unit that reads another step gives the whole evaluator a lookback, and every output is then 0 for the steps after each later revision or rebuild, for every account.

Python host enforces before Tau (the inputs Tau trusts):

  • Transaction BLS signature verification + canonical decoding (commands/sendtx.py).
  • Block proposer signature — Block.verify_consensus_proof() verifies the proposer's BLS signature over the canonical header and feeds the result to Tau as i10; the o6 = i10 rule then rejects an unsigned/forged block. Validator-set membership alone proves only that the named proposer is listed, not that the sender holds its key — the signature check is what binds them.
  • Proposer ∈ active validator set (consensus/engine.py).
  • Per-account sequence, expiration, fee_limit admission estimate, and DoS size bounds (consensus/admission.py).
  • Canonical Tau input-stream construction (heights, timestamps, ids).

Python host enforces after Tau (applying the verdict):

  • Account balance/sequence updates and fee charging as a single staged overlay, committed only if the tx stays accepted (all-or-nothing).
  • Supply-conservation invariant (no mint/burn) — a mismatch raises, it does not silently continue.
  • Governance lifecycle: quorum tally, activation-delay floor, and height transitions.
  • state_hash computation and exact-match comparison against the block header.

If Tau and Python disagree:

  • The host may reject a Tau-valid block — a wrong state_hash, a supply violation, a proposer not in the set, or a failed signature reject the block regardless of the Tau verdict.
  • The host does not accept anything the Tau verdict failed: o6 == 0 (with no require_bls_sig escape clause) ⇒ rejected.
  • An invalid consensus fee (o9) raises FeeRuleError: the proposer aborts the round and validators defer the block — it is simply not committed and can be re-offered later (there is no negative cache; a persistently-bad voted-in rule can therefore halt progress at that height until governance corrects it).

Non-consensus local policy (may differ per node without forking): mempool size/eviction, rate limits, gossip/DHT discovery, log verbosity, RPC bind address.


State hash & canonical encoding

Every block commits to its full post-state via state_hash and to its body via merkle_root. Both must be reproduced bit-for-bit by every node, so the encodings are fixed.

State hash (consensus/state.py::compute_consensus_state_hash):

state_hash = BLAKE3(
    consensus_rules_text  ||   # active consensus Tau program (UTF-8)
    application_rules_text ||   # active application Tau program (UTF-8)
    accounts_hash         ||   # SHA-256(canonical_json({"balances":…,"sequences":…}))
    consensus_meta_hash        # BLAKE3(encode_consensus_meta(…))
)

consensus_meta_hash binds the active validator set, pending governance updates, votes, the activation schedule (with heights), and the resolved vote_quorum policy. So state_hash commits to: account balances, sequence numbers, both rule programs (hence the fee policy, which the rules emit), the validator set, all in-flight governance, and the quorum policy. All collections are canonically sorted before hashing.

Deliberately not in state_hash:

  • network_id / genesis_hash — bound separately: the genesis hash is fixed by the previous_hash chain back to block 0, and both are enforced at the P2P handshake.
  • state_locator — a DHT lookup hint, not consensus data.
  • consensus_proof (the block signature) and the mempool — not state.

Breaking change: the vote_quorum policy is now folded into state_hash (previously it was not, which allowed two nodes with different quorum config to agree on the hash while computing different governance outcomes). Genesis artifacts and chains created before this change are incompatible — regenerate with scripts/gen_genesis.py.

Canonical identifiers:

Identifier Function → hash Encoding
Block hash BlockHeader.canonical_bytes → SHA-256 Fixed big-endian binary: block_number(8B) ‖ previous_hash(32B) ‖ timestamp(8B) ‖ merkle_root(32B) ‖ proposer_pubkey(48B, omitted if empty) ‖ state_hash(32B). Excludes state_locator, consensus_proof.
tx_id compute_tx_hash → SHA-256 Compact sorted-key JSON of the whole transaction, including signature.
merkle_root compute_merkle_root → SHA-256 Leaves = tx_ids; odd levels duplicate the last node; empty tree = SHA-256("").
update_id compute_update_id → BLAKE3 Length-prefixed binary over (rule_revisions, activate_at_height, host_contract_patch); excludes signature/sequence/fee.

Determinism caveats — clients must match the host byte-for-byte:

  • Token amounts and fee_limit travel as decimal strings, and tx_id/signatures are computed over the raw JSON without type-normalization. A client that sends 100 instead of "100", or uppercase hex, produces a different tx_id and signature. Always send amounts as strings and pubkeys/hashes as lowercase hex.
  • Hashed JSON is always sort_keys=True, separators=(",",":"). Timestamps are integer seconds (no timezone, no float). No floats appear in any hashed payload.

Cryptography & signatures

  • Scheme: BLS12-381 via py_ecc G2Basic (IETF basic scheme, DST …G2_XMD:SHA-256_SSWU_RO_NUL_). Public keys are G1, 48 bytes / 96 lowercase hex; signatures are G2, 96 bytes; private keys are 32 bytes / 64 hex. TAU_BLOCK_SIGNATURE_SCHEME=bls_g2 is descriptive metadata — the scheme is fixed in code.
  • Validation: Verify runs public-key and signature subgroup checks and rejects the point at infinity; transaction ingest additionally runs KeyValidate on sender_pubkey and both transfer addresses. Malformed keys/signatures fail closed (INVALID_SIGNATURE); if py_ecc is unavailable, ingest is refused (BLS_UNAVAILABLE), never skipped.
  • Signed payloads:
    • Transaction → SHA-256 of canonical JSON of {sender_pubkey, sequence_number, expiration_time, expire_at_height, fee_limit, tx_type, …type-specific}. tx_type is signed, so a user_tx signature cannot be replayed as a governance vote. expire_at_height is signed whenever present, and required on anything new: the wall-clock expiration_time is rechecked at apply against the block timestamp, which the proposer picks, so the height is the deadline a proposer cannot move.
    • Block header → SHA-256 of BlockHeader.canonical_bytes(), verified by Block.verify_consensus_proof().

⚠️ No cross-network / version domain separation (replay risk). Signed payloads do not include network_id, genesis_hash, or a protocol version, and there is no explicit tx-vs-block domain tag (they are separated only implicitly by incompatible encodings). A transaction signed for one Tau network is cryptographically valid on any other network where the same keypair is funded. Do not reuse account/validator keys across networks. Adding explicit domain tags (e.g. TAU_TESTNET_TX_V1 | network_id | genesis_hash | tx_type | body) is planned and will be a breaking signature-format change.


Transaction fees

Fees are sourced from the Tau consensus rules and votable by validators — no separate fee machinery.

  • Consensus fee (o9, strict). Active consensus rules emit the base fee on stream o9 (genesis default always (o9[t]:bv[24] = { #x00000a }:bv[24]). → 10/step). Change it via a consensus_rule_update carrying the full new consensus spec, voted to its activation height. Absent o9 → fee model inactive (0). A present-but-invalid o9 is a consensus failure: proposers abort the round and validators defer the block rather than guess.
  • User custom fee (o8, lenient). User application rules may add a per-transfer fee on o8. Absent → 0 silently; invalid → 0 with a node-side warning. Must be sender-scoped (reference i12 or i3), enforced at admission as UNSCOPED_USER_RULE: o8 is composed across all deployed rules, so a flat unconditional always (o8[t] = 3). would levy a fee on every user_tx on the network, not just the author's.
  • Total: total_fee = Σ over the tx's Tau steps of (o9 + o8) — one step per transfer; a transfer-less user_tx is charged one fee-query step. Multi-transfer txs pay N× by design.
  • fee_limit is a cap. Rejected (at admission and in-block) if total_fee > fee_limit; the sender pays total_fee, not the cap. Every tx carries a valid fee_limit; only user_tx is charged (validators never need funds to govern).
  • Credited to block.header.proposer_pubkey, or to a governance-set fee_beneficiary when one is active — a host_contract_patch carrying "fee_beneficiary": "<96-hex>" routes the levy to a fixed account (e.g. a treasury); "proposer" reverts to the default. Pinnable at genesis with --fee-beneficiary, readable from getgovernance. A patch activating at height H governs H+1 onward, because height transitions run after transactions execute. Charge-on-inclusion: a fee-rejected tx pays nothing (no writes, no nonce bump). Sender must cover Σ transfers + total_fee.
  • Determinism note: during block application the node feeds real values on i1 (amount), i3/i4 (from/to pubkeys bv[384]), i5 (block timestamp), i12 (sender pubkey) and the tx's custom inputs. i2 (balance) is the sender's balance at the parent block, frozen for the whole block, so proposer and verifier agree and a balance-reading policy (o5) rule is deterministic — a min-balance floor is expressible (issue #20). Two transfers from one sender in a block see the same pre-block balance, so a floor does not compound within it. Admission evaluates i2 against the current head, which is advisory in the same sense as i5; that drift only matters for fees, so rule text reading i2 is rejected when it also writes o8, and consensus o9 revisions reading i2 stay rejected. i3/i4/i5 are immutable-per-transfer / consensus-injected, hence identical at admission and apply, and may be read freely. Scope on i12/i3/i4, gate on i5, tier on i1. Admission error FEE_LIMIT_TOO_LOW returns the computed required_fee.
  • Mempool priority is by admission-time estimate (estimated_fee DESC, fee_limit DESC, received_at ASC); inflating fee_limit does not buy priority.
  • Limitations: percentage fees are not yet confirmed expressible — flat fees and comparison-ladder tiers are what this node has actually exercised. The long-standing claim was that the deployed tau-lang does not verify multiplication, but bf_mul/bf_div/bf_shl/bf_shr are all in the grammar, so that premise is unverified rather than established. tests/test_o9_arithmetic_probe_native.py settles it against the real engine; it needs native bindings built for the interpreter running the suite. Note a computed o9 needs no width change — the live rule is already bv[24], and Tau arithmetic unifies operands within one bv family. User rule text referencing o6/o7/o9 is rejected at admission, as is a fee rule reading i2.

Governance

Consensus rules and the validator set change only through on-chain governance. A consensus_rule_update carries the full new consensus spec (plus an optional host_contract_patch for validator add/remove); consensus_rule_votes approve it. Update identity is update_id = BLAKE3(rule_revisions, activate_at_height, host_contract_patch) (signature / sequence / fee excluded), so the id is canonical and collision-resistant.

Lifecycle:

submitted ──(quorum reached)──> scheduled ──(height ≥ activate_at_height)──> active ──> archived
    │
    └──(activate_at_height passes while still pending)──> expired (archived)
  • Quorum — promotion to scheduled needs votes ≥ approval_threshold = ⌈2N/3⌉ (supermajority) or N/2+1 (majority), where N is the current active validator set and the policy comes from genesis (bound into the state hash). A validator cannot vote twice (votes are a set); only active validators count — a forged or non-validator vote inside a block is a deterministic no-op.
  • Activation delay — enforced at admission and at block application: activate_at_height ≥ inclusion_height + N. A crafted block cannot reach quorum and activate in the same block.
  • Expiry — a pending update whose activate_at_height arrives before quorum is archived, not activated.
  • Safety — a revision that fails to compile is rejected at admission (isolated staging compile) and again at activation; an activated rule emitting an invalid o9 halts progress until further governance (see Security & risk model). Governance txs still require a valid signature, sequence, and expiration; they carry fee 0 and therefore sort below any paid user tx.

Rule sharing

A user can send a Tau rule to another user, who reviews it and either accepts it into their own specification or rejects it. Three transaction types carry the lifecycle:

type fields beyond the common five lane
rule_offer recipient_pubkey, rule_text, expire_at_height slow
rule_offer_accept offer_id, rule_text (repeated verbatim) slow
rule_offer_reject offer_id fast

Offer identity is offer_id = BLAKE3(offerer, recipient, rule_text, expire_at_height) — sequence, fee and signature excluded, so the id is a property of the offer rather than of the transaction that carried it. All three types pay fees (governance's exemption exists so validators never need funds to govern, which does not apply here; the alternative is free rule spam).

                       ┌──────────────────────────────────────────┐
   rule_offer          │                                          │
  ────────────────▶ offered ──rule_offer_accept──▶ accepted ───────┤
                       │                                          │ resolved set
                       ├──rule_offer_reject──────▶ rejected  ──────┤ (bound into
                       │                                          │  the state hash)
                       └──height ≥ expire_at_height──▶ expired ────┘

Whose specification does an accepted rule join?

There is no per-user specification in the engine: accepted rules land in one global rule accumulation. The documented convention was that a user "scopes" a rule by guarding it on their own sender identity (i12). That convention does not hold, and measurements on the real engine (pinned by tests/test_rule_scoping_native.py) say why:

  1. An output stream that no clause constrains for a given input still materializes, with an arbitrary witness — observed as 0, which for o5 is exactly USER_POLICY_BLOCK_VALUE. So always ( (i12 = A) -> (o5 = 1) ). gives A its intended value and gives everyone else a materialized block. An implication guard is a network-wide transfer block, not isolation.
  2. Two total-form rules on the same stream do not compose: conjoined the spec is unsatisfiable and the interpreter fails to build; fed sequentially through i0 the later one silently supersedes the earlier. So appending one guarded unit per user cannot work either.
  3. A single nested total-form unit does work, and applying it through i0 is equivalent to building it conjoined — which is what makes a replaying node match a live one.

So the accepted-clause registry is the per-user specification, and the node deterministically emits one composite rule unit per shared output stream:

always ( (i12[t]:bv[384] = { #x<acceptor_1> }:bv[384]) ? ( <body_1> )
       : ( (i12[t]:bv[384] = { #x<acceptor_2> }:bv[384]) ? ( <body_2> )
       : ( o5[t]:bv[24] = { #x000001 }:bv[24] ) ) ).

Acceptors are ordered by raw public-key bytes; the innermost else-branch is the stream's neutral value (o5 → allow). Each composite replaces the previous one, so the live spec keeps exactly one unit per stream, and an empty registry emits nothing at all — so the stream is never mentioned and never materializes for anybody.

Offered rules must therefore be exactly one always ( ... ). unit, with balanced parentheses, no nested temporal operator, no reference to i12 (the node supplies the guard), no reference to any step but [t], and exactly one target output stream drawn from a permitted set (o5 today). Consensus-owned streams (o0–o4, o6–o9), apply-time-mocked inputs (i2), and reserved input streams are refused.

Caveats. There is no retraction: an acceptor changes an accepted rule by accepting a different offer on the same stream, which replaces their clause. Accepting is irreversible otherwise. Caps are frozen constants, not config, because a per-node value would let honest nodes disagree about admissibility: 8 KiB per rule, 32 pending offers per recipient, 16 per offerer, a 100 000-block maximum offer window, and 64 acceptors per stream. On o5, once approval slots are active, MAX_TIER_AUTHORS (2) authors bound both this door and self-service policy rules.

Checking conflict status

getruleconflict <offer_id> returns a layered report. It is advisory and node-local and never gates a transaction: it depends on process-local state (per-stream bitvector typing is process-global and sticky, and the shrink width derives from the node's own intern table), the engine build is explicitly unstable, and the cost is unbounded — binding it to block validity would turn a heavy rule into a chain halt.

layer what it proves
shape one always unit writing one permitted stream
reserved_domains does not write consensus-owned streams, read mocked inputs, or touch reserved inputs
registry_collision whether you already hold a clause on this stream (accepting replaces it), and who else holds one
bv_widths the same stream declared at two bitvector widths, compared statically so the live interpreter's typing is never touched
compile the composed rule parses and steps, in a killable subprocess
unrealizable unavailable — see below

Because each acceptor gets its own guarded branch of one composite, cross-user conflict is removed by construction, which is what makes registry_collision a decidable question rather than a guess.

Satisfiability is not checked. tau-lang exposes sat/unsat/valid/unrealizable in its C++ API but not through its Python bindings, so a logical contradiction cannot be detected at all. A clean report means no conflict was observed, never no conflict exists.

State-hash binding

The offer book and clause registry are folded into consensus_meta.mechanism_specific_metadata as rule_offers_root and rule_clauses_root, emitted only while the book is non-empty — the same hash-compat gate eligibility_mode uses. So genesis stays byte-identical and no regenesis is required; the first offer makes an un-upgraded node diverge loudly (state-hash mismatch, block rejected) rather than silently. The only coordination requirement is that nodes upgrade before the first offer lands. rule_text and an offer's terminal status are node-local durability for the read RPCs and are deliberately not hashed.

Bounds and cap breaches at block apply are soft no-ops with a receipt log, so block validity stays a function of the block alone; only a Tau failure on the composed rule hard-rejects the transaction.

Retractable transfers

Long-form guide: docs/retractable.md.

A retractable_transfer parks funds in escrow until a release point, either a block height (release_at_height) or a Unix time (release_at_time). Until then the sender can retract_transfer and the escrow comes back. At the release point the funds go to the recipient without anyone sending another transaction.

tau-testnet retractable send --key alice --to <recipient_pubkey> --amount 10 --release-in 20      # 20 blocks after the next one
tau-testnet retractable send --key alice --to <recipient_pubkey> --amount 10 --release-at-height 5000
tau-testnet retractable send --key alice --to <recipient_pubkey> --amount 10 --release-after 3600   # seconds from now
tau-testnet retractable list --key alice --role out        # open entries (and queued parks/retracts)
tau-testnet retractable show <retractable_id>              # open, released or retracted
tau-testnet retractable retract --key alice <retractable_id>
tau-testnet retractable id --from-pubkey <alice_pubkey> --to <recipient_pubkey> --amount 10 \
    --sequence 7 --release-at-height 5000

Semantics:

  • Park. The amount leaves the sender's balance when the park is mined and sits in escrow. The park passes the same gates as an ordinary transfer (the sender's o5 policy with empty approval slots, the fee step, the balance), so scheduling cannot bypass a co-sign policy. The fee is charged once at park time and never refunded. Escrowed funds are not balance, so they do not count as stake.
  • Retract. Only the sender, feeless, valid while the block is strictly before the release point. The escrow returns to the sender.
  • Release. The first block with height >= release_at_height, or whose header timestamp is >= release_at_time, pays the recipient in an end-of-block sweep. Retract (<) and release (>=) cannot both happen in one block.
  • Id. Content-derived from sender, recipient, amount, sequence number and release point, so retractable id and getretractableid can compute it before the park is signed. The id is printed by retractable send.
  • Caps. 16 open entries per sender, 4096 in total, and a release point at most 100 000 blocks or 90 days ahead. A park's own deadline is capped at its release point (the CLI clamps it), so a park is never mined at or after its release.
  • Reads. getretractable, getretractables, getretractableid; gettxstatus reports a confirmed park's id and state; getaccountstate reports escrowed_out and escrowed_in.

Timing caveats:

  • A block timestamp is chosen by its proposer, within the parent's timestamp and local clock + 120 s. A time-based release can land up to about two minutes early, and a retract near a time deadline is judged by the block's timestamp. Height-based release points are exact.
  • Height does not advance on an idle chain. The miner wakes to seal the one block that crosses a due release point; it does not fast-forward height. For an idle chain, prefer a time-based release.
  • A retract must be included in a block before the release point, not merely sent. A feeless retract sorts last in the mempool under backlog, so retract with margin.

Upgrade. This adds a field to the consensus metadata hash with no activation flag, so it needs a regenesis and every node on the same build. An older binary forks at the first park block.

Mempool policy

  • Lanes — every tx is classified fast (coin transfers) or slow (anything carrying Tau rule text: rule_offer, rule_offer_accept, consensus_rule_update, or a user_tx with a non-empty operations["0"]). A user_tx carrying both a rule and transfers is slow — the signature covers the whole payload, so it cannot be split. Applying a rule recompiles a composite, whose cost climbs steeply with specification complexity, while a transfer is one cheap step; without separation a burst of rule work delays every queued transfer. Fee priority cannot express the split, because a rule emits its own o8 user fee and can therefore price itself to the front of the queue.
  • Selection order — the fast lane is filled first, then the slow lane up to a quota. Within each lane the order is unchanged: estimated_fee DESC, fee_limit DESC, received_at ASC. Inflating fee_limit does not outrank a higher estimate. Selection is proposer-local policy — validators replay the stored block body order — so lane ordering cannot fork.
  • Sequences — admission checks projected (confirmed + pending) state; a sender may stack contiguous sequences, but a gap is rejected. There is no replacement / RBF — a stuck low-fee tx must expire or be evicted, not be replaced.
  • Duplicates — a repeat tx_id is idempotently ignored.
  • Capacity & eviction — the cap is TAU_MAX_MEMPOOL_TXS (default 5000), enforced on both the soft admission pre-check and the DB-layer eviction, each counting pending rows only. Eviction is quota-first: the slow lane may hold at most TAU_MEMPOOL_RULE_LANE_MAX_FRACTION of the cap (default 0.1) and overflow inside it is evicted from its own rows, so a burst of rule transactions cannot evict queued fee-paying transfers. Otherwise the oldest pending tx is evicted (FIFO, fee-blind; reserved txs are never evicted).
  • Per-block rule budget — a block may carry at most max_rule_txs_per_block rule-sharing transactions (default 8, governance-patchable, bound into the state hash when non-default). This is block validity, not policy: without it a proposer could author a block every validator times out on. Legacy operations["0"] user_tx rules are deliberately not counted, so historical blocks still replay. A proposer may be more conservative via TAU_BLOCK_MAX_RULE_TXS, never more permissive.
  • Sequence contiguity under quotas — lane quotas can leave a sender's lower-numbered tx behind while including a higher one. The engine hard-rejects such a gap and the tx would be disposed with the block, so the builder defers the whole sender instead and it waits for its predecessor.
  • Expiry — expired txs are pruned opportunistically on insert (no background reaper).
  • Persistence — the mempool survives restart (same SQLite file) and is not re-validated on boot.
  • Re-validation — the proposer fully re-executes and re-validates every tx at block-build time, so admission is best-effort, not authoritative.

Features

  • Tau-driven pluggable consensus — block validity (o6) and proposer eligibility (o7) decided by living consensus_rules; fully on-chain governance via consensus_rule_update / consensus_rule_vote with activation delays and a vote quorum.
  • On-chain PoA validator set — proposers are gated against the active validator set; set membership changes only through governance.
  • Persistent blockchain & fork choice — SQLite-backed chain, multiple competing tips, heaviest-valid-branch fork choice with deterministic tie-break, seamless reorgs (revert/re-apply state).
  • P2P networking (libp2p) — protocols: handshake, ping, sync, blocks, tx, gossip; gossip topics tau/blocks/1.0.0 and tau/transactions/1.0.0; genesis-hash handshake gate; bootstrap retry/backoff; NAT announce addresses; DHT-backed state/provider records. Block numbers are 0-indexed; sync decisions use head_hash, not just head_number.
  • Authenticated transactions & blocks — BLS12-381 signatures over canonical payloads (details); transactions carry per-account sequence numbers (replay protection) and expiration; blocks carry a proposer signature verified before they affect the head. Signatures are not yet domain-separated across networks — see the replay-risk note in Cryptography & signatures.
  • Typed transactions — user_tx, consensus_rule_update, consensus_rule_vote, rule_offer, rule_offer_accept, rule_offer_reject; common fields sender_pubkey, sequence_number, expiration_time, expire_at_height, fee_limit, signature.
  • Rule sharing — send a Tau rule to another user, who checks its conflict status and then accepts it into their own specification or rejects it. See Rule sharing.
  • Transaction lanes — coin transfers and rule-bearing transactions are selected, evicted and gossiped under separate quotas, so rule work cannot delay transfers. See Mempool policy.
  • Hardening — block proposer-signature verification, governance vote-quorum bound into the state hash, consensus-enforced activation delay, genesis-hash handshake gate; SQLite WAL + busy timeout, refuse-to-start on schema drift (no silent data loss), mempool size cap + expiry pruning, admission size limits, production guards on dev/test flags.

DHT configuration & gossip health

The libp2p DHT layer exposes runtime knobs through NetworkConfig:

Option Default Purpose
dht_refresh_interval 60.0 Seconds between routing-table refreshes.
dht_bucket_refresh_interval dht_refresh_interval Stale-peer refresh/eviction interval.
dht_bucket_refresh_limit 8 Max stale peers revalidated per cycle.
dht_stale_peer_threshold 3600.0 Age before a peer is considered stale.
dht_opportunistic_cooldown 120.0 Min time between reseeding the same gossip/handshake-discovered peer.
gossip_health_window 120.0 Sliding window for get_metrics_snapshot() gossip health.

NetworkService.get_metrics_snapshot() (or the periodic [metrics] log line) reports gossip activity, routing-table counts, and bucket refresh results.


Response envelope

Node commands reply with a single-line JSON envelope (TCP appends \r\n; WebSocket emits it raw):

{"status":"ok","command":"getbalance","data":{"address":"a63b...ea73","balance":"1000"}}
{"status":"error","command":"sendtx","error":{"code":"INVALID_SEQUENCE","message":"Invalid sequence number: expected 5, got 4.","details":{"expected":5,"received":4}}}
  • status is "ok" or "error"; command echoes the request; data on success, error.code/error.message(+details) on failure.
  • Types: addresses/hashes/IDs and token amounts are strings (overflow-safe); counts/heights/sequence numbers are integers; timestamps are ISO 8601 strings.
  • Error codes: INVALID_PARAMS, PARSE_ERROR, INVALID_SIGNATURE, INVALID_SEQUENCE, TX_EXPIRED, TX_REJECTED, TX_INVALID, BLS_UNAVAILABLE, FEE_LIMIT_TOO_LOW, FEE_RULE_ERROR, INSUFFICIENT_FUNDS, DUPLICATE_UPDATE, UNSCOPED_USER_RULE, TEMPORAL_REFERENCE, ADMISSION_TIMEOUT, ADMISSION_UNAVAILABLE, MINING_NOT_ELIGIBLE, MINING_BUSY, MEMPOOL_EMPTY, MEMPOOL_FULL, MINING_CONFIG_ERROR, MINING_FAILED, BLOCK_NOT_CREATED, GOVERNANCE_ERROR, OFFER_UNKNOWN, FORBIDDEN, COMM_TIMEOUT, TIMEOUT, UNKNOWN_COMMAND, RATE_LIMITED, INTERNAL_ERROR.
  • DUPLICATE_UPDATE carries details.update_id and details.lifecycle_state, so a wallet retrying a consensus_rule_update can tell "already landed" from a real rejection. ADMISSION_TIMEOUT / ADMISSION_UNAVAILABLE mean the isolated rule compile exceeded its bound or could not be run — the transaction was not admitted, and a retry is reasonable.
  • The hello version=N handshake stays plain text (versions 1 and 2).

createblock takes one optional argument: createblock allow-empty. Block production is serialized node-wide (one round at a time; a concurrent caller gets MINING_BUSY rather than building a competing block at the same height), and an empty mempool is refused with MEMPOOL_EMPTY unless allow-empty is passed — that flag exists for advancing height, e.g. to a governance activation boundary. Tune the waits with TAU_BLOCK_PRODUCTION_LOCK_TIMEOUT (default 0, try-lock) and TAU_CHAIN_INGEST_LOCK_TIMEOUT (default 30).

Pending-aware wallet queries

getbalance <address> returns only the confirmed chain balance. For a wallet view that accounts for queued sends, use the additive commands (both read-only):

  • getaccountstate <address> — confirmed balance plus the mempool view:

    {"status":"ok","command":"getaccountstate","data":{
      "address":"a63b...ea73","chain_balance":"1000","pending_outgoing":"101",
      "pending_incoming":"0","pending_fees":"2","available_balance":"897",
      "pending_txs":[{"hash":"…","direction":"outgoing","amount":"101","fee":"2",
                      "status":"queued","sequence_number":3,"tx_type":"user_tx",
                      "received_at":"…","expires_at":"…"}]}}

    pending_outgoing is the transfer total only and pending_fees the sender's admission fee estimate; together they mirror the admission check (balance >= transfers + estimated_fee), and available_balance = max(0, chain_balance − pending_outgoing − pending_fees). Prefer chain_balance − available_balance as the authoritative "not currently spendable" amount — it is stable regardless of how the constituent fields are split. Unconfirmed pending_incoming is reported but never counted as spendable. Amounts are strings; direction is outgoing / incoming / self.

  • gettxstatus <tx_hash> — lifecycle of a transaction by hash. data.status is one of queued (in the mempool, incl. reserved-for-mining), confirmed (with block_hash/block_number/confirmations), expired, evicted, rejected, or unknown. unknown is a normal answer, not an error.

sendtx returns the computed tx_hash in its success data, so a wallet can poll gettxstatus immediately after queuing.

  • checktx <json_payload> — read-only admission dry-run: would sendtx accept this? Nothing enters the mempool, nothing is broadcast, nothing is charged. Rejections carry the identical code/details sendtx would have returned. Tau evaluation is not performed (tau_evaluated: false), so there is no estimated_fee and no o5 policy verdict — those steps mutate interpreter and intern state, which a free unauthenticated RPC must not do. Use it as the pre-flight before asking a user to sign; gettxstatus is the post-flight.

Block structure

A block header carries block_number (0-indexed height), previous_hash, timestamp, merkle_root, state_hash (the full post-state commitment — see State hash & canonical encoding), and state_locator (a DHT lookup hint, not consensus data). The body carries the ordered transactions and their tx_ids. Consensus is attested by consensus_proof — the proposer's BLS signature over the canonical header, verified by Block.verify_consensus_proof() and gated by the o6 = i10 rule. There is no proof-of-work; validity and proposer eligibility are decided by the Tau consensus program at each height. See block.py (Block, BlockHeader, compute_tx_hash, compute_merkle_root).


Development

# fast unit suite (Trio; disable the asyncio plugin)
./venv/bin/python3 -m pytest -p no:asyncio

# with the real Tau engine, point at the native bindings
PYTHONPATH=../tau-lang/build-Release/bindings/python/nanobind \
  ./venv/bin/python3 -m pytest -p no:asyncio

CI builds tau-lang and runs the consensus suite against the real engine (.github/workflows/ci.yml). The full suite in a single process can hit a known pre-existing native segfault under sustained Tau use; run per-file if you see it.

Key components: server.py (TCP/WS server), tau_manager.py + tau_native.py (Tau engine lifecycle/IPC), commands/ (RPC handlers incl. sendtx, createblock), consensus/ (engine, governance, fees, admission), chain_state.py (balances/sequences/rebuild), network/ (libp2p service, DHT, gossip), db.py (SQLite), block.py, config.py, rules/ (built-in transfer-validation Tau rules), scripts/gen_genesis.py, tests/.

Submitting issues

Tau Testnet issues.

Production safety checklist

TAU_ENV=production enforces exactly one thing in code: it refuses to boot with TAU_FORCE_TEST=1 (no mock Tau in production). Every other safe default below is global, not production-gated, and several can be overridden by env vars with no guard — so verify them yourself before exposing a node:

  • Network id — set a unique TAU_NETWORK_ID (the tau-local default is not rejected).
  • Validator key — supply your own TAU_MINER_PRIVKEY(_PATH) / TAU_MINER_PUBKEY; the shipped data/test_miner.key is not refused.
  • RPC/WS bind — keep TAU_HOST=127.0.0.1 (default) or firewall it; 0.0.0.0 is accepted with no warning. In Docker, publish RPC/WS only on 127.0.0.1 (see quickstart).
  • WS origins — set TAU_WS_ALLOWED_ORIGINS explicitly. The check is substring-based and always allows no-Origin clients and any localhost/127.0.0.1 origin.
  • WSS — set both TAU_WS_CERT_PATH and TAU_WS_KEY_PATH; a partial/invalid TLS config silently downgrades to plaintext WS.
  • Governance — keep TAU_GOVERNANCE_OPEN_ADMISSION=false (production only warns if it is on alongside bootstrap peers).
  • Remote createblock — leave TAU_ALLOW_REMOTE_CREATEBLOCK=false; the loopback gate is the only protection.
  • Identity & DB — set/back up TAU_IDENTITY_KEY_PATH and TAU_DB_PATH; both default into data/ and are auto-created.
  • Rate limiting — the plain TCP RPC path has none; the WS limiter is a fixed 5 req/s per connection (reset on reconnect). Front it with a proxy if exposed.

Security & risk model

This is alpha, non-finalized PoA. Tokens have no value; the cryptography and consensus are unaudited.

Fork choice & finality. The "heaviest-valid-branch" weight is simply block height, with ties broken on the lexicographically smallest block hash (fork_choice_scheme: height_then_hash). There is no finality and no checkpoints — a reorg can run as deep as the common ancestor (bounded only by a 2000-block path cap in get_chain_path, beyond which the switch is refused). A validator controlling enough proposer slots can build a competing branch; honest nodes switch to a longer valid one. Reorgs revert and re-apply full state by replaying from genesis along the new path and commit it in a single DB transaction; the fast extend path writes the block and the head pointer in two transactions and relies on replay-on-restart for crash consistency.

P2P threat model.

  • Authentication — transport is libp2p (Ed25519 peer identity). The application handshake gates peers on matching network_id and genesis_hash; a peer presenting an empty/missing or mismatching genesis is rejected once this node has a genesis (fork protection).
  • Block/tx relay — gossiped transactions pass through the same admission path as RPC submissions; blocks are fully validated (proposer signature, validator-set membership, state_hash, fees) before they can affect the canonical head. Invalid blocks are rejected and never committed, but there is no peer scoring/banning yet — a peer can keep re-sending junk (throttled only at the libp2p connection level, TAU_RATE_LIMIT_PER_PEER).
  • DHT records are discovery hints, not trusted consensus data — advertised state is re-validated against the local chain before use; provider/peer records are capped and truncated on intake to bound poisoning.
  • Not yet addressed — eclipse/Sybil resistance (bootstrap peers are trusted for discovery) and per-peer block/header quarantine.

RPC / WebSocket exposure

RPC (TCP, default 127.0.0.1:65432) and WebSocket (+1 → 65433) bind to TAU_HOST. There is no per-call authentication; the only network gate is that createblock is refused from non-loopback clients (and always over WS) unless TAU_ALLOW_REMOTE_CREATEBLOCK=true, decided from the real socket peer address (not header-spoofable). The web wallet keeps private keys client-side and signs locally — keys are never sent to the node. Treat RPC/WS as a loopback / trusted-LAN surface; front it with TLS and an origin allowlist if you must expose it.

Known alpha risks: no economic finality; no slashing; possible deep reorgs; unaudited cryptography and consensus; no cross-network signature domain separation (do not reuse keys across networks); a native segfault in the in-process consensus interpreter stops the node process (block evaluation runs in isolated workers); and a voted-in consensus rule that emits an invalid o9 (or otherwise fails at runtime) can halt progress at a height until governance corrects it.


Project status

Alpha — active development, for testing and experimentation. In place: Tau-driven consensus, on-chain PoA validator set with governance quorum, native fee model, BLS-authenticated transactions, persistent chain with fork choice/reorgs, multi-node P2P sync.

Future work

  • Richer Tau logic (verified multiplication → percentage fees; finality/checkpoints).
  • Validator rotation policies and stronger fork-choice/finality rules.
  • Metrics/observability endpoints and operator runbooks.
  • Broader integration-test coverage.

About

This project is the codebase for the Tau Testnet Alpha Blockchain

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages