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 usero8), 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.
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-standaloneThe 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:65432publishes the port on every host interface (0.0.0.0). The RPC/WS API has no authentication beyond a loopback gate oncreateblock; 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-standaloneTo 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 keypairTo join a network instead of running isolated, jump to Join the Tau Testnet. To run without Docker, see Run from source.
| 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 |
- 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).
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 — keepdocker runon 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.
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` CLIOption 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 --testOption 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 chaintau-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.
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_hashandTAU_NETWORK_IDor 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.
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 runon one line (or break lines with a backtick`instead of\); keep the single quotes around theTAU_BOOTSTRAP_PEERSandbash -cvalues.
RPC is exposed on localhost:65432, WebSocket on 65433.
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.shjoin.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-isolatedThe 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).
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-testnetCLI. To query a Docker node, install the client on the host withpip install -e .(notau-langbuild 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.
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-isolatedFor 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.
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_IDand the three genesis artifacts (which pin the validator set, thevote_quorumpolicy, and the base fee). KeepTAU_GOVERNANCE_OPEN_ADMISSION=falseon a public network. Thevote_quorumpolicy 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.
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.
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 statusFull 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.
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>:65433python 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.
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-privkeyto 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-feelikewise (0–16777215). - Adding a validator after launch: an existing validator proposes a
consensus_rule_updatewithhost_contract_patch.validator_additions; once it reaches the vote quorum (supermajority= ⌈2N/3⌉ by default, ormajority) it activates. Removal works symmetrically.
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.
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).
"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 iso6 = 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 wholeuser_tx. Lets users express recipient whitelists (i4), time-locks (i5), and spending limits (i1). See WALLET_USAGE.md. Tiered co-signature approvals build ono5: 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/o8are shared streams composed across every deployed rule, so rule text writing them must referencei12(sender pubkey) ori3(from address). Without that, onealways (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]ori1[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 asi10; theo6 = i10rule 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_limitadmission 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_hashcomputation 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 norequire_bls_sigescape clause) ⇒ rejected. - An invalid consensus fee (
o9) raisesFeeRuleError: 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.
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 theprevious_hashchain 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_quorumpolicy is now folded intostate_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 withscripts/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_limittravel as decimal strings, andtx_id/signatures are computed over the raw JSON without type-normalization. A client that sends100instead of"100", or uppercase hex, produces a differenttx_idand 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.
- Scheme: BLS12-381 via
py_eccG2Basic(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_g2is descriptive metadata — the scheme is fixed in code. - Validation:
Verifyruns public-key and signature subgroup checks and rejects the point at infinity; transaction ingest additionally runsKeyValidateonsender_pubkeyand both transfer addresses. Malformed keys/signatures fail closed (INVALID_SIGNATURE); ifpy_eccis unavailable, ingest is refused (BLS_UNAVAILABLE), never skipped. - Signed payloads:
- Transaction →
SHA-256of canonical JSON of{sender_pubkey, sequence_number, expiration_time, expire_at_height, fee_limit, tx_type, …type-specific}.tx_typeis signed, so auser_txsignature cannot be replayed as a governance vote.expire_at_heightis signed whenever present, and required on anything new: the wall-clockexpiration_timeis rechecked at apply against the block timestamp, which the proposer picks, so the height is the deadline a proposer cannot move. - Block header →
SHA-256ofBlockHeader.canonical_bytes(), verified byBlock.verify_consensus_proof().
- Transaction →
⚠️ No cross-network / version domain separation (replay risk). Signed payloads do not includenetwork_id,genesis_hash, or a protocol version, and there is no explicittx-vs-blockdomain 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.
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 streamo9(genesis defaultalways (o9[t]:bv[24] = { #x00000a }:bv[24]).→ 10/step). Change it via aconsensus_rule_updatecarrying the full new consensus spec, voted to its activation height. Absento9→ fee model inactive (0). A present-but-invalido9is 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 ono8. Absent → 0 silently; invalid → 0 with a node-side warning. Must be sender-scoped (referencei12ori3), enforced at admission asUNSCOPED_USER_RULE:o8is composed across all deployed rules, so a flat unconditionalalways (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-lessuser_txis charged one fee-query step. Multi-transfer txs pay N× by design. fee_limitis a cap. Rejected (at admission and in-block) iftotal_fee > fee_limit; the sender paystotal_fee, not the cap. Every tx carries a validfee_limit; onlyuser_txis charged (validators never need funds to govern).- Credited to
block.header.proposer_pubkey, or to a governance-setfee_beneficiarywhen one is active — ahost_contract_patchcarrying"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 fromgetgovernance. 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 pubkeysbv[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 evaluatesi2against the current head, which is advisory in the same sense asi5; that drift only matters for fees, so rule text readingi2is rejected when it also writeso8, and consensuso9revisions readingi2stay rejected.i3/i4/i5are immutable-per-transfer / consensus-injected, hence identical at admission and apply, and may be read freely. Scope oni12/i3/i4, gate oni5, tier oni1. Admission errorFEE_LIMIT_TOO_LOWreturns the computedrequired_fee. - Mempool priority is by admission-time estimate (
estimated_fee DESC, fee_limit DESC, received_at ASC); inflatingfee_limitdoes 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_shrare all in the grammar, so that premise is unverified rather than established.tests/test_o9_arithmetic_probe_native.pysettles it against the real engine; it needs native bindings built for the interpreter running the suite. Note a computedo9needs no width change — the live rule is alreadybv[24], and Tau arithmetic unifies operands within one bv family. User rule text referencingo6/o7/o9is rejected at admission, as is a fee rule readingi2.
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
scheduledneeds 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_heightarrives 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
o9halts 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.
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 ────┘
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:
- An output stream that no clause constrains for a given input still materializes, with an arbitrary witness — observed as
0, which foro5is exactlyUSER_POLICY_BLOCK_VALUE. Soalways ( (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. - 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
i0the later one silently supersedes the earlier. So appending one guarded unit per user cannot work either. - A single nested total-form unit does work, and applying it through
i0is 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.
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.
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.
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 5000Semantics:
- 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
o5policy 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 idandgetretractableidcan compute it before the park is signed. The id is printed byretractable 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;gettxstatusreports a confirmed park's id and state;getaccountstatereportsescrowed_outandescrowed_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.
- Lanes — every tx is classified
fast(coin transfers) orslow(anything carrying Tau rule text:rule_offer,rule_offer_accept,consensus_rule_update, or auser_txwith a non-emptyoperations["0"]). Auser_txcarrying both a rule and transfers isslow— 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 owno8user 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. Inflatingfee_limitdoes 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_idis 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 mostTAU_MEMPOOL_RULE_LANE_MAX_FRACTIONof 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_blockrule-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. Legacyoperations["0"]user_txrules are deliberately not counted, so historical blocks still replay. A proposer may be more conservative viaTAU_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.
- Tau-driven pluggable consensus — block validity (
o6) and proposer eligibility (o7) decided by livingconsensus_rules; fully on-chain governance viaconsensus_rule_update/consensus_rule_votewith 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 topicstau/blocks/1.0.0andtau/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 usehead_hash, not justhead_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 fieldssender_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.
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.
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}}}statusis"ok"or"error";commandechoes the request;dataon 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_UPDATEcarriesdetails.update_idanddetails.lifecycle_state, so a wallet retrying aconsensus_rule_updatecan tell "already landed" from a real rejection.ADMISSION_TIMEOUT/ADMISSION_UNAVAILABLEmean 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=Nhandshake 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).
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_outgoingis the transfer total only andpending_feesthe sender's admission fee estimate; together they mirror the admission check (balance >= transfers + estimated_fee), andavailable_balance = max(0, chain_balance − pending_outgoing − pending_fees). Preferchain_balance − available_balanceas the authoritative "not currently spendable" amount — it is stable regardless of how the constituent fields are split. Unconfirmedpending_incomingis reported but never counted as spendable. Amounts are strings;directionisoutgoing/incoming/self. -
gettxstatus <tx_hash>— lifecycle of a transaction by hash.data.statusis one ofqueued(in the mempool, incl. reserved-for-mining),confirmed(withblock_hash/block_number/confirmations),expired,evicted,rejected, orunknown.unknownis 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: wouldsendtxaccept this? Nothing enters the mempool, nothing is broadcast, nothing is charged. Rejections carry the identicalcode/detailssendtxwould have returned. Tau evaluation is not performed (tau_evaluated: false), so there is noestimated_feeand noo5policy 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;gettxstatusis the post-flight.
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).
# 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:asyncioCI 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/.
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(thetau-localdefault is not rejected). - Validator key — supply your own
TAU_MINER_PRIVKEY(_PATH)/TAU_MINER_PUBKEY; the shippeddata/test_miner.keyis not refused. - RPC/WS bind — keep
TAU_HOST=127.0.0.1(default) or firewall it;0.0.0.0is accepted with no warning. In Docker, publish RPC/WS only on127.0.0.1(see quickstart). - WS origins — set
TAU_WS_ALLOWED_ORIGINSexplicitly. The check is substring-based and always allows no-Origin clients and anylocalhost/127.0.0.1origin. - WSS — set both
TAU_WS_CERT_PATHandTAU_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_PATHandTAU_DB_PATH; both default intodata/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.
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_idandgenesis_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 (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.
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.
- 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.
