Exfer is a public blockchain built for machines, not people, to spend on.
When an autonomous agent — a trading bot, a service that pays per API call, an IoT device buying its own electricity — needs to pay another agent, the conventions humans have learned to tolerate stop working. A wallet UI doesn't fit. A fee auction doesn't fit. A smart-contract VM whose execution cost depends on global state doesn't fit. An agent has to construct a transaction, compute its exact cost in advance, and know with certainty that it will validate — no human present to retry, raise the fee, or read an error.
Exfer is what's left when you design a chain around that constraint.
- No fee auction. Fees are fixed per transaction, FIFO within a band, protocol-enforced minimum. An agent can pre-fund itself accurately.
- No gas estimation. Costs are statically computable from the serialized transaction. There is no Turing-complete VM and no global mutable state to simulate against.
- Total functional scripting. Every spending condition is expressed in Exfer Script — a combinator language in which every script terminates and every cost is bounded before execution. HTLC, multisig, vault, escrow, and delegation are first-class CLI commands, not contracts you write and deploy.
- Extended UTXO. The data model is Bitcoin-style — each transaction consumes and creates discrete outputs — so reasoning about state is local, parallel, and reentrancy-free.
- Argon2id memory-hard proof-of-work. Mining is CPU-friendly and resistant to the kind of ASIC concentration that locks small operators out.
- 10-second target block time. Predictable confirmation cadence. Retargets every 4 320 blocks.
The result is a chain where an agent's day looks like: fetch UTXO → build tx → compute exact fee → sign → submit → confirmed, with no surprises in between.
Don't want to read all this? Use an AI agent
Exfer is designed for machines. The docs are no different — if you'd rather have an LLM read them and answer your specific question, here's a prompt that primes Claude, ChatGPT, Cursor, or any agent that can browse URLs to be an Exfer expert.
You are an expert on Exfer, a public proof-of-work blockchain designed for machine-to-machine payments. Help the user with their Exfer-related question.AUTHORITATIVE REFERENCES (load these before answering):
- Protocol spec: https://exfer.org/
- CLI cookbook: https://exfer.org/SKILL.html
- JSON-RPC docs: https://doc.exfer.site/rpc/
- Source code: https://github.com/ahuman-exfer/exfer
KEY FACTS YOU SHOULD KNOW:
- Exfer is NOT Bitcoin-compatible. Don't assume Bitcoin Core RPC names.
- Extended-UTXO model. Scripts are total functional combinators — they always terminate, and their cost is static.
- The JSON-RPC interface has exactly 7 methods: get_block_height, get_block, get_transaction, get_balance, get_address_utxos, get_script_utxos, send_raw_transaction.
- 1 EXFER = 100_000_000 exfers. Target block time 10s. Coinbase maturity 360 blocks.
- For automation: use --json on every CLI command, and pass the wallet passphrase via EXFER_PASS + --passphrase-env.
- Native script patterns (no smart-contract VM needed): HTLC, multisig
(2-of-2, 1-of-2, 2-of-3), vault, escrow, delegation. All exposed as
exfer script <pattern>-{lock,spend,claim,reclaim,...}CLI commands.
When you give CLI examples, prefer the patterns shown in SKILL.html. When you give RPC examples, match the parameter shapes documented at doc.exfer.site/rpc/. If the user is doing exchange / wallet / explorer integration, point them at doc.exfer.site/integrate/.
The user's question follows:
Where to go from here
This site is a community-maintained reference and live explorer. Pick the entry point that matches what you're doing.
| First time | Quick start |
| No install | Web wallet at https://exfer.dev, explorer at https://explorer.exfer.dev |
| Local wallet | Create a wallet |
| Receive a payment | Receive a payment |
| Send a payment | Send a payment |
| Run a full node | Install |
| Mine | How Exfer mining works |
| Build an exchange / wallet / explorer | Integrate Exfer |
| Need API details | JSON-RPC reference |
A few quick facts
- Genesis block:
d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051 - Network ports: P2P
9333, JSON-RPC9334(optional) - Units:
1 EXFER = 100 000 000 exfers - Initial block reward: 100 EXFER, halving roughly every 2 years
(
6 307 200blocks), floor at 1 EXFER - Coinbase maturity: 360 blocks (~1 hour)
- Live community RPC nodes: see Live nodes, with a "try it" widget on every method in the JSON-RPC reference so you can call any endpoint without setting up a node first
How this site is organized
The sidebar follows the typical user journey:
- Getting Started — install the binary, create a wallet, send and receive payments. About 30 minutes from zero.
- Run a Node — install, sync, run as a system service, back up, troubleshoot.
- Mining — what Argon2id mining means in practice, solo, and pools.
- Script patterns — HTLC, multisig, vault, escrow, delegation walkthroughs.
- Integrate Exfer — exchange, wallet, and explorer integration recipes built on the JSON-RPC surface.
- Tools — live community-node dashboard, web wallet, block explorer.
- Reference — JSON-RPC API with try-it widgets, protocol spec link, CLI command list.
- About — FAQ, glossary, contributing.
Caveats
- Community-maintained, not officially affiliated. Authoritative
sources are the upstream
EXFER.md(protocol spec) andSKILL.md(CLI cookbook). This site renders them, plus original content, for convenience. - Public RPC nodes are best-effort and rate-limited. For production integrations, run your own node.
- No formal security audit at this writing. The project is young. Treat substantial value accordingly — see Backup & recovery and Vault.
Quick start
This walkthrough takes you from nothing installed to a running Exfer node with a wallet that's ready to receive payments. About five minutes of your time, plus however long the initial sync takes.
We use the pre-built Linux binary below; macOS and Windows downloads work the same way and are covered in Install. If you prefer to compile from source, that's there too.
1. Download the binary
curl -LO https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-linux-x86_64
chmod +x exfer-linux-x86_64
mv exfer-linux-x86_64 exfer
That's a single self-contained binary — no system dependencies beyond the
usual C runtime. Place it wherever you like (./exfer is fine for now,
but moving it to /usr/local/bin/exfer makes it available everywhere).
2. Bootstrap a node and wallet in one command
./exfer init
init creates an encrypted wallet, starts a full node, and begins syncing
the chain. You'll be prompted for a wallet passphrase — pick something
strong and write it down somewhere durable, because there is no seed
phrase recovery; the encrypted key file plus the passphrase are the only
things between you and your funds.
If you're scripting this — for CI, an agent, or a server — pass the passphrase non-interactively instead:
EXFER_PASS="your-passphrase" ./exfer init --passphrase-env EXFER_PASS --json
By default everything lands under ~/.exfer/: the wallet at
~/.exfer/wallet.key and the chain data alongside it. Add --mine if
you want to start mining at the same time.
3. Watch it sync
The first time a node starts, it goes through an initial block
download (IBD): finding peers, fetching headers, then fetching block
bodies, validating as it goes. Peers are discovered automatically through
DNS (seed.exfer.org) — you don't need to specify them.
You'll see this in the log when the cold-sync portion finishes:
INFO exfer: Sync manager: cold-bootstrap IBD complete
INFO exfer: IBD complete at height 559300
After that, new blocks arrive at roughly the 10-second target cadence.
You can check progress from another terminal:
curl -s -X POST http://127.0.0.1:9334 \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block_height","params":{},"id":1}'
Compare the returned height against any community node.
When the two are within a block or two of each other, you're caught up.
4. Grab your address
./exfer wallet info --wallet ~/.exfer/wallet.key --json
The output:
{
"address": "8d896d64864f53214acb49aeb44a09a03d5bb23d19a417a6ce7b0da65c7bd750",
"pubkey": "fcbd5a818501cd5439ebe8c0c5ff244c0f1475333e226b7f998e6eb80552c69d"
}
The 64-hex address is what you give to anyone paying you. The pubkey
is what you'd configure on a mining server (see Solo mine)
so that the server never needs to hold your private key.
5. You're done
You now have:
- a full node listening on
9333/tcpfor P2P and9334/tcpfor RPC (localhost only), - an encrypted wallet at
~/.exfer/wallet.key, - an address that others can pay.
Where to go next depends on what you want to do:
- Get paid → Receive a payment walks through sharing the address and watching for the incoming transaction.
- Pay someone → Send a payment, once you have some EXFER in the wallet.
- Survive reboots → Run as a service makes the node start automatically and restart on failure.
- Don't lose the wallet → Backup & recovery explains the three things to back up and which ones matter most.
Create a wallet
A wallet is a single Ed25519 keypair stored in an encrypted file. Each wallet file = one keypair = one address. (Exfer is not a BIP-32 HD wallet — if you want multiple addresses, generate multiple wallet files. See Wallet developer guide for HD-style address derivation.)
Generate
Encrypted (recommended):
exfer wallet generate --output ~/my-wallet.key --json
The CLI prompts twice for a passphrase. Pick something strong and write it down — the file is useless without it.
For non-interactive scripts (CI, agents):
EXFER_PASS="your-passphrase" exfer wallet generate --output ~/my-wallet.key --json
# (the CLI does not read EXFER_PASS automatically — see below for env-var
# flow with `exfer init`)
Unencrypted (only for ephemeral testing):
exfer wallet generate --output /tmp/test.key --no-encrypt --json
Never store production funds in an unencrypted wallet.
What you get back
{
"file": "/home/you/my-wallet.key",
"address": "8d896d64864f53214acb49aeb44a09a03d5bb23d19a417a6ce7b0da65c7bd750",
"pubkey": "fcbd5a818501cd5439ebe8c0c5ff244c0f1475333e226b7f998e6eb80552c69d"
}
| Field | What it's for |
|---|---|
file | The path to the encrypted key file. Back this up. |
address | 64-hex (32 bytes). Share this with people who pay you. |
pubkey | The Ed25519 public key. Used when mining (--miner-pubkey) so the mining server never needs the private key. |
The private key stays inside the encrypted .key file and is decrypted
on demand by the CLI when you sign a transaction.
Back it up immediately
cp ~/my-wallet.key ~/Documents/exfer-wallet.key.backup
# or onto a USB stick, etc.
The file is encrypted — it's safe to put on cloud storage, but you still have to remember the passphrase. There is no recovery if you lose both.
See Backup & recovery for the full backup strategy (wallet, node identity, chain data).
Inspect an existing wallet
exfer wallet info --wallet ~/my-wallet.key --json
Shows the address and pubkey. Requires the passphrase if the file is encrypted.
Why a single keypair per file?
Exfer's wallet model is intentionally simple. If you need many addresses (e.g. one per customer in an exchange), generate many wallet files and track the mapping yourself. See For wallet developers for HD-style derivation patterns.
Common pitfalls
- Losing the passphrase — you cannot recover. Treat passphrase loss as fund loss.
- Storing only encrypted backups in one place — disk crash kills the wallet. Keep at least two copies in physically different locations.
- Reusing the same wallet for mining and spending — fine technically,
but mining requires only the pubkey on the server. Keep the private key
off the mining machine and use
--miner-pubkeyinstead. See Solo mine.
Receive a payment
Receiving is the easy half: share your address, then watch for the incoming transaction.
Share your address
Get your address:
exfer wallet info --wallet ~/my-wallet.key --json
Output:
{
"address": "8d896d64864f53214acb49aeb44a09a03d5bb23d19a417a6ce7b0da65c7bd750"
}
That 64-hex string is what the sender pays. It's a hash of your public key, so handing it out does not reveal your pubkey or private key.
Watch for the payment to arrive
Polling your balance every few seconds works fine for casual use. Use a community RPC node so you don't have to wait for a local node to sync first:
RPC="http://82.221.100.201:9334"
exfer wallet balance --wallet ~/my-wallet.key --rpc "$RPC" --json
Output once the payment hits:
{
"address": "8d896d64...",
"balance": 1000000000,
"source": "rpc",
"rpc_url": "http://82.221.100.201:9334"
}
balance is in exfers. Divide by 100_000_000 for EXFER:
1_000_000_000 exfers = 10 EXFER.
Wait for confirmations
A transaction in the mempool can still be reorged or replaced. Wait for
N confirmations before treating the funds as settled:
| Use case | Confirmations | Wall time |
|---|---|---|
| Casual receipt | 30 | ~5 min |
| Standard value | 60 | ~10 min |
| Large / irreversible | 360 | ~1 hr |
See Confirmation & reorg semantics for details.
Watch a specific incoming transaction
If the sender shares the tx_id, you can poll its status directly. See
Send a payment → Wait for confirmation
for the polling loop.
Build it into a service
For exchanges or services that need to credit user balances on deposit, read For exchanges. It walks through:
- generating one wallet per user
- the recommended block-scan deposit-detection loop
- handling reorgs after credit
Send a payment
You need:
- An encrypted wallet (see Create a wallet)
- Enough EXFER in it (
exfer wallet balance ...) - The recipient's 64-hex address
- A reachable RPC node (your own, or a community node)
One-liner
RPC="http://82.221.100.201:9334"
exfer wallet send \
--wallet ~/my-wallet.key \
--to 8d896d64864f53214acb49aeb44a09a03d5bb23d19a417a6ce7b0da65c7bd750 \
--amount "10 EXFER" \
--fee "0.001 EXFER" \
--rpc "$RPC" \
--json
You'll be prompted for the wallet passphrase. The transaction is signed locally (the private key never leaves your machine) and submitted to the RPC node, which broadcasts it.
Output:
{
"tx_id": "fb8a634fcce6cfc124de86fa0a4b3e6130a1e6bfda68a34dc4f30ec7a2a2b68c",
"size": 227,
"tip_height": 5553,
"submitted": true
}
Save the tx_id — you'll use it to track confirmation.
Flag reference
| Flag | What it does |
|---|---|
--wallet | Path to your encrypted .key file |
--to | Recipient's 64-hex address |
--amount | Amount to send. Accepts "10 EXFER" or "1000000000" (exfers) |
--fee | Transaction fee. Default 0.001 EXFER (100_000 exfers). Higher = faster inclusion under load |
--rpc | RPC node URL. Fetches your UTXOs and submits the tx |
--datadir | Use a local node instead of RPC (--rpc and --datadir are mutually exclusive) |
--json | Machine-readable output |
Wait for confirmation
Poll get_transaction until block_height appears:
TX_ID="fb8a634fcce6cfc124de86fa0a4b3e6130a1e6bfda68a34dc4f30ec7a2a2b68c"
for i in $(seq 1 60); do
RESULT=$(curl -s -X POST "$RPC" \
-H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"get_transaction\",\"params\":{\"hash\":\"$TX_ID\"},\"id\":1}")
if echo "$RESULT" | grep -q '"block_height"'; then
echo "Confirmed: $RESULT"
break
fi
echo "Pending… ($i/60)"
sleep 10
done
Once block_height is set, the transaction is in a block. Wait for the
number of confirmations appropriate for your value (see
Confirmation depth).
Fees
Exfer uses a per-transaction fee, not per-byte. There is no fee auction:
- Mempool ordering: FIFO within a fee-rate band; higher fees go first.
- Minimum: consensus-enforced minimum relay fee.
- Typical:
0.001 EXFER(100_000exfers) clears reliably under normal load.
Under sustained mempool pressure, bumping the fee gets you in the next block sooner. Replace-by-fee is not currently supported — if your tx is stuck, wait for it to drop from mempool (no eviction-based replacement yet).
What's actually happening under the hood
exfer wallet senddecrypts your wallet using the passphrase.- Calls
get_address_utxosover RPC to find your spendable outputs. - Selects enough UTXOs to cover
amount + fee(skips immature coinbases). - Builds an unsigned transaction with one output to
--to, one change output back to you, and a fee output to the miner. - Signs every input locally with your Ed25519 private key.
- Calls
send_raw_transactionwith the signed tx hex. - Returns the
tx_idto you.
The private key only ever decrypts in step 1 and signs in step 5, both in your local process.
Common errors
insufficient funds— your balance minus fee is less than the requested amount. Check withexfer wallet balance.Mempool pre-check failed: duplicate tx— you submitted the same signed tx twice. Wait for the first one to confirm.Transaction validation failed: input ... is immature coinbase— you tried to spend a coinbase output before 360 confirmations. Either wait or fund the wallet from a non-coinbase source.
For the full list of error codes, see Error codes.
Install
Pre-built binaries are published for Linux x86_64, macOS (Apple Silicon and Intel), and Windows x86_64. Build-from-source is also documented for ARM Linux or other targets.
Linux x86_64
curl -LO https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-linux-x86_64
chmod +x exfer-linux-x86_64
sudo mv exfer-linux-x86_64 /usr/local/bin/exfer
exfer --help
macOS Apple Silicon (M1/M2/M3/M4)
curl -LO https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-macos-arm64
chmod +x exfer-macos-arm64
xattr -d com.apple.quarantine exfer-macos-arm64 # remove "downloaded from internet" flag
sudo mv exfer-macos-arm64 /usr/local/bin/exfer
exfer --help
macOS Intel
curl -LO https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-macos-x86_64
chmod +x exfer-macos-x86_64
xattr -d com.apple.quarantine exfer-macos-x86_64
sudo mv exfer-macos-x86_64 /usr/local/bin/exfer
exfer --help
Windows
PowerShell:
Invoke-WebRequest `
-Uri https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-windows-x86_64.exe `
-OutFile exfer.exe
.\exfer.exe --help
Build from source
Requires Rust 1.85 or newer.
git clone https://github.com/ahuman-exfer/exfer.git
cd exfer
cargo build --release
sudo install -m 755 target/release/exfer /usr/local/bin/exfer
exfer --help
Build time: typically 5–15 minutes from a cold cache. Optimized release profile uses LTO and codegen-units=1.
Verify the install
exfer --help
Expected:
Exfer blockchain node
Usage: exfer <COMMAND>
Commands:
node Run a full node
mine Run the miner
wallet Wallet operations
script Script operations (HTLC, covenants)
init Initialize a new Exfer node
What gets created on disk
The first time you run exfer init or exfer node, the following files
are created in --datadir (default ~/.exfer):
| Path | Purpose |
|---|---|
~/.exfer/wallet.key | Encrypted wallet keypair (only if you used exfer init) |
~/.exfer/node_identity.key | Unique P2P identity for your node. Keep secret: if leaked, an attacker can impersonate your node on the P2P network |
~/.exfer/chain/ | The block database (grows over time) |
~/.exfer/state/ | The UTXO and mempool state |
All key files are created with permissions 0600 (owner-only). If your
filesystem strips permissions (e.g. WSL on a Windows drive), pass
--repair-perms to let exfer fix them at startup instead of exiting.
Next
- First time? Follow Quick start.
- Already initialized? Move on to Sync the chain.
Sync the chain
A fresh node has no blocks. On first startup it performs an initial block download (IBD): connect to peers, download headers, then bodies, validating along the way.
Start syncing
exfer node --datadir ~/.exfer --rpc-bind 127.0.0.1:9334
Peers are discovered automatically via DNS (seed.exfer.org) and a
hardcoded fallback list — no --peers needed. To pin manual peers:
exfer node --datadir ~/.exfer --peers 82.221.100.201:9333 --peers 89.127.232.155:9333
What you'll see in the log
INFO exfer: Node starting on 0.0.0.0:9333
INFO exfer: P2P listener bound
INFO exfer: Sync manager: cold-bootstrap IBD complete
INFO exfer: IBD complete at height 559300
After IBD complete, the node tracks the tip and accepts new blocks live
(~one every 10 s).
Watch progress
From another terminal:
curl -s -X POST http://127.0.0.1:9334 \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block_height","params":{},"id":1}'
Compare the returned height against a community node (or
the dashboard on this site). When they match within a block or two, you
are synced.
Speeding up the first sync
By default, blocks at or below the hardcoded checkpoint height (130 000) skip Argon2id PoW verification during IBD. This dramatically cuts cold sync time. All other validation (block linkage, signatures, UTXO accounting, state root, coinbase rules, timestamps) is still performed for every block.
The trust assumption is the binary author, not your peers — the checkpoint hash guarantees the canonical chain. If the block at the checkpoint height does not match, the chain is rejected.
To opt out:
exfer node --datadir ~/.exfer --no-assume-valid
# or to also re-verify every replayed block:
exfer node --datadir ~/.exfer --verify-all
--verify-all is for paranoid scenarios (suspected database corruption,
hardware faults). It implies --no-assume-valid.
How long does a cold sync take?
Order of magnitude — varies by disk speed, network, and host CPU:
| Phase | Approx. duration on a typical VPS |
|---|---|
| Header chain | a few minutes |
| Pre-checkpoint blocks (assume-valid) | tens of minutes |
| Post-checkpoint blocks (full PoW verify) | growing with tip height |
If you can copy a recent ~/.exfer/chain/ and ~/.exfer/state/ from a
trusted machine, you can skip most of this — see
Backup & recovery for the cautions.
Stay in sync after the first run
The node stays running and follows the tip. To restart cleanly after a reboot, use a system service — see Run as a service.
If the node falls behind (network outage, host suspended), it automatically catches up on the next start; no special command needed.
Common sync issues
See Troubleshooting. The most frequent ones:
- Stuck at one height for minutes — usually a slow peer; the sync manager rotates to a different peer after a timeout.
- Address already in use on 9333 — another
exferinstance is running, or that port is taken. See Troubleshooting. Permissions on node_identity.key are too open— pass--repair-permsto auto-fix.
Run as a service
You want the node to start on boot, stay running, restart on crash, and log somewhere predictable. Three options below.
Linux: systemd
Create /etc/systemd/system/exfer.service:
[Unit]
Description=Exfer node
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=exfer
Group=exfer
ExecStart=/usr/local/bin/exfer node \
--datadir /var/lib/exfer \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/exfer
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Create the user and data directory once:
sudo useradd --system --home /var/lib/exfer --shell /usr/sbin/nologin exfer
sudo install -d -o exfer -g exfer -m 0750 /var/lib/exfer
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now exfer
sudo systemctl status exfer
sudo journalctl -fu exfer
Mining variant — replace the ExecStart line with:
ExecStart=/usr/local/bin/exfer mine \
--datadir /var/lib/exfer \
--miner-pubkey YOUR_PUBKEY_HEX \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
The pubkey-only mining mode means the wallet private key never lives on the mining host. See Solo mine.
macOS: launchd
Create ~/Library/LaunchAgents/org.exfer.node.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>org.exfer.node</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/exfer</string>
<string>node</string>
<string>--datadir</string>
<string>/Users/you/.exfer</string>
<string>--rpc-bind</string>
<string>127.0.0.1:9334</string>
<string>--repair-perms</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/exfer.log</string>
<key>StandardErrorPath</key><string>/tmp/exfer.log</string>
</dict>
</plist>
Load it:
launchctl load ~/Library/LaunchAgents/org.exfer.node.plist
launchctl list | grep exfer
tail -f /tmp/exfer.log
To stop / reload:
launchctl unload ~/Library/LaunchAgents/org.exfer.node.plist
launchctl load ~/Library/LaunchAgents/org.exfer.node.plist
Docker
Minimal Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L -o /usr/local/bin/exfer \
https://github.com/ahuman-exfer/exfer/releases/latest/download/exfer-linux-x86_64 \
&& chmod +x /usr/local/bin/exfer
WORKDIR /data
EXPOSE 9333
CMD ["exfer", "node", "--datadir", "/data", "--rpc-bind", "0.0.0.0:9334", "--repair-perms"]
Run:
docker build -t exfer .
docker run -d --name exfer \
-p 9333:9333 -p 127.0.0.1:9334:9334 \
-v exfer-data:/data \
--restart unless-stopped \
exfer
docker logs -f exfer
Notes:
- Use a named volume (
-v exfer-data:/data) to persist the chain across container restarts. - Map
9334only to localhost (127.0.0.1:9334:9334). The RPC has no authentication — exposing it on0.0.0.0is dangerous on shared hosts.
Resource expectations
A small VPS is plenty for a full node:
| Resource | Comfortable | Tight |
|---|---|---|
| RAM | 1 GB | 512 MB |
| Disk | 50 GB | 20 GB (grows over time) |
| CPU | 1 vCPU | shared-CPU is OK |
| Network | 10 Mbps | 1 Mbps still works once synced |
Mining changes the picture — Argon2id is memory-hard, so each miner thread needs ~64 MiB of resident memory. See Solo mine.
Backup & recovery
Three things to back up. They have different recovery characteristics — treat them differently.
What to back up
| File | Lose it = lose what? | Recoverable from? |
|---|---|---|
wallet.key (encrypted) | Your funds. Without the file and the passphrase, the coins are unspendable. | Nothing. There is no seed phrase recovery. |
node_identity.key | Your node's P2P identity. Losing it doesn't lose funds, but other peers will see you as a new node. Leaking it lets others impersonate you. | Auto-regenerated on next start, with a new identity. |
chain/ + state/ (the data directory) | Sync time only. You can rebuild from peers. | Re-syncing from peers. |
Wallet backup (most important)
Make at least two copies, store in physically different places, and write down the passphrase separately:
cp ~/.exfer/wallet.key ~/Documents/exfer-wallet-2026-05-16.key
# also: USB stick, encrypted cloud bucket, locked drawer, etc.
Because the file is encrypted, it is safe to put on cloud storage. But losing the passphrase is the same as losing the file — both are required.
Recommendation:
- Two physical copies (e.g. two USB sticks, in two locations).
- One encrypted cloud copy.
- Passphrase: stored in a password manager whose master password is itself durably backed up.
Recover a wallet
exfer wallet info --wallet /path/to/backup.key
# enter passphrase when prompted
If the file decrypts, the wallet is intact. The address and balance follow from the keypair plus chain state.
Node identity backup (sensitive)
~/.exfer/node_identity.key is the Ed25519 identity your node uses on the
P2P network. Treat it like an SSH host key:
- Back it up only if you specifically want to preserve your node's identity across reinstalls (rarely necessary).
- Never share it. Anyone holding it can impersonate your node, which can let them join peer-trust slots reserved for your node.
Permissions are enforced at 0600. If your filesystem strips perms (WSL
on a Windows drive, some NAS mounts), pass --repair-perms instead of
backing up the file manually.
Data directory backup (optional)
The chain/ and state/ directories can be regenerated by re-syncing
from peers, so backing them up is purely a sync-time optimization. If you
do back them up:
- Take the snapshot only while the node is stopped — copying a live database can produce a corrupt snapshot.
- Use a tool that copies atomically (
rsync --deleteafter stop, or LVM snapshot). - Verify the restored node syncs cleanly to the current tip before trusting it.
sudo systemctl stop exfer
sudo rsync -a --delete /var/lib/exfer/chain /backup/exfer-chain-$(date +%F)/
sudo rsync -a --delete /var/lib/exfer/state /backup/exfer-state-$(date +%F)/
sudo systemctl start exfer
Restoring from a stale snapshot is fine — the node will catch up the missing tip blocks from peers on next start.
Upgrading the binary
sudo systemctl stop exfer
# replace /usr/local/bin/exfer with the new release binary
sudo systemctl start exfer
journalctl -fu exfer
The on-disk database format is not guaranteed to be backward- compatible across major versions. Before a major upgrade:
- Read the upstream CHANGELOG for migration notes.
- Snapshot
chain/andstate/. - Have a plan to roll back the binary if startup fails.
Minor (v1.x.y → v1.x.z) upgrades are typically a drop-in replacement.
Disaster recovery
Worst case (host destroyed, no chain snapshot, no node identity):
- Reinstall the binary on a new host.
- Restore
wallet.keyfrom your offline backup. - Run
exfer node(orexfer mine) — a freshnode_identity.keyis generated, the chain re-syncs from peers. - Use
exfer wallet balance --rpc <node>to confirm funds visible.
Time to recover: download + IBD (see Sync the chain).
Troubleshooting
Common errors people hit when first running an Exfer node, and what to do.
"Address already in use"
ERROR exfer: FATAL: P2P listener failed on 0.0.0.0:9333: Address already in use (os error 98)
Something else is on port 9333. Either:
- Another
exferinstance is already running. Stop it:sudo systemctl stop exfer pgrep -af exfer - Another program owns the port. Identify it:
sudo ss -ltnp | grep 9333 - Pick a different port for this instance:
Note that peers reach you on whatever port you bind. For inbound P2P connections you'll need to map your firewall / NAT to the same port.exfer node --bind 0.0.0.0:19333
"Permissions on node_identity.key are too open"
ERROR exfer: node_identity.key has insecure permissions (0644). Refusing to start.
The file must be owner-readable only (0600). Either fix it manually:
chmod 0600 ~/.exfer/node_identity.key
Or let exfer repair it on startup:
exfer node --datadir ~/.exfer --repair-perms
The latter is the right choice for filesystems that strip permissions (WSL on a Windows drive, certain NAS mounts).
Sync stuck at one height
If get_block_height doesn't move for minutes despite a working network:
- Are the peers healthy? Look at the log for
peer N: ...lines. The sync manager rotates to a different peer after a timeout if one stalls. - Are you still in IBD? During cold-bootstrap, header sync runs first; bodies follow. The tip height only advances once bodies start validating.
- Check disk space. A full disk silently breaks RocksDB writes.
df -h ~/.exfer - Restart cleanly. Some peer-state issues clear on restart.
"Failed to decrypt wallet"
You typed the passphrase wrong, or the file is corrupted. The CLI cannot tell the two apart from outside — both produce a generic decryption failure.
- Try a backup copy of the wallet first.
- If the backup decrypts, the original file is corrupted (probably truncated by a bad copy). Replace with the backup.
- If no backup decrypts, the passphrase is wrong. There is no recovery path.
"Address must be 32 bytes"
You passed an address that isn't 64 hex chars. Check:
- No surrounding whitespace.
- No
0xprefix (Exfer addresses are bare hex). - Length is exactly 64 chars.
echo -n "$ADDRESS" | wc -c # must print 64
"Insufficient funds"
exfer wallet send reports balance is too low to cover amount + fee.
- Check actual balance:
exfer wallet balance --wallet ... --rpc ... - Check
amount: did you mean exfers or EXFER?--amount 10is 10 exfers (0.0000001 EXFER).--amount "10 EXFER"is 10 EXFER. The CLI accepts both forms, but they mean different things. - Mature coinbases only: outputs younger than 360 blocks are excluded.
- Dust outputs (below 200 exfers) are skipped.
RPC returns HTTP 429
You hit a rate limit on the RPC endpoint (typically a community node).
- For
send_raw_transaction: 60 / min / IP. - For UTXO scans (
get_balance,get_address_utxos,get_script_utxos): 30 / min / IP.
Back off and retry with jitter. For sustained workloads, run your own node so you control the rate limits.
Log noise: "rate-limited peer X"
Demoted from WARN to DEBUG in v1.8.1. If you're on an older binary
and the log is dominated by these lines, upgrade to the latest release.
"Failed to deserialize transaction"
You called send_raw_transaction with bytes that aren't a valid Exfer
transaction:
- Wrong serialization format (e.g. used Bitcoin's, not Exfer's — see Protocol specification §5).
- Hex string has odd length or non-hex characters.
- Trailing garbage after the canonical end of the transaction.
Reconstruct with exfer wallet send (which signs correctly), or follow
Wallet developer guide for the byte-level
construction.
When to ask for help
Open an issue at https://github.com/ahuman-exfer/exfer/issues with:
- Exact
exfer --versionoutput. - The complete command you ran.
- The full error message and the previous few lines of log.
- OS, distro version, filesystem (ext4 / btrfs / NTFS-on-WSL / etc.).
The more reproducible the report, the faster it gets fixed.
How exfer mining works
Exfer uses Argon2id as its proof-of-work function. Unlike SHA-256 (Bitcoin) which is fast and cheap to ASIC, Argon2id is memory-hard — each hash attempt requires reading and mutating a large block of RAM, which makes custom hardware much less of a runaway advantage.
The high-level shape: you pick a nonce, compute Argon2id over the block header, and check whether the result meets the current difficulty target. If not, increment the nonce and try again. The miner that finds a valid hash first publishes the block and claims the coinbase reward.
Parameters at a glance
| Constant | Value |
|---|---|
| PoW algorithm | Argon2id |
Memory cost (m) | 64 MiB |
Time cost (t) | 2 iterations |
Parallelism (p) | 1 lane |
| Target block time | 10 s |
| Difficulty retarget | every 4 320 blocks |
| Initial block reward | 100 EXFER |
| Reward half-life | 6 307 200 blocks (~2 years) |
| Minimum reward | 1 EXFER |
| Coinbase maturity | 360 blocks (~1 hour) |
| Genesis block | d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051 |
Hash rate expectations
Argon2id is memory-bandwidth bound, not CPU-bound. Two things determine your throughput:
- Cache-line throughput / memory bandwidth — DDR4-3200 dual channel is meaningfully faster than single-channel DDR3.
- Resident memory available — each concurrent attempt needs 64 MiB. A box with 1 GB of RAM can run roughly 8–10 concurrent attempts before thrashing.
Realistic rough numbers per machine (single thread, very rough order of magnitude):
| Hardware | Approx. attempts / s per thread |
|---|---|
| Modern x86-64 desktop (DDR4) | ~10–40 |
| VPS shared CPU | ~2–10 |
| Raspberry Pi 5 | ~1–3 |
Multi-threading multiplies linearly until memory bandwidth saturates.
What you actually earn
For now, every mined block pays the full coinbase reward (currently
~100 EXFER per block, halving every ~2 years) plus the sum of transaction
fees in the block. The probability of any single attempt finding a block
is roughly (your_hash_rate) / (network_hash_rate).
Network hash rate fluctuates. To check what's plausible right now:
- Look at recent blocks via
get_blockin the RPC explorer — note the timestamps and difficulty. - Compare your local attempts/sec against the implied network rate.
- Solo mining at < 0.1 % of network hash rate means months between blocks on average. Consider pool mining for steady smaller payouts.
Why CPU, not GPU?
GPUs are good at parallel computations with little memory per worker. Argon2id flips that — it wants lots of memory per worker, with random access patterns. GPUs do exist for Argon2id but the speedup over commodity DDR4 CPUs is far smaller than for SHA-256 (where it's ~10⁴×).
ASICs for Argon2id are theoretically possible but economically unattractive at Exfer's scale today.
Reward schedule
Block rewards follow exponential decay rather than the Bitcoin-style step halving:
reward(h) = max(MIN_REWARD, INITIAL_REWARD * exp(-ln(2) * h / HALF_LIFE))
with INITIAL_REWARD = 100 EXFER, MIN_REWARD = 1 EXFER,
HALF_LIFE = 6 307 200 blocks.
In practice the reward halves every ~2 years and asymptotes at 1 EXFER per block. For the precise emission formula see Protocol specification §11.
Next
- Solo mine — the exact command, plus running it as a service.
- Pool mining — when solo doesn't make sense.
Solo mine on a CPU
You need:
- An installed
exferbinary (Install) - A wallet (we only need the pubkey, not the private key — see below)
- A machine with at least a few hundred MB of RAM and a routable network
Get a payout pubkey
Generate a wallet on a different machine (not the mining host):
exfer wallet generate --output ~/payout.key --json
Grab the pubkey field. The mining host needs only this 64-hex pubkey —
the private key never leaves the wallet machine. This protects funds if
the mining host is compromised.
{
"pubkey": "fcbd5a818501cd5439ebe8c0c5ff244c0f1475333e226b7f998e6eb80552c69d",
"address": "8d896d64864f53214acb49aeb44a09a03d5bb23d19a417a6ce7b0da65c7bd750"
}
Start mining
exfer mine \
--datadir ~/.exfer \
--miner-pubkey fcbd5a818501cd5439ebe8c0c5ff244c0f1475333e226b7f998e6eb80552c69d \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
| Flag | What it does |
|---|---|
--datadir | Where chain + state live. Same as exfer node |
--miner-pubkey | The pubkey that gets the coinbase reward. No private key needed on this host. |
--rpc-bind | Optional: expose RPC on the given address. Use 127.0.0.1 only. |
--repair-perms | Auto-fix node_identity.key permissions if stripped by the filesystem |
exfer mine is a superset of exfer node — it syncs the chain, accepts
peer connections, and additionally tries to win blocks.
Watch it work
# new tip every ~10 s under normal network conditions
journalctl -fu exfer | grep -E 'New tip|Mined block'
If you find a block, you'll see a Mined block at height H line. The
reward output goes to your --miner-pubkey and is immature for the
next 360 blocks before you can spend it.
Confirming coinbase rewards arrived
Coinbase outputs are returned with is_coinbase: true by
get_address_utxos. Use the get_address_utxos try-it
widget with your address to see them.
The is_coinbase flag plus the height field tell you whether the
output is spendable yet (tip_height - height >= 360).
Run as a service
See Run as a service for systemd, launchd, and
Docker templates. Substitute exfer node with exfer mine --miner-pubkey YOUR_PUBKEY_HEX.
Important: the systemd User=exfer should not be the user that holds
your wallet. The mining service is intended to be unprivileged and own
only the chain database.
Running on a low-RAM box
Each parallel Argon2id attempt needs 64 MiB. Today the miner uses a fixed number of worker threads tuned for the host's logical CPU count. On a tight VPS:
- Make sure the box has at least 512 MiB free RAM after the OS.
- Avoid running the miner alongside other memory-hungry processes (the Argon2id working set is random-access; if it spills to swap, hash rate collapses).
- If the host is shared (e.g. a tiny VPS), prefer pool mining — solo on a tiny box is mostly heat.
Stopping cleanly
sudo systemctl stop exfer
# or, if running in the foreground:
^C
The miner persists its state on the way out; no chain-database corruption from a clean shutdown.
Sanity checks before leaving it running for days
- Disk has headroom:
df -h $DATADIRshows growth, no full filesystem. - Time is correct: out-of-sync clocks cause peers to reject your
blocks.
timedatectl statusshould say "NTP synchronized: yes". - Pubkey is right: a typo in
--miner-pubkeymeans any blocks you find pay an address you don't control. - Backups in place: see Backup & recovery.
Pool mining
Solo mining is fine if you have a meaningful share of network hash rate. At small share, blocks are rare and lumpy — months between hits is normal. Pools aggregate hash rate from many miners and pay out proportional shares, so you get steady smaller payouts.
Community pools
Two community-run pools are currently active. Both are operated by third parties — check each pool's site for connection details, fee schedule, payout threshold, and any stratum/proxy software they ship.
| Pool | URL | Type |
|---|---|---|
| LuckyPool | https://exfer.luckypool.io/ | community-run |
| NinjaRaider | https://ninjaraider.com/exfer-pplns | PPLNS |
Things to look at on each pool's site before pointing miners at it:
- Fee — typically 1–3 % of rewards.
- Payout scheme — PPLNS, PPS, etc. They distribute variance differently.
- Minimum payout threshold — make sure it's achievable for your hash share.
- Stratum / submission protocol — what miner software does the pool support? Is there a reference config?
- Operator transparency — public contact, recent operational history.
Payouts go directly to the EXFER address you configure with the pool;
the pool never custodies your funds, only your share-accounting work.
You can verify every payout via
get_address_utxos against your address.
Self-hosted mini-pool
If you control multiple miners (a small farm), the simplest pool-like
setup is just running multiple exfer mine instances all paying the
same pubkey. Periodically sweep the pubkey's UTXOs to a cold wallet.
It's not share-accounting in the traditional pool sense — it's just multiple miners under one identity — but it captures the "steady payouts" property without trusting a third-party operator.
When solo is the right call
- You have a meaningful share (≥ 1 %) of network hash rate.
- You can tolerate variance in payout timing.
- You distrust pool operators on principle.
In those cases Solo mine is what you want.
HTLC payments
A Hash Time-Locked Contract (HTLC) is a payment that the recipient can claim only by revealing a preimage. If they don't claim before a deadline, the sender reclaims. This is the foundation of trustless atomic swaps, machine-to-machine escrow, and Lightning-style payment channels.
Exfer ships HTLC as a first-class script type — no smart-contract VM, no
gas. Three CLI commands cover the full lifecycle: htlc-lock,
htlc-claim, htlc-reclaim.
Concept
Sender A Receiver B
│ │
│ share hash H = SHA256(p) │ (B picks secret p)
│ ◄────────────────────────────┤
│ │
│ htlc-lock(H, timeout) │
│ ─────────────────────────► │
│ │
│ │ htlc-claim(p) ─► funds to B
│ │
│ (if B never claims) │
│ htlc-reclaim after timeout │ ─► funds back to A
│ ─────────────────────────► │
The script enforces:
- Before
timeout: spendable only by B presenting a preimage that hashes toH. - At or after
timeout: spendable only by A.
Both branches are signature-checked, so an outside observer who learns the preimage still cannot claim the funds — they need B's signature too.
End-to-end worked example
Two agents, A (sender) and B (receiver).
B: generate preimage and hash
PREIMAGE=$(openssl rand -hex 32)
HASH_LOCK=$(echo -n "$PREIMAGE" | xxd -r -p | shasum -a 256 | cut -d' ' -f1)
echo "Share with A: $HASH_LOCK"
# B keeps $PREIMAGE secret until ready to claim
A: get current block height and lock the funds
RPC="http://82.221.100.201:9334"
HEIGHT=$(curl -s -X POST "$RPC" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block_height","params":{},"id":1}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])")
TIMEOUT=$((HEIGHT + 500)) # ~83 minutes from now
exfer script htlc-lock \
--wallet ~/agent-a.key \
--receiver <AGENT_B_PUBKEY> \
--hash-lock "$HASH_LOCK" \
--timeout "$TIMEOUT" \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
# Output includes tx_id — share it with B
A: wait for the lock tx to confirm
TX_ID="<tx_id from above>"
for i in $(seq 1 60); do
R=$(curl -s -X POST "$RPC" -H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"get_transaction\",\"params\":{\"hash\":\"$TX_ID\"},\"id\":1}")
echo "$R" | grep -q '"block_height"' && { echo "Lock confirmed"; break; }
sleep 10
done
B: claim by revealing the preimage
exfer script htlc-claim \
--wallet ~/agent-b.key \
--tx-id "$TX_ID" \
--preimage "$PREIMAGE" \
--sender <AGENT_A_PUBKEY> \
--timeout "$TIMEOUT" \
--rpc "$RPC" \
--json
The preimage is now visible on-chain inside B's claim transaction. A (and anyone else) can read it from the block — which is exactly what makes cross-chain atomic swap work: once B claims on chain X, A learns the preimage and can claim the mirror HTLC on chain Y.
A: reclaim if B never showed up
# only succeeds if current_height >= TIMEOUT
exfer script htlc-reclaim \
--wallet ~/agent-a.key \
--tx-id "$TX_ID" \
--receiver <AGENT_B_PUBKEY> \
--hash-lock "$HASH_LOCK" \
--timeout "$TIMEOUT" \
--rpc "$RPC" \
--json
The CLI pre-checks the current height against TIMEOUT and refuses to
submit if the lock is still active.
Command reference
htlc-lock
exfer script htlc-lock \
--wallet ~/sender.key \
--receiver <RECEIVER_PUBKEY> \
--hash-lock <SHA256_HEX> \
--timeout <BLOCK_HEIGHT> \
--amount "10 EXFER" \
--fee "0.001 EXFER" \
--rpc "$RPC" \
--json
htlc-claim
exfer script htlc-claim \
--wallet ~/receiver.key \
--tx-id <LOCK_TX_ID> \
--preimage <PREIMAGE_HEX> \
--sender <SENDER_PUBKEY> \
--timeout <TIMEOUT_HEIGHT> \
--rpc "$RPC" \
--json
htlc-reclaim
exfer script htlc-reclaim \
--wallet ~/sender.key \
--tx-id <LOCK_TX_ID> \
--receiver <RECEIVER_PUBKEY> \
--hash-lock <SHA256_HEX> \
--timeout <TIMEOUT_HEIGHT> \
--rpc "$RPC" \
--json
Common pitfalls
timeouttoo tight. If the lock tx isn't even confirmed by the timetimeouthits, the HTLC is effectively dead-on-arrival — A can reclaim immediately, B has no window to claim. Leave at least a few hundred blocks of cushion past tx confirmation.timeouttoo loose. A's funds are locked for the duration. For atomic swaps, choose the longer chain's timeout strictly greater than the shorter chain's, so B always has time to react.preimagemismatch. The hash you advertise must be exactlySHA256(preimage_bytes). Common bug: hashing the hex string of the preimage instead of the bytes. The example above usesxxd -r -p | shasum -a 256to hash the bytes.- Coinbase maturity. If A's wallet is funded only from recent coinbase outputs, the lock will fail with "immature coinbase". Either wait, or fund A's wallet from a non-coinbase source.
Related script types
The CLI also has multisig, vault, escrow, and delegation patterns — see Protocol specification for the underlying script language. The CLI subcommands are documented in CLI reference.
Multisig
Three multisig flavors ship as first-class script patterns. Pick the one that matches your trust model.
| Pattern | Required signers | Typical use |
|---|---|---|
| 2-of-2 | both parties | joint custody, payment channels, two-party agreement |
| 1-of-2 | either party | shared account with two key holders, backup access |
| 2-of-3 | any two of three | committee governance, 2FA with recovery key |
All three use exfer script <pattern>-lock to lock funds and
<pattern>-spend to unlock them.
2-of-2
Both signatures required. Use when the two parties must agree.
# Lock
exfer script multisig2of2-lock \
--wallet ~/my-wallet.key \
--pubkey-b <OTHER_PUBKEY_HEX> \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
# Spend (both wallets sign)
exfer script multisig2of2-spend \
--wallet ~/wallet-a.key \
--wallet2 ~/wallet-b.key \
--tx-id <LOCK_TX_ID> \
--to <DESTINATION_ADDRESS> \
--rpc "$RPC" \
--json
1-of-2
Either signer can unlock. Use when you want a backup keyholder who can recover funds independently.
exfer script multisig1of2-lock \
--wallet ~/my-wallet.key \
--pubkey-b <OTHER_PUBKEY_HEX> \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
exfer script multisig1of2-spend \
--wallet ~/my-wallet.key \
--tx-id <LOCK_TX_ID> \
--other-pubkey <OTHER_PUBKEY_HEX> \
--path a \
--rpc "$RPC" \
--json
--path is a if your key was registered first (the --wallet side of
multisig1of2-lock), b otherwise. Getting this wrong produces a
script-evaluation failure at submit time.
2-of-3
Any two of three signers. Quorum / committee patterns.
exfer script multisig2of3-lock \
--wallet ~/my-wallet.key \
--pubkey-b <PUBKEY_B_HEX> \
--pubkey-c <PUBKEY_C_HEX> \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
exfer script multisig2of3-spend \
--wallet ~/signer1.key \
--wallet2 ~/signer2.key \
--tx-id <LOCK_TX_ID> \
--to <DESTINATION_ADDRESS> \
--pubkey-a <PUBKEY_A_HEX> \
--pubkey-b <PUBKEY_B_HEX> \
--pubkey-c <PUBKEY_C_HEX> \
--path ab \
--rpc "$RPC" \
--json
--path selects which pair of the three keys are signing: ab, ac,
or bc. The two --wallet files must correspond, in order, to the two
positions in --path.
Common pitfalls
- Wrong
--path. The CLI accepts the spend but the network rejects it because the script eval fails. Re-derive the path from your wallet positions at lock time. - Pubkey vs address mixup.
--pubkey-*flags want the 64-hex public key (fromexfer wallet info), not the address. Addresses are derived from pubkeys but are not the same value. - Lost cosigner. For 2-of-2, losing one key bricks the funds. For 2-of-3, you can still recover with the remaining two — that's the main reason to choose 2-of-3 over 2-of-2 for any non-trivial value.
Related
- HTLC payments — hash-based conditional spending.
- Vault — primary + recovery key combination.
- Escrow — three-path mutual / arbiter / timeout.
Vault
A vault locks funds behind two spend paths:
- Primary key can spend, but only after a chosen locktime height.
- Recovery key can spend immediately, any time.
The intended use is a self-imposed delay on the primary key with an emergency override. If the primary key is compromised, the attacker cannot spend immediately — they have to wait past the locktime, giving you (the recovery key holder) a window to move the funds out first.
Lock
exfer script vault-lock \
--wallet ~/primary-wallet.key \
--recovery-pubkey <RECOVERY_PUBKEY_HEX> \
--locktime <BLOCK_HEIGHT> \
--amount "100 EXFER" \
--rpc "$RPC" \
--json
Pick --locktime based on how much delay you want. At 10 s per block:
| Delay you want | Add this many blocks |
|---|---|
| 1 hour | 360 |
| 6 hours | 2 160 |
| 1 day | 8 640 |
| 1 week | 60 480 |
| 1 month | ~260 000 |
Compute from current tip: locktime = current_height + delay_blocks.
Spend with primary key (after locktime)
exfer script vault-spend \
--wallet ~/primary-wallet.key \
--tx-id <LOCK_TX_ID> \
--recovery-pubkey <RECOVERY_PUBKEY_HEX> \
--locktime <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
The CLI fails locally if current_height <= locktime. After the
locktime passes, the spend succeeds.
Recover with recovery key (any time)
exfer script vault-recover \
--wallet ~/recovery-wallet.key \
--tx-id <LOCK_TX_ID> \
--primary-pubkey <PRIMARY_PUBKEY_HEX> \
--locktime <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
This works at any height, including before the locktime. The recovery key is the "fire alarm" — you only sign with it if the primary is compromised or you want to abort the timelock.
Threat model
- Primary key compromise: attacker has the primary key, but cannot
spend until
locktime. You spot the leak (via monitoring or just noticing UTXOs are locked), and use the recovery key to sweep funds before the attacker's window opens. - Recovery key compromise: attacker can spend immediately. The vault offers no protection here. Keep the recovery key colder than the primary (offline, hardware, paper, etc.).
- Both keys compromised: total loss. Same as any 1-of-2 multisig.
For deeper defense, layer a vault on top of multisig (e.g. multisig 2-of-3 for the recovery branch) — that's not a built-in pattern but can be assembled by composing scripts. See Protocol specification §6 for the script language.
Common pitfalls
- Locktime in the past at lock time. If
locktime <= current_heightwhen you lock, the primary key can spend right away — the vault has no protective effect. Always sanity-check before submitting. - Recovery key on same machine as primary. Defeats the purpose. Keep the recovery key on a different host, ideally offline.
- Forgetting
--locktimeon spend. The script binds the locktime in the spending witness — you must repeat the same--locktimeyou used at lock time, not the current height.
Escrow
A three-path payment where the funds can be released by any of:
- Mutual release — both parties (A and B) sign.
- Arbiter decision — a designated third party signs.
- Timeout refund — A reclaims after a deadline.
The classic use is buyer–seller trade with a neutral arbiter: funds are locked while the trade is in progress. On success, both sign and the seller gets paid. On dispute, the arbiter rules. If everything goes quiet, the buyer gets refunded.
Lock
exfer script escrow-lock \
--wallet ~/party-a.key \
--party-b <PARTY_B_PUBKEY_HEX> \
--arbiter <ARBITER_PUBKEY_HEX> \
--timeout <BLOCK_HEIGHT> \
--amount "50 EXFER" \
--rpc "$RPC" \
--json
--timeout is the block height after which A can reclaim. Pick it long
enough that both parties have time to settle the trade. See the
locktime conversion table in Vault.
Release (mutual)
Both parties sign together. Release goes to any destination they agree on:
exfer script escrow-release \
--wallet ~/party-a.key \
--wallet2 ~/party-b.key \
--tx-id <LOCK_TX_ID> \
--to <DESTINATION_ADDRESS> \
--party-a <PARTY_A_PUBKEY_HEX> \
--party-b <PARTY_B_PUBKEY_HEX> \
--arbiter <ARBITER_PUBKEY_HEX> \
--timeout <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
Arbitrate
The arbiter decides without needing either party's signature. Use only when the trade is disputed:
exfer script escrow-arbitrate \
--wallet ~/arbiter.key \
--tx-id <LOCK_TX_ID> \
--to <DESTINATION_ADDRESS> \
--party-a <PARTY_A_PUBKEY_HEX> \
--party-b <PARTY_B_PUBKEY_HEX> \
--timeout <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
The destination is whoever the arbiter awards the funds to. The script itself does not constrain the arbiter's choice — that's why you only choose arbiters you trust to follow your agreement.
Reclaim (A after timeout)
If neither mutual release nor arbiter action happened by timeout, A
reclaims:
exfer script escrow-reclaim \
--wallet ~/party-a.key \
--tx-id <LOCK_TX_ID> \
--party-b <PARTY_B_PUBKEY_HEX> \
--arbiter <ARBITER_PUBKEY_HEX> \
--timeout <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
The CLI fails locally if current_height <= timeout.
Choosing an arbiter
The arbiter is a trusted third party — they can unilaterally award the funds. Reasonable choices:
- A reputable community member (DAO multisig, well-known operator).
- A specialized escrow service.
- A judge / mediator with a public arbitration policy.
If you can't agree on an arbiter, use HTLC or Multisig 2-of-2 instead — those don't require a third-party role.
Common pitfalls
- Arbiter compromise = full loss. The arbiter signature alone releases funds. Pick carefully; consider using a multisig key as arbiter for higher-value escrows.
- Repeating the locked params on every spend.
--party-a,--party-b,--arbiter,--timeoutmust match the lock-time values exactly on every spend path. The script binds them in the witness; mismatched values produce script-eval failures. - Picking
--timeouttoo short. If the trade isn't done bytimeout, A can sweep funds out from under B even if the trade technically succeeded. Err generous.
Delegation
Lock funds so that:
- Owner can spend at any time.
- Delegate can spend only before a chosen expiry height.
The owner retains full control. The delegate gets a time-bounded right to spend — useful when you want to grant a subordinate, a service, or an agent the ability to transact on your behalf, but bounded in time so that mistakes or compromises self-correct.
Lock
exfer script delegation-lock \
--wallet ~/owner.key \
--delegate <DELEGATE_PUBKEY_HEX> \
--expiry <BLOCK_HEIGHT> \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
Pick --expiry based on how long you want the delegate's authority to
last. See the locktime conversion table in Vault.
Owner spend (any time)
exfer script delegation-owner-spend \
--wallet ~/owner.key \
--tx-id <LOCK_TX_ID> \
--delegate <DELEGATE_PUBKEY_HEX> \
--expiry <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
The owner can always recall the funds, even before expiry. This is the "emergency revoke" path.
Delegate spend (before expiry only)
exfer script delegation-delegate-spend \
--wallet ~/delegate.key \
--tx-id <LOCK_TX_ID> \
--owner <OWNER_PUBKEY_HEX> \
--expiry <BLOCK_HEIGHT> \
--rpc "$RPC" \
--json
The CLI refuses to submit once current_height >= expiry. On chain, the
script enforces the same — any block-publishing miner will reject a
delegate spend at or after the expiry height.
When this fits
- Agent / bot operating an account. Give the agent a delegate key with a 1-week expiry; renew the lock weekly. If the agent's key leaks, the blast radius is bounded to the remaining time on the lock.
- Subordinate spending allowance. A finance assistant can pay invoices up to a budget, time-bounded.
- Service that auto-trades for you. Funds are restricted to a short window; you renew rather than handing over indefinite custody.
When this does not fit
- You want the delegate to be unable to spend at all once expired. Delegation gives that: post-expiry, only the owner can sign.
- You want the owner to be unable to override the delegate. This pattern is owner-first; the owner can always spend. If you need an irrevocable grant, use a different script (e.g. multisig where the owner is not a signer).
Common pitfalls
- Forgetting to renew before expiry. Funds get stuck (well — not stuck, but only owner-spendable). If you ran a long-running agent off a delegation, automate the renewal cycle.
- Setting
--expirytoo far out. A compromised delegate key with a year of life is nearly as bad as a permanent grant. Short and renewed beats long and forgotten. - Re-locking with the wrong
--owner/--delegateon spend. Both flags bind into the script witness — values must match the lock-time configuration exactly.
For exchanges
You want to list EXFER. You need to:
- Generate deposit addresses per user.
- Detect deposits as they confirm.
- Credit user balances after enough confirmations.
- Process withdrawals by signing and broadcasting transactions.
- Handle reorgs — sometimes a confirmed deposit gets reorganized out and you must debit.
This page walks through each of those. The JSON-RPC reference has the per-method details.
Generate deposit addresses
Exfer is not BIP-32 HD. Each wallet file = one keypair = one address. For deposit addresses, the simplest pattern is one wallet file per user:
exfer wallet generate --output users/user_42.key --no-encrypt --json
{
"file": "users/user_42.key",
"pubkey": "fcbd...c69d",
"address": "8d89...d750"
}
Store the mapping user_id -> address in your database. The 64-hex
address is what you display in the user's deposit page.
Why --no-encrypt? Server-side, you can't prompt for a passphrase
on every signature operation. Use OS-level encryption (LUKS, FileVault,
dm-crypt) on the disk that holds the key files instead.
Higher-volume alternative: derive keys deterministically from an HD
seed outside of the CLI and use send_raw_transaction
to broadcast. See For wallet developers.
Detect deposits
Recommended pattern: block scan, not per-address polling.
Per-address get_address_utxos polling
does not scale — it counts against the UTXO-scan rate limit (30 / min /
IP) and wastes work on inactive addresses.
Block scan loop:
H = last_scanned_height # persisted in your DB
every 5-10 seconds:
tip = get_block_height()
for h in (H+1 .. tip):
block = get_block(height=h)
for tx_id in block.transactions:
tx = get_transaction(hash=tx_id)
decode tx.tx_hex
for each output:
if output.address in (issued_deposit_addresses):
record (tx_id, output_index, value, h, block.hash)
H = h
persist H
Decoding tx_hex requires understanding Exfer's serialization — see
Protocol specification §5 and
For wallet developers.
If you have a small set of deposit addresses (< 100), per-address
polling via get_address_utxos every 30–60 s is also fine.
Credit user balances
After detecting a deposit at height h, wait N confirmations before
crediting:
| Tier | Confirmations | Wall time |
|---|---|---|
| Small deposits | 30 | ~5 min |
| Standard deposits | 60 | ~10 min |
| Large deposits (> ~$10k equiv) | 360 | ~1 hr |
The "matches coinbase maturity" depth of 360 blocks is a hard ceiling on what a reorg could undo, in practice.
Watch for reorgs
A confirmed transaction can be reorged out and reappear in a different
block (or disappear entirely). Defend by re-checking block_hash:
every minute, for each watched deposit:
cur = get_transaction(hash=tx_id)
if cur.block_hash != stored.block_hash:
if cur is missing:
# the tx is reorged out and not back in the mempool
debit the user, mark deposit as "reverted"
else:
# tx moved to a different block, recount confirmations from new height
update stored.block_hash, stored.block_height
require N more confirmations before re-crediting
For high-value flows, consider deferring credit beyond N=360 and
running this reorg watch indefinitely.
Process withdrawals
Easiest path: let the CLI build and sign for you, then submit via RPC.
exfer wallet send \
--wallet hotwallet.key \
--to <USER_WITHDRAWAL_ADDRESS> \
--amount "10 EXFER" \
--fee "0.001 EXFER" \
--rpc http://127.0.0.1:9334 \
--json
This:
- Fetches UTXOs of the hot wallet via
get_address_utxos. - Builds an unsigned transaction.
- Signs locally. The private key never leaves the machine running this command.
- Submits via
send_raw_transaction.
For air-gapped cold custody, build + sign on the offline machine using
the same binary, then move the resulting hex to an online machine and
call send_raw_transaction directly.
Hot / warm / cold split
Standard exchange practice. A rough mapping:
| Layer | What it holds | Key location |
|---|---|---|
| Hot wallet | enough for daily withdrawals | online, encrypted at rest, automated signing |
| Warm wallet | rolling reserve refilling hot | online but isolated, manual signing |
| Cold storage | bulk reserves | offline, multisig 2-of-3 or vault |
Periodically sweep deposits to cold; refill hot from warm. Treat deposit address keys as low-privilege — sweep them out promptly.
Health checks
A node is healthy for production service when:
curl -s -X POST http://127.0.0.1:9334 \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block_height","params":{},"id":1}'
returns within 1 s and the returned height advances within the
expected block cadence (~10 s per block). Alarm if the gap to a peer
node grows beyond 5 blocks.
The Live Nodes page on this site shows the same probe result for the community nodes; use that as a quick comparison point.
Fee policy
Exfer fees are per-transaction (not per-byte). No fee auction. There
is a consensus-enforced minimum relay fee. 0.001 EXFER
(100_000 exfers) clears the mempool reliably under normal conditions.
Don't bid up withdrawal fees aggressively — bias instead toward batched withdrawals (one transaction with N outputs) if you process volume.
See also
- JSON-RPC reference — every method called above.
- For wallet developers — byte-level transaction construction and HD-style address derivation.
- For block explorers — full-archive scanning patterns.
For wallet developers
Building a wallet (CLI, mobile, web, hardware integration)? You need to:
- Derive keys — single key per wallet, or HD-style many keys from one seed.
- Read state — fetch UTXOs and confirmations via RPC.
- Build transactions — serialize correctly so the network accepts them.
- Sign — Ed25519 over a canonical message.
- Broadcast — submit via
send_raw_transaction.
This page is a roadmap. The byte-level details live in Protocol specification §5 (Transactions); the RPC contract in JSON-RPC reference.
Key model
An Exfer keypair is Ed25519. The address is SHA-256 of the
canonical pubkey commitment (see Protocol spec
§5).
There is no native HD scheme. You can:
- Use the CLI's encrypted file format directly — one keypair per file. Simple but doesn't compose well for many addresses.
- Roll your own deterministic derivation — apply BIP-32 or your preferred scheme to derive Ed25519 keys from a seed, then compute the address-hash yourself. This is the right approach for a multi-account wallet.
If you derive your own keys, the only thing you have to match is the
address derivation: address = SHA-256(canonical_pubkey_commitment).
The CLI's wallet generate is a reference implementation.
Read state
For a given address, the RPC surface you'll touch:
| Method | Purpose |
|---|---|
get_balance | Cheap "any value here?" check |
get_address_utxos | Itemized spendable outputs (for building txs) |
get_transaction | Status of a specific tx |
get_block_height | Compute confirmation depth |
Confirmation count for a UTXO returned by get_address_utxos:
tip_height - utxo.height + 1.
Coinbase maturity: skip UTXOs with is_coinbase: true if
tip_height - utxo.height < 360.
Build a transaction
The CLI does this for you when you call exfer wallet send. For a
native wallet you'll reimplement the same logic:
- Select inputs. Walk UTXOs in some deterministic order (e.g.
largest-first to minimize change tx size, or oldest-first to age
coins). Accumulate until
total_input >= amount + fee. Skip dust (< 200 exfers) and immature coinbase. - Build outputs.
- One output to recipient (
amountexfers, locked by recipient's address script). - One change output back to sender (
total_input - amount - feeexfers, locked by sender's address script). Skip if change would be below dust threshold; the surplus becomes additional fee.
- One output to recipient (
- Serialize unsigned tx. Follow the canonical byte layout from Protocol spec §5.
- Compute signing message. Domain-separated hash over the canonical unsigned tx. See Protocol spec §3 for the domain string.
- Sign each input with the corresponding private key, producing Ed25519 signatures.
- Embed signatures in the witness fields, producing the final canonical serialized tx.
- Submit via
send_raw_transaction.
A reference implementation lives in src/wallet/ and src/tx/ in the
upstream exfer source tree.
Script-locked outputs
If your wallet supports anything beyond plain address payments — HTLC, multisig, vault, etc. — you'll be constructing custom locking scripts. Two paths:
- Shell out to the CLI for
exfer script <pattern>-lockand-spend. Easiest to ship, hardest to integrate into a native UI. - Reproduce the script construction natively. See the script
patterns in the upstream source (
src/script/patterns.rsor similar) and the script language definition in Protocol spec §6.
For non-trivial scripts, also use
get_script_utxos to enumerate outputs
locked to your custom script.
Fee selection
There is no fee market auction. Pick a fixed default (e.g.
0.001 EXFER = 100_000 exfers); fall back to bumping the fee only when
the mempool is hot. Replace-by-fee is not yet supported — once you
broadcast, you wait.
Common implementation bugs
- Wrong serialization endianness. Exfer uses a specific byte order for length-prefixed fields. Use the upstream serializer if at all possible.
- Off-by-one in maturity check. Coinbase is mature when
tip_height - utxo.height >= 360, not> 360. Triple-check on a testnet. - Forgetting domain separation for signing. Plain
SHA-256(tx)is not the signing message. The protocol uses domain-separated hashing — see Protocol spec §3. - Using SDK Ed25519 with wrong curve params. Exfer follows the RFC-8032 Ed25519 spec; some libraries default to a non-standard variant. Verify against test vectors.
Test against community nodes
Build your wallet against a community node, but do not ship a production wallet that always uses public RPC:
- It leaks user privacy (their balance queries hit a third party).
- It depends on third-party uptime.
- It's rate-limited.
A reasonable model: ship with a list of default RPCs (community + your own), let the user paste their own node URL, encourage them to run a node for serious use. The Live Nodes page is a fine default-list source.
See also
- JSON-RPC reference
- Protocol specification
- For exchanges — overlapping concerns: HD keys, deposit detection, withdrawals.
For block explorers
You want to ingest the full Exfer chain, store it queryable, and surface it to users on a web UI. Concretely:
- Run an archive node — every block, every transaction, available indefinitely.
- Index block / transaction / address data into a search-friendly store (Postgres, Elasticsearch, ClickHouse, whatever).
- Surface a web UI with the usual explorer features.
- Stay live as new blocks arrive, and handle reorgs.
This page sketches the data plane. The community-hosted explorer at https://explorer.exfer.dev is one reference implementation.
Archive node
Run a regular full node — Exfer doesn't prune by default, so a regular
exfer node is an archive node.
exfer node \
--datadir /var/lib/exfer \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
For maximum integrity, disable assume-valid so every block's Argon2id PoW is verified during sync:
exfer node --datadir /var/lib/exfer --rpc-bind 127.0.0.1:9334 --no-assume-valid
(Slow first sync; worthwhile for an explorer where you don't want to trust the binary author's checkpoint.)
Initial ingest (cold start)
Walk every block from genesis to tip:
for h in 0 .. tip:
block = get_block(height=h)
insert blocks(h, block.hash, block.timestamp, ...)
for tx_id in block.transactions:
tx = get_transaction(hash=tx_id)
decode tx.tx_hex per protocol-spec §5
insert txs(tx_id, block_hash, h, raw_hex, ...)
for each input: insert tx_inputs(...)
for each output: insert tx_outputs(tx_id, idx, value, address, is_coinbase)
This is bounded by your RPC + DB throughput, not by the node — a local node easily serves tens of thousands of requests per second.
To compute address balances (the most popular explorer query), you
need to track which outputs are spent. The simplest model is a flat
utxos table that you INSERT on output creation and DELETE on
input consumption:
CREATE TABLE utxos (
tx_id BYTEA NOT NULL,
output_index INTEGER NOT NULL,
address BYTEA NOT NULL,
value BIGINT NOT NULL,
height BIGINT NOT NULL,
is_coinbase BOOLEAN NOT NULL,
PRIMARY KEY (tx_id, output_index)
);
CREATE INDEX ON utxos(address);
Address balance is then SELECT SUM(value) FROM utxos WHERE address = ?.
Stay live
After the initial ingest, poll the tip and apply new blocks in order:
H = last_indexed_height
every 5-10 seconds:
tip = get_block_height()
while H < tip:
H = H + 1
ingest block at H
Handle reorgs
The chain occasionally re-organizes — a block at height h is replaced
by a different block at the same height. You must detect this and
unwind your index.
Approach: persist block_hash alongside height for each indexed
block. Before ingesting H+1, verify the local H's block_hash
still matches get_block(height=H).hash:
while True:
local = lookup_block(height=H)
remote = get_block(height=H)
if local.hash != remote.hash:
# reorg detected at H
unwind H and walk backwards until match
H = H - 1
continue
break
# now safe to ingest H+1
When unwinding, undo each transaction's effects:
- Re-insert UTXOs that were spent in that block.
- Delete UTXOs that were created in that block.
- Mark the orphaned block / txs as "reorged out" rather than deleting outright (users may have URLs pointing at them).
For shallow reorgs (depth < 5) this is rare and fast. Deep reorgs require special care — flag for human review.
Address derivation
Outputs reference an address (32 bytes) that's a hash of the locking
script. Most outputs are plain pay-to-pubkey-hash style; non-trivial
outputs (HTLC, multisig, vault, etc.) lock to a script-hash address
where the address itself doesn't reveal the underlying script.
To surface "this UTXO is an HTLC locked to receiver R", you need to:
- Recognize the script template at decode time (the upstream source enumerates the known patterns).
- Stash a
script_templatefield on each output during indexing.
For arbitrary scripts, get_script_utxos
returns UTXOs for an exact script hex; useful for indexing custom
covenants if your explorer supports them.
Performance notes
- The RPC
get_blockreturns only the txid list; the bodies require N additionalget_transactioncalls per block. For high-throughput ingest, parallelize tx fetches per block (subject to the connection cap). - Run the explorer's node and DB on the same host or LAN — RPC over the internet is the easy bottleneck.
- Index lazily for cold data, eagerly for hot data (recent blocks).
See also
- JSON-RPC reference — the API.
- Protocol specification §5 — how to
deserialize
tx_hex. - The hosted explorer at https://explorer.exfer.dev for a reference user experience.
Live Nodes
Public Exfer JSON-RPC endpoints surfaced by this site. Health is polled every 30 seconds from a background task running on the proxy server.
Use these endpoints for read-only testing and exploration. For production integrations (exchanges, wallets, explorers, settlement services), run your own node — see Run Your Own Node.
Loading node status…
How health is determined
Every 30 seconds the proxy issues a get_block_height call to each listed
node with a 5-second timeout. A node is reported as online if it returns
a well-formed JSON-RPC result; otherwise the error field on the card
contains the failure reason (HTTP status, timeout, decode failure, etc.).
The height you see on each card is the tip reported at the last probe. A
node falling more than a handful of blocks behind the others is likely
syncing slowly, partitioned, or otherwise unhealthy — pick a different one
for your tests.
Why a CORS proxy
The Exfer node's RPC server is plain JSON-RPC and does not emit
Access-Control-Allow-Origin headers, so browsers refuse direct
fetch() calls from a third-party origin. This site exposes a thin
proxy at POST /rpc/<node_id> that forwards your request body unchanged
to the chosen upstream node and returns the JSON-RPC response with
permissive CORS headers added. The proxy does not parse, cache, or
rewrite the payload; it caps body size at 2 MB and times out at 15 s.
For terminal use, you can call the upstream nodes directly with curl —
the CORS restriction does not apply outside the browser.
Web wallet
A browser-based wallet for Exfer is hosted by the community at:
Use it when you want to send, receive, or check balances without installing a binary or running a node. Keys are generated and signed client-side in your browser; the page only sends signed transactions to public RPC nodes.
When to use the web wallet
- You're trying Exfer for the first time and don't want to install anything.
- You want to send a small payment from a machine where you can't or won't install the CLI.
- You're showing someone the chain on a phone.
When not to use the web wallet
- Large balances. Browser-based wallets share the host's threat
surface (extensions, malicious sites, browser zero-days). For
long-term storage, generate the wallet with the CLI on an offline
machine and back up the encrypted
.keyfile — see Create a wallet and Backup & recovery. - Operational integrations. Exchanges, payment processors, services with API consumers — build against the JSON-RPC API and your own node, not against a third-party web UI.
What lives at exfer.dev
The site is community-maintained; check the page itself for the current feature set. Common features for a chain like this:
- Generate a new keypair locally
- Import an existing key
- View balance + transaction history
- Send a payment (sign in-browser, broadcast via public RPC)
The CLI remains the authoritative tool — the web wallet is convenience on top.
Block explorer
The community-hosted block explorer for Exfer is at:
Use it to:
- Look up a transaction by its
tx_idand see confirmation status. - Browse blocks by height or hash.
- Inspect an address — balance and transaction history.
- Watch the chain tip live.
When to use the explorer
- You sent a transaction and want to confirm it landed in a block.
- Someone gave you an address and you want to check its balance or recent activity.
- You're debugging an integration and want a sanity check against an independent view of the chain.
Pairing the explorer with your own integration
For machine-readable access you still want the JSON-RPC API — the explorer is human-facing. Common pattern:
- Build against your own node's RPC (or a community node) for programmatic checks.
- Surface explorer links in your UI so end users can click through to see a transaction or address externally.
URL conventions on the explorer are typically:
https://explorer.exfer.dev/tx/<tx_id>https://explorer.exfer.dev/block/<height>or/<block_hash>https://explorer.exfer.dev/address/<address>
(Exact URL shape lives in the explorer's UI — check by clicking around. The site is community-maintained and may evolve.)
JSON-RPC Reference
This section is the authoritative reference for the JSON-RPC interface exposed
by exfer node / exfer mine when started with --rpc-bind. The transport
shape is JSON-RPC 2.0 over HTTP POST. The methods are not Bitcoin-compatible
— Exfer is an extended-UTXO chain with its own protocol; see
EXFER.md for the
underlying spec.
What's here
- Transport — request/response envelope, HTTP semantics, how to enable the RPC server.
- Limits & back-pressure — body size caps, rate limits, HTTP 429 behavior.
- Error codes — the four JSON-RPC error codes Exfer uses.
- Protocol primitives — address / hash / amount / height / outpoint encoding, coinbase maturity, block cadence.
- Methods — one page per method:
- Confirmation depth & reorg semantics — how reorg resistance scales with depth.
- References — upstream sources and binaries.
Each method page has a "try it" widget that issues a real call against a community node so you can experiment without running a node locally.
Transport
- Protocol: JSON-RPC 2.0 over HTTP/1.1
- Method:
POST /(any path is accepted; the body is what matters) - Content-Type:
application/json - Encoding: UTF-8
- Framing:
Content-Lengthheader is required on every response and is enforced on requests - Keep-alive: not used; one request per connection
Request envelope
{
"jsonrpc": "2.0",
"method": "<method_name>",
"params": { ... },
"id": <int|string|null>
}
Successful response
{ "jsonrpc": "2.0", "result": { ... }, "id": <same id> }
Error response
{ "jsonrpc": "2.0", "error": { "code": <int>, "message": "..." }, "id": <same id> }
Enabling the RPC server
exfer node --datadir ~/.exfer --rpc-bind 127.0.0.1:9334
Default ports: P2P 9333, RPC 9334. The RPC server has no built-in
authentication — bind to localhost or place it behind a reverse proxy that
provides authentication and TLS.
Limits & back-pressure
| Limit | Value | Scope |
|---|---|---|
| Concurrent connections | 32 | Server-wide |
| Per-connection read timeout | 30 s | Per request |
| Default request body | 64 KiB | All methods unless overridden |
send_raw_transaction body | 2.5 MiB | Per request |
get_script_utxos body | 200 KiB | Per request |
| Response body | 8 MiB | Per response |
| Response headers | 64 KiB | Per response |
send_raw_transaction rate | 60 / min / IP | Sliding window |
| UTXO scan rate (balance/utxos) | 30 / min / IP | Sliding window |
| UTXO scan concurrency | 1 globally | Serialized to protect tip-write lock |
| UTXO list size | 1000 entries | See truncated flag below |
When a rate limit is hit, the server returns HTTP 429 with body
{"error":"rate limited"}. Back off and retry with jitter.
Error codes
Exfer uses the standard JSON-RPC 2.0 codes:
| Code | Name | Meaning |
|---|---|---|
-32700 | Parse error | Malformed JSON or framing |
-32601 | Method not found | Unknown method |
-32602 | Invalid params | Bad hex, wrong length, missing field, tx invalid, block missing |
-32603 | Internal error | Storage / serialization / transient failure |
The message field is human-readable; do not parse it. Branch on code
plus explicit substrings only for retry decisions.
Protocol primitives
These show up across multiple methods.
| Field | Type | Notes |
|---|---|---|
| Address | 32-byte hex (64 chars) | An address is a script hash. The same string is accepted as the script_hex parameter in get_script_utxos. |
| Hash256 | 32-byte hex (64 chars) | Transaction IDs, block IDs, merkle roots — all 32-byte SHA-256. |
| Amount | unsigned integer | Denominated in exfers (base units). 1 EXFER = 100_000_000 exfers. |
| Height | unsigned 64-bit | Genesis is height 0. |
| OutPoint | {tx_id, output_index} | output_index is a 32-bit unsigned int. |
Coinbase maturity
Coinbase outputs are unspendable until 360 blocks (approximately one hour at
the 10 s target block time) have elapsed. get_address_utxos and
get_script_utxos expose is_coinbase: true on each output so that callers
can filter unmatured ones explicitly.
Block cadence
Target 10 s per block. Use this when reasoning about confirmation depth — see Confirmation depth & reorg semantics.
Methods
Seven JSON-RPC methods are exposed. Each method has its own page with parameters, return shape, error codes, and a try-it widget that runs the call against a live community node.
| Method | Purpose |
|---|---|
get_block_height | Current chain tip |
get_block | Block header + txid list by height or hash |
get_transaction | Look up a transaction by txid |
get_balance | Spendable balance of an address |
get_address_utxos | Itemize unspent outputs of an address |
get_script_utxos | Itemize UTXOs locked by a non-address script |
send_raw_transaction | Broadcast a signed transaction |
get_block_height
Return the current chain tip.
Params
None (use null, {}, or []).
Result
{
"height": 5553,
"block_id": "5c1b9e...e7"
}
Example
curl -s -X POST http://127.0.0.1:9334 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"get_block_height","id":1}'
get_block
Fetch a block by hash or height. Exactly one must be provided.
Params
{ "hash": "<64-hex>" }
or
{ "height": 5553 }
Result
{
"hash": "5c1b9e...e7",
"height": 5553,
"timestamp": 1726000000,
"tx_count": 3,
"transactions": [
"fb8a634f...8c",
"1a2b3c4d...ee",
"..."
],
"prev_block_id": "...",
"difficulty_target": "...",
"nonce": 12345678,
"state_root": "...",
"tx_root": "..."
}
transactions[0] is always the coinbase. Other entries are the txids in
canonical block order. To fetch each transaction's body, call
get_transaction with the txid.
Errors
-32602 No block at height H— height beyond tip or pruned-32602 Block not found— hash unknown-32602 Provide either 'hash' or 'height'— neither given
get_transaction
Look up a transaction by its txid. Searches the mempool first, then the confirmed chain.
Params
{ "hash": "<64-hex tx_id>" }
Result (mempool)
{
"tx_id": "fb8a634f...8c",
"tx_hex": "01000000...",
"in_mempool": true
}
Result (confirmed)
{
"tx_id": "fb8a634f...8c",
"tx_hex": "01000000...",
"in_mempool": false,
"block_hash": "5c1b9e...e7",
"block_height": 5553
}
tx_hex is the canonical serialized form (the exact bytes used to compute
tx_id). To recover output amounts and locking scripts, deserialize tx_hex
using Exfer's transaction format (see
EXFER.md §4–5).
Errors
-32602 Transaction not found— unknown txid, or evicted from mempool, or reorged out of the chain.
get_balance
Sum of unspent output values for an address (script hash), respecting coinbase maturity at the current tip.
Params
{ "address": "<64-hex>" }
Result
{
"address": "8d896d64...50",
"balance": 7368884920683
}
balance is in exfers. Divide by 100_000_000 to get EXFER.
This endpoint counts against the UTXO-scan rate limit (30 / min / IP).
get_address_utxos
Enumerate unspent outputs for an address. Equivalent to
get_balance but itemized.
Params
{ "address": "<64-hex>" }
Result
{
"address": "8d896d64...50",
"tip_height": 5553,
"truncated": false,
"utxos": [
{
"tx_id": "fb8a634f...8c",
"output_index": 0,
"value": 1000000000,
"height": 5400,
"is_coinbase": false
},
{ "...": "..." }
]
}
Notes
- Returns up to 1000 UTXOs. If the address has more,
truncated: trueis set; callers must consume some entries (e.g. by spending them) before the remainder becomes reachable through this endpoint, or rely on a full-archive integration. is_coinbase: trueoutputs are not yet spendable iftip_height - height < 360.- Confirmation count:
tip_height - height + 1.
Counts against the UTXO-scan rate limit (30 / min / IP).
get_script_utxos
Enumerate unspent outputs locked by an arbitrary output script. Allows querying UTXOs locked by scripts beyond plain address-hash form (e.g. HTLC, multisig, covenant scripts).
Params
{ "script_hex": "<hex-encoded output script, ≤ 65535 bytes>" }
Result
{
"script_hex": "<echo of the queried script_hex>",
"tip_height": 5553,
"truncated": false,
"utxos": [
{
"tx_id": "fb8a634f...8c",
"output_index": 0,
"value": 1000000000,
"script_len": 142,
"height": 5400,
"is_coinbase": false
}
]
}
Notes
script_lenis the byte length of the queried script (constant per response — it reflects the request, not per-UTXO state).truncated, coinbase maturity, confirmation count, and 1000-entry cap follow the same semantics asget_address_utxos.
Counts against the UTXO-scan rate limit (30 / min / IP). Request body cap
is 200 KB (script_hex is up to 131 070 hex chars, since each byte encodes
to two hex chars).
send_raw_transaction
Broadcast a signed transaction. The node validates against the current tip, inserts into mempool, and relays to peers.
Params
{ "tx_hex": "<canonical serialized transaction, hex>" }
Result
{ "tx_id": "fb8a634f...8c" }
tx_id is the protocol-canonical hash of the serialized form. Callers
should reconstruct it locally and compare before trusting the value
returned by the RPC server.
Errors
-32602 Failed to deserialize transaction: ...— malformed bytes-32602 Trailing bytes after transaction: ...— extra bytes after tx body-32602 Mempool pre-check failed: ...— duplicate tx, double-spend, etc.-32602 Transaction validation failed: ...— consensus rule violation (missing UTXO, immature coinbase, fee too low, script failure, etc.)-32603 Tip changed during validation, try again— retriable race
Rate-limited to 60 / min / IP. Body cap 2.5 MiB.
On success, the transaction is in the receiving node's local mempool. To verify propagation, query a different node or wait for inclusion in a block.
Confirmation depth & reorg semantics
Exfer has a 10 s target block time. Reorg resistance grows with
confirmation depth; a transaction at depth d survives any reorg shorter
than d blocks. As reference points:
| Depth | Wall time (~) | Notes |
|---|---|---|
| 1 | 10 s | Block-inclusion confirmation; reorg-fragile |
| 30 | 5 min | Survives short reorg activity under normal conditions |
| 60 | 10 min | Conservative for most non-trivial value transfers |
| 360 | ~1 hr | Matches coinbase maturity; deep finality |
A confirmed transaction's block_hash can change if a reorg occurs. Callers
that persist chain state should record (tx_id, block_hash, block_height)
for each watched transaction and periodically re-call
get_transaction. If the returned block_hash
differs from the previously recorded value, the transaction has been
reorganized and must be re-evaluated.
References
- Protocol specification:
EXFER.md - Source of truth for RPC method dispatch:
src/rpc.rs - Releases and binaries: https://github.com/ahuman-exfer/exfer/releases
Changes to the RPC surface are noted in
CHANGELOG.md
alongside the release that introduced them.
Exfer: A Peer-to-Peer Settlement Protocol for Autonomous Machines
Version 1.0.0
Overview
Currency, contracts, and enforcement make it possible for human beings to work collectively — achieving outcomes no single human can reach alone. The same applies to autonomous machines. Exfer provides all three, native to autonomous machines, with finality. The protocol is the minimum infrastructure that turns independent agents into an economy.
Exfer is a permissionless proof-of-work blockchain for autonomous machine-to-machine commerce. It combines Argon2id memory-hard mining, an extended UTXO model, and Exfer Script — a total functional combinator language — for transaction conditions. Miners solve proof-of-work puzzles to produce blocks; transactions transfer value under conditions expressed as combinator programs that are statically typed, guaranteed to terminate, and have costs computable before execution.
All scripts terminate. Costs are statically computable before execution. The UTXO model eliminates global state and reentrancy. An autonomous agent can construct a transaction, compute its exact cost, and know with certainty that it will validate — without simulating execution, competing in a fee auction, or reasoning about concurrent state changes. There is no gas estimation. There is no mempool priority auction. Scripts are Merkleized — commit to the full program when locking funds, reveal only the executed path when spending.
This is operational context. It affects how an agent plans transactions: knowing costs are deterministic means precomputing fees, knowing scripts terminate means no timeout logic for validation, knowing there is no state contention means transactions can be constructed independently.
Part I: Protocol Specification
1. Notation and Conventions
Byte order. All multi-byte integers are serialized in little-endian byte order unless stated otherwise. The sole exception: 256-bit values used in difficulty target comparisons and cumulative work are compared as big-endian unsigned integers (their byte-level representation is the numeric big-endian form).
Integer types. u8, u16, u32, u64 denote unsigned integers of the stated bit width. u128 is used as an intermediate type for overflow-safe arithmetic; it never appears on the wire.
Hash256. A 32-byte SHA-256 digest, stored and transmitted as a raw 32-byte sequence.
VarBytes. A variable-length byte string: length(u16 LE) || data[length]. Maximum data length: 65,535 bytes (u16 range).
ceil_div(a, b). Ceiling division. Computed as (a + b - 1) / b using u128 intermediate to prevent overflow of a + b - 1.
Pseudocode. All pseudocode is language-agnostic. || denotes byte concatenation. [n] denotes an array of n bytes. Indices are zero-based.
2. Data Types and Encoding
| Type | Wire size | Encoding |
|---|---|---|
| u8 | 1 byte | unsigned integer |
| u16 | 2 bytes | little-endian |
| u32 | 4 bytes | little-endian |
| u64 | 8 bytes | little-endian |
| Hash256 | 32 bytes | raw bytes (big-endian as a 256-bit number) |
| VarBytes | 2 + length bytes | u16 LE length prefix, then data |
| Bool flag | 1 byte | 0x00 = absent/false, 0x01 = present/true |
3. Domain-Separated Hashing
All hash computations use domain-separated SHA-256. The construction is prefix-free:
domain_hash(separator, data) = SHA-256(len(separator) || separator || data)
where len(separator) is a single byte encoding the separator length (0–255). This ensures no domain separator is a prefix of another's encoding.
Raw SHA-256 (without domain separation) is used only for:
- Block ID computation:
SHA-256(header_bytes) - SMT internal nodes:
SHA-256(left || right) - SMT leaf values:
SHA-256(output_bytes || height_u64_le || is_coinbase_u8) - Datum hashing:
SHA-256(datum_bytes)
PoW password and salt derivation use domain_hash (i.e., SHA-256(len_byte || separator || header_bytes)), not raw SHA-256.
The complete domain separator catalog is in Appendix B.
4. Block Structure
4.1 Block Header
Fixed 156 bytes. All integers little-endian. Hash fields are raw 32-byte sequences.
| Offset | Size | Field | Type | Description |
|---|---|---|---|---|
| 0 | 4 | version | u32 | Protocol version. Must be 1. |
| 4 | 8 | height | u64 | Block height. Genesis = 0. |
| 12 | 32 | prev_block_id | Hash256 | Parent block's ID. Genesis = all zeros. |
| 44 | 8 | timestamp | u64 | Unix timestamp (seconds since epoch). |
| 52 | 32 | difficulty_target | Hash256 | Full 256-bit PoW target. |
| 84 | 8 | nonce | u64 | Miner-chosen value for PoW search. |
| 92 | 32 | tx_root | Hash256 | Merkle root of transaction hashes. |
| 124 | 32 | state_root | Hash256 | Sparse Merkle tree root of UTXO set. |
Total: 156 bytes.
4.2 Block ID
block_id = SHA-256(header_bytes)
Input is the 156-byte serialized header. This is raw SHA-256 (no domain separator). Block ID is NOT the Argon2id hash.
4.3 Block Body
Serialized immediately after the header:
tx_count(u32 LE) || transaction[0] || transaction[1] || ... || transaction[tx_count-1]
- Transaction 0 must be the coinbase transaction.
- No other transaction may have the coinbase sentinel outpoint.
- Maximum block size: 4,194,304 bytes (4 MiB), measured on the full serialized block.
4.4 Transaction Merkle Root
The tx_root is a Merkle tree over witness-committed transaction hashes (WtxId), not TxId. This prevents block malleability by committing to all witness data.
Leaf values: For each transaction, compute WtxId = domain_hash("EXFER-WTXID", full_serialization).
Tree construction:
- If zero transactions: root = all-zero Hash256.
- If one transaction: root = that transaction's WtxId.
- If odd count: duplicate the last hash.
- Pair adjacent hashes and compute parent:
domain_hash("EXFER-TXROOT", left || right). - Repeat until one hash remains.
4.5 State Root (Sparse Merkle Tree)
The state root commits to the complete UTXO set via a sparse Merkle tree of depth 256.
Leaf key:
leaf_key = domain_hash("EXFER-STATE", tx_id || output_index_le32)
Leaf value:
leaf_value = SHA-256(canonical_output_bytes || height_u64_le || is_coinbase_u8)
where canonical_output_bytes is the serialized TxOutput (Section 5.4), height_u64_le is the u64 LE block height at which the UTXO was created, and is_coinbase_u8 is 0x01 if the UTXO came from a coinbase transaction, 0x00 otherwise.
Empty subtree hashes:
empty_hash[0] = [0x00; 32]
empty_hash[d] = SHA-256(empty_hash[d-1] || empty_hash[d-1]) for d = 1..256
Internal nodes:
node_hash = SHA-256(left_child_hash || right_child_hash)
Path: The 256 bits of the leaf key determine the path from root to leaf. Bit 0 (MSB of byte 0) selects left (0) or right (1) at depth 0. Bit 255 (LSB of byte 31) selects at depth 255.
Empty UTXO set root: empty_hash[256].
5. Transactions
5.1 Transaction Structure
A transaction consists of three sections serialized in order:
tx_header || tx_body || tx_witnesses
tx_header (4 bytes):
input_count(u16 LE) || output_count(u16 LE)
tx_body (variable):
input[0] || ... || input[input_count-1] || output[0] || ... || output[output_count-1]
Each input (36 bytes):
prev_tx_id(Hash256) || output_index(u32 LE)
Each output (variable): see Section 5.4.
tx_witnesses (variable):
witness[0] || ... || witness[input_count-1]
The witness count equals the input count. Each witness:
VarBytes(witness_data) || has_redeemer(u8) || [VarBytes(redeemer) if has_redeemer=1]
5.2 Transaction ID (TxId)
TxId = domain_hash("EXFER-TX", tx_header || tx_body)
Witnesses are excluded from TxId computation. This prevents witness malleability from changing transaction identity.
5.3 Witness-Committed Transaction ID (WtxId)
WtxId = domain_hash("EXFER-WTXID", tx_header || tx_body || tx_witnesses)
Includes the full serialization. Used in the block's tx_root Merkle tree.
5.4 Output Serialization
Each output is serialized as:
value(u64 LE)
|| VarBytes(script)
|| has_datum(u8) || [VarBytes(datum) if has_datum=1]
|| has_datum_hash(u8) || [Hash256(datum_hash) if has_datum_hash=1]
Flags: has_datum and has_datum_hash are single bytes. Value 0x00 = absent, 0x01 = present. Any other value is non-canonical and must be rejected.
Constraints:
- Datum size: at most 4,096 bytes.
- If both datum and datum_hash are present,
SHA-256(datum)must equal datum_hash.
6. Exfer Script
Exfer Script is a total functional combinator language. Programs are directed acyclic graphs (DAGs) of combinator nodes. All programs terminate. Costs are statically computable.
6.1 Type System
Type ::= Unit
| Sum(Type, Type)
| Product(Type, Type)
| List(Type)
| Bound(k) -- bounded natural: values 0..k-1
Derived types:
Bool = Sum(Unit, Unit)— Left = false, Right = trueOption(A) = Sum(Unit, A)— Left(Unit) = None, Right(a) = Some(a)Bytes = List(Bound(256))— variable-length byte stringHash256 = Bound(0)— sentinel for 256-bit hash (opaque to type system, handled by jets)U64 = Bound(u64_max)— 64-bit unsigned integerU256— 256-bit unsigned integer, a nominal type with a dedicatedType::U256variant. U256 is not an alias forProduct(U64, U64): it is a distinct type that prevents type confusion. A U256 value produced by 256-bit arithmetic jets (Add256, Mul256, etc.) cannot be consumed by product projections (Take/Drop) or 64-bit jets (Eq64), and vice versa. The wire format is unchanged (tag 0x07, 32 bytes big-endian).
6.2 Combinators
| Combinator | Notation | Type Rule |
|---|---|---|
| Iden | A → A | Identity function |
| Unit | A → Unit | Discard input, return unit |
| Comp(f, g) | A → C | f: A→B, g: B→C — composition |
| Pair(f, g) | A → Product(B, C) | f: A→B, g: A→C — both on same input |
| Take(f) | Product(A, B) → C | f: A→C — project first |
| Drop(f) | Product(A, B) → C | f: B→C — project second |
| InjL(f) | A → Sum(B, C) | f: A→B — inject left |
| InjR(f) | A → Sum(B, C) | f: A→C — inject right |
| Case(f, g) | Sum(A, B) → C | f: A→C, g: B→C — branch on tag |
| Fold(f, z, k) | Product(Bound(k), A) → B | z: A→B, f: Product(A, B)→B — bounded iteration |
| ListFold(f, z) | Product(List(A), B) → B | z: B→B, f: Product(A, B)→B — list iteration |
| Jet(id) | per jet | Native operation (Section 7) |
| Witness | Unit → T | Read value from witness data at evaluation time |
| MerkleHidden(hash) | — | Pruned subtree placeholder (cannot be evaluated) |
| Const(v) | Unit → T | Constant value embedded in program |
DAG invariant: Nodes are stored in an arena. Children have strictly higher indices than their parent. Node 0 is the root. All nodes must be reachable from the root.
6.3 Typing Rules
Type inference proceeds bottom-up (from leaves to root). Unit serves as a wildcard in type inference: any type position occupied by Unit is compatible with any other type.
For each combinator, with child types already inferred:
- Iden:
A → A. Initially typed as input = Unit, output = Unit (both unconstrained). Parent context refines A to a concrete type. The input and output are always the same type. - Unit:
A → Unit. Input is unconstrained (initially Unit). Output is always Unit. - Comp(f, g): f.output must be compatible with g.input. Result: input = f.input, output = g.output.
- Pair(f, g): Both children take the same input. Result: input = f.input (or g.input if f.input is Unit), output = Product(f.output, g.output).
- Take(f): Result: input = Product(f.input, Unit), output = f.output.
- Drop(f): Result: input = Product(Unit, f.input), output = f.output.
- InjL(f): Result: input = f.input, output = Sum(f.output, Unit).
- InjR(f): Result: input = f.input, output = Sum(Unit, f.output).
- Case(f, g): f.output must be compatible with g.output. Result: input = Sum(f.input, g.input), output = f.output (or g.output if f.output is Unit).
- Fold(f, z, k): z.output and f.output must be compatible. Result: input = Product(Bound(k), z.input), output = z.output (or f.output if z.output is Unit).
- ListFold(f, z): z.output and f.output must be compatible. Element type extracted from f.input if it is Product(A, B). Result: input = Product(List(elem_type), z.input), output = z.output (or f.output if z.output is Unit).
- Jet(id): Input and output types are fixed per jet (Section 7).
- Witness: input = Unit, output = Unit (refined by parent context).
- MerkleHidden(hash): input = Unit, output = Unit.
- Const(v): input = Unit, output = inferred from value.
Compatibility rule: Two types are compatible if they are equal, or if either is Unit, or if they are structurally matching (both Sum, both Product, both List) with pairwise compatible components.
Refinement exception: During type refinement, fixed-output combinators (Unit, Const, Jet) are never refined. Their output types are determined by their definition and cannot be overridden by parent context. This prevents the typechecker from claiming a Unit combinator produces U64.
6.4 Evaluation Semantics
Programs are evaluated recursively on the DAG with a resource budget (steps and cells). Maximum evaluation depth: 128.
- Iden: Return input unchanged. Cost: 1 step.
- Unit: Return Unit. Cost: 1 step.
- Comp(f, g): Evaluate f on input, then evaluate g on f's result. Cost: 1 step + f's cost + g's cost.
- Pair(f, g): Evaluate f and g both on the same input. Return Product(f_result, g_result). Cost: 1 step + 1 cell + f's cost + g's cost.
- Take(f): Input must be Product(a, b). Evaluate f on a. Cost: 1 step + f's cost.
- Drop(f): Input must be Product(a, b). Evaluate f on b. Cost: 1 step + f's cost.
- InjL(f): Evaluate f on input. Return Left(result). Cost: 1 step + 1 cell + f's cost.
- InjR(f): Evaluate f on input. Return Right(result). Cost: 1 step + 1 cell + f's cost.
- Case(f, g): Input must be Left(a), Right(b), or Bool. If Left(a) or Bool(false): evaluate f on a (or Unit). If Right(b) or Bool(true): evaluate g on b (or Unit). Cost: 1 step + selected branch cost.
- Fold(f, z, k): Input must be Product(_, init). Evaluate z on init to get initial accumulator. Then k times: evaluate f on Product(init, accumulator). Cost: 1 step + z's cost + k × (1 step + f's cost).
- ListFold(f, z): Input must be Product(list, init). Evaluate z on init to get initial accumulator. For each element in list: evaluate f on Product(element, accumulator). Cost: 1 step + z's cost + n × (1 step + f's cost), where n = list length.
- Jet(id): Charge runtime cost (data-proportional). Execute native operation. Cost: varies per jet.
- Witness: Read next value from witness byte stream. Cost: 1 step. When a Witness combinator is evaluated, the deserialized value is validated against the expected output type from the typechecker. Values that don't match (e.g., Left(U64(7)) when Bool is expected) are rejected with a WitnessError before they can influence control flow. If the expected type is Unit (unresolved by the typechecker), any value is accepted.
- MerkleHidden(hash): Cannot be evaluated. Always errors.
- Const(v): Return the constant value. Cost: 1 step + 1 cell.
Witness consumption: After evaluation completes, all witness bytes must be consumed. Unconsumed witness bytes cause validation failure (prevents witness malleability).
Script success: A script succeeds if and only if evaluation returns Bool(true).
6.5 Cost Model
Static cost is computed bottom-up on the DAG before evaluation. Cost has two components: steps (execution operations) and cells (heap allocations).
| Combinator | Steps | Cells |
|---|---|---|
| Iden | 1 | 0 |
| Unit | 1 | 0 |
| Comp(f, g) | cost(f).steps + cost(g).steps + 1 | cost(f).cells + cost(g).cells |
| Pair(f, g) | cost(f).steps + cost(g).steps + 1 | cost(f).cells + cost(g).cells + 1 |
| Take(f) | cost(f).steps + 1 | cost(f).cells |
| Drop(f) | cost(f).steps + 1 | cost(f).cells |
| InjL(f) | cost(f).steps + 1 | cost(f).cells + 1 |
| InjR(f) | cost(f).steps + 1 | cost(f).cells + 1 |
| Case(f, g) | max(cost(f).steps, cost(g).steps) + 1 | max(cost(f).cells, cost(g).cells) + 1 |
| Fold(f, z, k) | 1 + cost(z).steps + k × (cost(f).steps + 1) | cost(z).cells + k × cost(f).cells |
| ListFold(f, z) | 1 + cost(z).steps + n × (cost(f).steps + 1) | cost(z).cells + n × cost(f).cells |
| Jet(id) | jet_cost(id).steps | jet_cost(id).cells |
| Witness | 1 | 0 |
| MerkleHidden | 0 | 0 |
| Const(v) | 1 + ceil_div(serialized_bytes(v), 64) | 1 + ceil_div(serialized_bytes(v), 64) |
For ListFold, n = max(input_count, output_count) of the spending transaction (known at validation time).
Per-input step cap: 4,000,000 steps. Scripts exceeding this are rejected. Per-transaction step budget: 20,000,000 steps (sum over all inputs). Memory limit: 16,777,216 bytes (16 MiB) per script evaluation. Maximum DAG depth: 128. Maximum node count: 65,535.
6.6 Serialization and Merkle Commitment
Binary serialization format:
node_count(u32 LE) || root_index(u32 LE) || node[0] || node[1] || ... || node[node_count-1]
Each node is serialized with a tag byte followed by combinator-specific data:
| Tag | Combinator | Data |
|---|---|---|
| 0x00 | Iden | — |
| 0x01 | Comp(f, g) | f(u32 LE) g(u32 LE) |
| 0x02 | Unit | — |
| 0x03 | Pair(f, g) | f(u32 LE) g(u32 LE) |
| 0x04 | Take(f) | f(u32 LE) |
| 0x05 | Drop(f) | f(u32 LE) |
| 0x06 | InjL(f) | f(u32 LE) |
| 0x07 | InjR(f) | f(u32 LE) |
| 0x08 | Case(f, g) | f(u32 LE) g(u32 LE) |
| 0x09 | Fold(f, z, k) | f(u32 LE) z(u32 LE) k(u64 LE) |
| 0x0A | ListFold(f, z) | f(u32 LE) z(u32 LE) |
| 0x0B | Jet(id) | id(u32 LE) |
| 0x0C | Witness | — |
| 0x0D | MerkleHidden(h) | hash(32 bytes) |
| 0x0E | Const(v) | value_len(u32 LE) value_bytes |
Value serialization tags:
| Tag | Value | Data |
|---|---|---|
| 0x00 | Unit | — |
| 0x01 | Left(v) | value |
| 0x02 | Right(v) | value |
| 0x03 | Pair(a, b) | a then b |
| 0x04 | List(vs) | count(u32 LE) then elements |
| 0x05 | Bytes(bs) | length(u32 LE) then data |
| 0x06 | U64(n) | n(u64 LE) |
| 0x07 | U256(d) | data(32 bytes, big-endian) |
| 0x08 | Bool(b) | 0x00=false, 0x01=true |
| 0x09 | Hash(h) | hash(32 bytes) |
Merkle commitment:
The Merkle hash of a program is computed bottom-up. For each node:
node_merkle_hash = domain_hash("EXFER-SCRIPT", tag_byte || child_hash_1 || child_hash_2 || ...)
Children are referenced by their Merkle hash (not their NodeId). Specific formats:
- Leaf nodes (Iden, Unit, Witness):
domain_hash("EXFER-SCRIPT", [tag]) - Single-child (Take, Drop, InjL, InjR):
domain_hash("EXFER-SCRIPT", [tag] || child_hash) - Two-child (Comp, Pair, Case, ListFold):
domain_hash("EXFER-SCRIPT", [tag] || f_hash || g_hash) - Fold:
domain_hash("EXFER-SCRIPT", [tag] || f_hash || z_hash || k_le8) - Jet:
domain_hash("EXFER-SCRIPT", [tag] || id_le4) - MerkleHidden(h): returns
hdirectly (the hash IS the commitment) - Const(v):
domain_hash("EXFER-SCRIPT", [tag] || value_bytes)
Canonical serialization: Deserializing a script and re-serializing it must produce identical bytes. Non-canonical encodings are rejected.
7. Jets
Jets are native operations with fixed type signatures and known costs. Each jet has a 32-bit numeric ID.
7.1 Jet Registry
| Category | ID Range | Jets |
|---|---|---|
| Cryptographic | 0x0001–0x0004 | Sha256, Ed25519Verify, SchnorrVerify, MerkleVerify |
| Arithmetic (64-bit) | 0x0100–0x0107 | Add64, Sub64, Mul64, Div64, Mod64, Eq64, Lt64, Gt64 |
| Arithmetic (256-bit) | 0x0200–0x0207 | Add256, Sub256, Mul256, Div256, Mod256, Eq256, Lt256, Gt256 |
| Byte Operations | 0x0300–0x0304 | Cat, Slice, Len, EqBytes, EqHash |
| Introspection | 0x0400–0x0408 | TxInputs, TxOutputs, TxValue, TxScriptHash, TxInputCount, TxOutputCount, SelfIndex, BlockHeight, TxSigHash |
| List Operations | 0x0500–0x0505 | ListLen, ListAt, ListSum, ListAll, ListAny, ListFind |
SchnorrVerify (0x0003) is reserved. It is not implemented and always fails. Output scripts containing unimplemented jets are rejected (funds would be permanently locked).
7.2 Cryptographic Jets
Sha256 (0x0001)
- Input:
Bytes - Output:
Hash256 - Computes SHA-256 of input bytes.
- Static cost: 1,000 steps, 1 cell.
- Runtime cost:
500 + (len / 64) × 8steps.
Ed25519Verify (0x0002)
- Input:
Product(Bytes, Product(Bytes, Bytes))— (message, (pubkey, signature)) - Output:
Bool - Pubkey must be exactly 32 bytes. Signature must be exactly 64 bytes. Returns false if lengths are wrong.
- Rejects small-order (weak) public keys. Returns false if the pubkey is a point of order dividing 8.
- Uses ZIP-215 verification (accepts non-canonical point encodings).
- Static cost: 5,000 steps, 1 cell.
- Runtime cost:
5,000 + ceil_div(message_bytes, 64) × 8steps.
MerkleVerify (0x0004)
- Input:
Product(Hash256, Product(Hash256, Bytes))— (root, (leaf, proof)) - Output:
Bool - Proof format: sequence of 33-byte steps
[side(u8) || sibling(32 bytes)]. Side 0 = current is left child, side 1 = current is right child. Returns false if proof length is not a multiple of 33 or any side byte exceeds 1. - Internal hashing:
domain_hash("EXFER-MERKLE", left || right). - Static cost: 32,000 steps, 1 cell.
- Runtime cost:
500 + (proof_len / 33) × 500steps.
7.3 Arithmetic Jets (64-bit)
All take Product(U64, U64) input.
| Jet | ID | Output | Behavior | Error |
|---|---|---|---|---|
| Add64 | 0x0100 | U64 | a + b | Overflow if result > u64 max |
| Sub64 | 0x0101 | U64 | a - b | Overflow if a < b |
| Mul64 | 0x0102 | U64 | a × b | Overflow if result > u64 max |
| Div64 | 0x0103 | U64 | a / b (floor) | DivisionByZero if b = 0 |
| Mod64 | 0x0104 | U64 | a mod b | DivisionByZero if b = 0 |
| Eq64 | 0x0105 | Bool | a = b | — |
| Lt64 | 0x0106 | Bool | a < b | — |
| Gt64 | 0x0107 | Bool | a > b | — |
Static cost: 10 steps, 1 cell (all).
7.4 Arithmetic Jets (256-bit)
All take Product(U256, U256) input. U256 values are 32 bytes, big-endian.
| Jet | ID | Output | Behavior | Error |
|---|---|---|---|---|
| Add256 | 0x0200 | U256 | a + b | Overflow if result ≥ 2^256 |
| Sub256 | 0x0201 | U256 | a - b | Overflow if a < b |
| Mul256 | 0x0202 | U256 | a × b | Overflow if result ≥ 2^256 |
| Div256 | 0x0203 | U256 | a / b (floor) | DivisionByZero if b = 0 |
| Mod256 | 0x0204 | U256 | a mod b | DivisionByZero if b = 0 |
| Eq256 | 0x0205 | Bool | a = b | — |
| Lt256 | 0x0206 | Bool | a < b (big-endian) | — |
| Gt256 | 0x0207 | Bool | a > b (big-endian) | — |
Static cost: 50 steps, 1 cell (all).
7.5 Byte Operation Jets
Cat (0x0300)
- Input:
Product(Bytes, Bytes)— Output:Bytes - Concatenates the two byte sequences.
- Static cost: 100 steps, 1 cell.
- Runtime cost:
10 + total_len / 8steps.
Slice (0x0301)
- Input:
Product(Bytes, Product(U64, U64))— (source, (start, length)) - Output:
Bytes - Returns
source[start .. min(start+length, source.len())]. If start > source.len(), returns empty bytes. - Static cost: 100 steps, 1 cell.
- Runtime cost:
10 + source_len / 8steps.
Len (0x0302)
- Input:
Bytes— Output:U64 - Returns byte count.
- Static cost: 10 steps, 0 cells.
EqBytes (0x0303)
- Input:
Product(Bytes, Bytes)— Output:Bool - Accepts both Bytes and Hash values. Returns true if byte-equal.
- Static cost: 500 steps, 0 cells.
- Runtime cost:
10 + max(len_a, len_b) / 8steps.
EqHash (0x0304)
- Input:
Product(Hash256, Hash256)— Output:Bool - Accepts both Bytes and Hash values. Returns true if byte-equal.
- Static cost: 500 steps, 0 cells.
- Runtime cost: 14 steps.
7.6 Introspection Jets
These jets access the transaction context during script evaluation.
TxInputs (0x0400)
- Input:
Unit - Output:
List(Product(Hash256, Product(U64, Product(U64, Hash256)))) - Each element: (prev_tx_id, (output_index, (value, script_hash))).
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + input_count × 10steps.
TxOutputs (0x0401)
- Input:
Unit - Output:
List(Product(U64, Product(Hash256, Option(Hash256)))) - Each element: (value, (script_hash, datum_hash)).
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + output_count × 10steps.
TxValue (0x0402)
- Input:
U64(input index) - Output:
U64(value in exfers) - Error: OutOfBounds if index ≥ input count.
- Static cost: 10 steps, 0 cells.
TxScriptHash (0x0403)
- Input:
U64(input index) - Output:
Hash256 - Error: OutOfBounds if index ≥ input count.
- Static cost: 10 steps, 0 cells.
TxInputCount (0x0404)
- Input:
Unit— Output:U64 - Static cost: 5 steps, 0 cells.
TxOutputCount (0x0405)
- Input:
Unit— Output:U64 - Static cost: 5 steps, 0 cells.
SelfIndex (0x0406)
- Input:
Unit— Output:U64 - Returns the index of the input currently being validated.
- Static cost: 5 steps, 0 cells.
BlockHeight (0x0407)
- Input:
Unit— Output:U64 - Returns current block height.
- Static cost: 5 steps, 0 cells.
TxSigHash (0x0408)
- Input:
Unit— Output:Bytes - Returns the domain-separated signing digest:
"EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body. - Static cost: 5 steps, 0 cells.
- Runtime cost:
5 + sig_hash_len / 64steps.
7.7 List Operation Jets
ListLen (0x0500)
- Input:
List(A)— Output:U64 - Static cost: 10 steps, 0 cells.
ListAt (0x0501)
- Input:
Product(List(A), U64)— Output:Option(A) - Returns Some(element) if index in bounds, None otherwise.
- Static cost: 10 steps, 1 cell.
ListSum (0x0502)
- Input:
List(U64)— Output:U64 - Sums all elements. Returns 0 for empty list. Error: Overflow.
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + list_lengthsteps.
ListAll (0x0503)
- Input:
List(Bool)— Output:Bool - Returns true if all elements are true (vacuously true for empty list).
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + list_lengthsteps.
ListAny (0x0504)
- Input:
List(Bool)— Output:Bool - Returns true if any element is true (false for empty list).
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + list_lengthsteps.
ListFind (0x0505)
- Input:
List(Bool)— Output:Option(U64) - Returns Some(index) of first true element, None if none found.
- Static cost: 1,000 steps, 0 cells.
- Runtime cost:
10 + list_lengthsteps.
8. Script Evaluation (Locking and Unlocking)
8.1 Output Locking
An output is locked by placing a script commitment in its script field:
- Pubkey hash lock (32-byte script): The script field contains
domain_hash("EXFER-ADDR", pubkey), a 32-byte pubkey hash. This is the simple signature-based locking mechanism. - Script lock (>32-byte script): The script field contains the full serialized program (Section 6.6). At spend time, the validator deserializes, type-checks, computes cost, and evaluates the program.
The distinction is purely by length: exactly 32 bytes = pubkey hash lock. Any other length = script lock.
Ambiguity guard: If a 32-byte script also deserializes as a valid Exfer Script program, the output is rejected (prevents ambiguous spending semantics).
8.2 Input Validation
For each input in a transaction:
If the spent output's script is a pubkey hash lock (32 bytes):
- Witness must be exactly 96 bytes:
pubkey(32) || signature(64). - Redeemer must be absent.
- Compute
domain_hash("EXFER-ADDR", pubkey). Must equal the script. - Signing message:
"EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body. - Reject the pubkey if it is a small-order (weak) Ed25519 point — such keys can validate signatures across unrelated messages.
- Ed25519 verify (ZIP-215) the signature over the signing message with the pubkey.
If the spent output's script is a script lock (≠32 bytes):
- Deserialize the script. Re-serialize and verify byte-for-byte equality (canonical check).
- Type-check the program. Root must output Bool.
- Strict type edge check: all composition edges have exact type matches (no Unit wildcards in internal edges).
- Reject scripts containing unimplemented jets, MerkleHidden nodes, or heterogeneous list constants.
- Root input type must be compatible with the runtime input shape (Section 8.5).
- DAG depth must not exceed 128.
- Minimum-case cost must not exceed 4,000,000 steps.
- Resolve datum (Section 8.3).
- Build script input value (Section 8.5).
- Compute cost with actual list sizes from the transaction.
- Reject if cost exceeds 4,000,000 steps.
- Evaluate with budget = 4,000,000 steps, computed cells.
- Result must be
Bool(true).
8.3 Datum Resolution
if output has inline datum:
if output also has datum_hash:
verify SHA-256(datum) = datum_hash
return datum
else if output has datum_hash:
spender must provide datum in witness redeemer field
verify SHA-256(provided_datum) = datum_hash
verify provided datum length ≤ 4,096 bytes
return provided_datum
else:
return None
8.4 Redeemer Handling
The redeemer is an optional byte string in the witness. For pubkey hash locks, the redeemer must be absent. For script locks, the redeemer is available to the script via the input value and may also serve as the datum provider for hash-committed datums.
8.5 Script Context
The runtime provides each script with an input value of type:
Product(Bytes, Product(Option(Bytes), Product(Option(Bytes), Unit)))
Meaning: (witness, (redeemer_opt, (datum_opt, ()))).
The script context — accessed via introspection jets — contains:
- All transaction inputs: (prev_tx_id, output_index, value, script_hash) per input
- All transaction outputs: (value, script_hash, datum_hash) per output
- Self index: the index of the input being evaluated
- Block height
- Signing digest:
"EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body
8.6 Budget Enforcement
- Per-input: 4,000,000 steps maximum.
- Per-transaction: 20,000,000 steps maximum (sum of actual runtime costs across all inputs).
- Memory: 16 MiB per script evaluation.
- Actual runtime cost (not static estimate) is used for fee calculation.
9. Proof of Work
The PoW hash is computed using Argon2id with independent domain separators for password and salt:
pw = domain_hash("EXFER-POW-P", header_bytes)
salt = domain_hash("EXFER-POW-S", header_bytes)
pow = Argon2id(password=pw, salt=salt, m=65536, t=2, p=1, output_len=32)
Parameters:
- Memory: 65,536 KiB (64 MiB)
- Iterations: 2
- Parallelism: 1
- Output length: 32 bytes
- Algorithm version: 0x13
Validity condition: pow < difficulty_target, where both values are compared as 256-bit big-endian unsigned integers (lexicographic byte comparison).
10. Difficulty Adjustment
Retarget window: 4,320 blocks. Target block time: 10 seconds. Expected time for a window: (4,320 - 1) × 10 = 43,190 seconds.
When to retarget: At every block whose height is a non-zero multiple of 4,320.
Retarget formula:
actual_time = timestamp(height - 1) - timestamp(height - 4320)
if actual_time = 0: actual_time = 1
min_time = expected_time / 4
max_time = expected_time × 4
clamped_time = clamp(actual_time, min_time, max_time)
new_target = old_target × clamped_time / expected_time
old_targetis the parent block's difficulty_target.- Arithmetic uses 256-bit multiply/divide (no floating point).
- Result clamped to minimum value 1 (target = 0 means no valid hash exists).
- No maximum target clamp (can reach [0xFF; 32]).
- Overflow in multiply saturates to [0xFF; 32].
Non-retarget blocks: Inherit parent's difficulty_target unchanged.
Genesis target: 2^248 (byte representation: [0x01, 0x00, ..., 0x00]).
11. Emission
Block reward formula:
R(height) = BASE_REWARD + floor(DECAY_COMPONENT × 2^(-height / HALF_LIFE))
Constants:
- BASE_REWARD = 100,000,000 (1 EXFER, the asymptotic minimum)
- DECAY_COMPONENT = 9,900,000,000 (99 EXFER)
- HALF_LIFE = 6,307,200 blocks (~2 years at 10s blocks)
- 1 EXFER = 100,000,000 exfers
Implementation: Q64.64 fixed-point arithmetic with a 4,097-entry lookup table.
LUT construction: LUT[0] = 2^64. LUT[i] = LUT[i-1] × K >> 64 where K = 18,443,622,869,203,936,790 (consensus-canonical constant). LUT[4096] = 9,223,758,693,993,446,757.
Interpolation: bucket_size = ceil(HALF_LIFE / 4096) = 1540. For height h, decompose: q = h / HALF_LIFE, r = h % HALF_LIFE, bucket = r / bucket_size, frac = r % bucket_size. Linear interpolation between LUT[bucket] and LUT[bucket+1]. If q ≥ 128: return BASE_REWARD.
Canonical reward vectors (all implementations must produce these exact values):
| Height | Reward (exfers) |
|---|---|
| 0 | 10,000,000,000 |
| 1 | 9,999,998,912 |
| 100 | 9,999,891,228 |
| 1,000 | 9,998,912,280 |
| 4,320 | 9,995,301,790 |
| 10,000 | 9,989,127,892 |
| 43,200 | 9,953,117,900 |
| 100,000 | 9,891,814,300 |
| 6,307,200 | 5,050,000,000 |
| 12,614,400 | 2,575,000,000 |
| 18,921,600 | 1,337,500,000 |
| 63,072,000 | 109,667,968 |
| 630,720,000 | 100,000,000 |
12. Transaction Validation
A non-coinbase transaction is valid if and only if all of the following hold:
- At least one input.
- At least one output.
- Witness count equals input count.
- No duplicate inputs (same outpoint referenced twice).
- Each input references an existing UTXO.
- Script validation passes for every input (Section 8.2).
- Value conservation:
sum(input_values) ≥ sum(output_values), computed with u128 intermediates. Both sums must fit in u64. - Minimum fee:
fee ≥ ceil_div(tx_cost, 100), where fee = sum(inputs) - sum(outputs) and tx_cost is the 8-component cost (Section 14). Uses actual runtime script cost, not the static estimate. - Dust threshold: Every output value ≥ 200 exfers.
- Coinbase maturity: Inputs spending coinbase outputs must have age ≥ 360 blocks.
- Size limit: Serialized transaction ≤ 1,048,576 bytes (1 MiB).
- Output script validity: Every output script must be well-typed with Bool output, pass strict type edge checks, contain no unimplemented jets or hidden nodes, have compatible root input type, depth ≤ 128, and minimum-case cost ≤ 4,000,000 steps (for script locks). Pubkey hash scripts (32 bytes) must not also deserialize as valid programs.
- Per-transaction script budget: Sum of actual script costs across all inputs ≤ 20,000,000 steps.
Witness size limits: Witness data ≤ 65,535 bytes per input. Redeemer ≤ 16,384 bytes. Datum ≤ 4,096 bytes per output.
Datum consistency: If both datum and datum_hash are present on an output, SHA-256(datum) must equal datum_hash.
Validation order: Cheap checks (UTXO existence, maturity, value sums, output script typing) run before expensive checks (signature verification, script evaluation) to prevent CPU amplification attacks.
13. Coinbase Rules
- Exactly one input with sentinel outpoint: prev_tx_id = all zeros.
- output_index encodes height:
output_index = height as u32. Height > u32::MAX is invalid. - Reward:
sum(output_values) = block_reward(height) + total_fees. Exact equality required. - Dust threshold: Every output value ≥ 200 exfers.
- Exactly one witness: Empty witness data, no redeemer. Exception: the genesis coinbase (height 0) may carry arbitrary witness data (used for the NIST Beacon attestation).
- Position 0 in the block's transaction list.
- Output script validity: Same rules as non-coinbase outputs (Section 12, rule 12).
- Size limit: ≤ 1,048,576 bytes.
- Datum consistency: Same as non-coinbase transactions.
14. Fee and Cost Model
tx_cost is the sum of 8 components:
tx_cost = script_eval_cost
+ output_typecheck_cost
+ witness_deser_cost
+ datum_deser_cost
+ tx_deser_cost
+ utxo_io_cost
+ smt_cost
+ script_validation_cost
- script_eval_cost: Sum over all inputs. Per input:
5,000 + ceil_div(sig_message_bytes, 64) × 8if the spent output's script is a pubkey hash (32 bytes), wheresig_message_bytesis the length of the domain-separated signing message; actual runtime step count from evaluation if the spent output's script is a script lock. - output_typecheck_cost: 1,000 per non-pubkey-hash output (0 for 32-byte scripts).
- witness_deser_cost:
sum over inputs of ceil_div(witness_bytes, 64) + ceil_div(redeemer_bytes, 64). - datum_deser_cost:
sum over outputs of ceil_div(datum_bytes, 64). - tx_deser_cost:
ceil_div(total_serialized_tx_bytes, 64). - utxo_io_cost:
input_count × 100 + output_count × 100. - smt_cost:
input_count × 500 + output_count × 500. - script_validation_cost:
sum over script-locked inputs of ceil_div(script_bytes, 64) × 10. Covers deserialization, canonicalization, type-checking, and cost analysis of each spent script. The multiplier (10) reflects that these operations are more expensive per byte than raw deserialization. Zero for pubkey-hash inputs.
All arithmetic uses u128 intermediates. Result must fit in u64.
Minimum fee:
min_fee = ceil_div(tx_cost, 100)
Dust threshold: 200 exfers (consensus-enforced).
15. Block Validation
A block is valid if and only if all of the following hold:
- Header is 156 bytes (implied by deserialization).
- Version = 1.
- Height = parent.height + 1 (genesis: height = 0).
- prev_block_id = parent.block_id() (genesis: all zeros).
- PoW valid: Argon2id hash < difficulty_target.
- Difficulty target matches expected value from retarget algorithm.
- Timestamp > MTP (median of up to 11 ancestor timestamps).
- Timestamp ≤ wall_clock + 120 seconds (policy, skipped during initial block download).
- Timestamp ≤ parent.timestamp + 604,800 (7-day gap limit, consensus).
- tx_root matches computed Merkle root of WtxIds.
- state_root matches computed SMT root after applying all transactions.
- First transaction is coinbase; no other transaction has sentinel outpoint.
- Coinbase valid (Section 13).
- No duplicate TxIds in the block.
- No double-spends within the block (no two non-coinbase transactions spend the same outpoint).
- Block size ≤ 4,194,304 bytes (4 MiB).
- Each non-coinbase transaction valid (Section 12).
- Coinbase reward = block_reward + total_fees.
Intra-block spending: Transaction at position i may spend outputs created by transactions at positions 0..i-1 in the same block (subject to all validation rules except coinbase maturity, which naturally prevents spending new coinbase outputs).
16. Fork Choice
Work computation:
work = floor(2^256 / target)
Both target and result are 256-bit big-endian unsigned integers. Target = 0 yields maximum representable work. Computed via: floor((2^256 - target) / target) + 1, saturating at 2^256 - 1.
Cumulative work: Sum of per-block work values from genesis to tip. Saturates at 2^256 - 1.
Fork choice rules (in priority order):
- Higher cumulative work is preferred.
- If equal work: higher height is preferred.
- If equal work and equal height: keep the current tip (no reorg).
17. Network Protocol
Wire Format
Handshake messages (Hello, AuthAck):
msg_type(u8) || payload_length(u32 LE) || payload[payload_length]
Post-handshake messages (all other types):
counter(u64 LE) || msg_type(u8) || payload_length(u32 LE) || payload[payload_length] || hmac(16 bytes)
The 8-byte counter is a monotonically increasing 64-bit integer, starting at 0 for each direction. The sender increments after each frame. The receiver rejects any frame whose counter is less than the minimum acceptable value (initially 0; set to counter + 1 after each accepted frame). This prevents replay attacks where a network-level attacker re-injects a previously observed authenticated frame.
The 16-byte HMAC is HMAC-SHA256 truncated to 128 bits, computed over counter(u64 LE) || msg_type || payload_length || payload using the session MAC key. Including the counter in the HMAC binds each tag to a specific sequence position, making replayed frames fail verification even if the frame bytes are identical. The receiver verifies the HMAC before processing the message; verification failure causes immediate disconnection.
Session key derivation: after the handshake completes, both sides convert their Ed25519 identity keys to X25519 (via to_montgomery()) and compute a Diffie-Hellman shared secret with the peer's converted public key. The session MAC key mixes this DH secret with the handshake transcript:
transcript = SHA-256("EXFER-AUTH" || genesis_id || version_le || nonce_a || nonce_b)
dh_shared_secret = X25519(our_identity_scalar, their_identity_montgomery)
session_key = SHA-256("EXFER-SESSION" || transcript || dh_shared_secret)
The DH shared secret can only be computed by holders of either peer's identity private key. An active MITM who relays the handshake cannot derive the session key and therefore cannot forge valid frame MACs. The per-session random nonces ensure each connection gets a unique key.
Direction binding: the session key is further split into two directional MAC keys to prevent cross-direction reflection attacks (where a captured frame from one direction is replayed as the other direction):
i2r_key = SHA-256("EXFER-MAC-IR" || session_key) // initiator → responder
r2i_key = SHA-256("EXFER-MAC-RI" || session_key) // responder → initiator
The initiator uses i2r_key for sending and r2i_key for receiving; the responder uses the reverse. Because each direction has a distinct key, a frame authenticated for one direction will fail HMAC verification when injected into the other.
Note: EXFER-AUTH, EXFER-SESSION, EXFER-MAC-IR, and EXFER-MAC-RI use raw SHA-256(separator || data) — they do not include the length-prefix byte used by domain_hash. These four are exceptions to the general domain_hash pattern because they are session-scoped handshake/key-derivation operations, not consensus-critical content-addressed hashes.
Maximum message size: 8,388,608 bytes (8 MiB).
Message Types
| ID | Name | Payload |
|---|---|---|
| 0x01 | Hello | Handshake message (see below) |
| 0x02 | Ping | empty |
| 0x03 | Pong | empty |
| 0x10 | NewBlock | Serialized Block |
| 0x11 | GetBlocks | u32 LE count, then count × Hash256 |
| 0x12 | BlockResponse | Serialized Block |
| 0x13 | GetTip | empty |
| 0x14 | TipResponse | height(u64 LE) block_id(Hash256) cumulative_work([u8; 32]) |
| 0x15 | Inv | u32 LE count, then count × Hash256 |
| 0x16 | GetAddr | empty |
| 0x17 | Addr | u32 LE count, then count × AddrEntry |
| 0x18 | AuthAck | signature(64 bytes) |
| 0x20 | NewTx | Serialized Transaction |
| 0x21 | GetHeaders | start_height(u64 LE) max_count(u32 LE) |
| 0x22 | Headers | u32 LE count, then count × header(156 bytes) |
Hello message (268 bytes for protocol version 5):
version(u32 LE)
|| genesis_block_id(Hash256)
|| best_height(u64 LE)
|| best_block_id(Hash256)
|| cumulative_work([u8; 32])
|| nonce([u8; 32]) -- random, for liveness
|| echo([u8; 32]) -- echo peer's nonce
|| pubkey([u8; 32]) -- Ed25519 identity key
|| sig([u8; 64]) -- handshake transcript signature
AddrEntry (26 bytes):
ip(16 bytes, IPv4-mapped-v6) || port(u16 LE) || last_seen(u64 LE)
Handshake
Mutually authenticated via Ed25519. Both peers prove identity key possession. Small-order (weak) identity keys are rejected before signature verification.
Transcript hash:
transcript = SHA-256("EXFER-AUTH" || genesis_id || version_le4 || nonce_a || nonce_b || role || tip_a || tip_b)
where role = 0x00 for responder, 0x01 for initiator. Each tip_x is a 72-byte tip commitment: best_height(8 bytes LE) || best_block_id(32 bytes) || cumulative_work(32 bytes), taken from that peer's Hello message. The initiator's tip is tip_a and the responder's is tip_b. Including these binds each peer's claimed chain tip into the authentication transcript, preventing an attacker from replaying a handshake while substituting different chain-tip fields.
Protocol:
- Initiator sends Hello with nonce_a, pubkey_a, sig=[0; 64].
- Responder verifies version and genesis. Sends Hello with nonce_b, echo=nonce_a, pubkey_b, sig_b over transcript(role=0x00).
- Initiator verifies echo, verifies sig_b, sends AuthAck with sig_a over transcript(role=0x01).
- Responder verifies sig_a. Connection established.
Timeout: 5 seconds for handshake completion.
Rate Limits
| Resource | Limit | Window |
|---|---|---|
| Blocks per peer | 12 | per minute |
| Global blocks | 24 | per minute |
| Transactions per peer | 60 | per minute |
| Global transactions | 200 | per minute |
| Pings per peer | 10 | per minute |
| Requests per peer | 30 | per minute |
| Unsolicited messages per peer | 10 | per minute |
| Response bytes per peer | 16 MiB | per minute |
| Global response bytes | 128 MiB | per minute |
| Invalid blocks per peer | 3 | per minute |
| Invalid transactions per peer | 16 | per minute |
GetTip, GetBlocks, and GetHeaders share a single request_count counter, capped at MAX_REQUESTS_PER_MIN (30) per peer per minute.
During Initial Block Download (CatchingUp state), the per-peer and global response byte budgets are not enforced for GetBlocks and GetHeaders responses. This allows the serving peer to deliver blocks and headers at full speed during IBD.
During IBD, only the active IBD peer is exempt from per-peer block rate limits. All other peers are rate-limited normally even during CatchingUp state. This prevents sybil peers from flooding unsolicited blocks.
Assume-valid optimization. Blocks at or below the hardcoded checkpoint height (130,000) skip Argon2id PoW verification during IBD and replay. All other validation is performed: block linkage, transaction validation, Ed25519 signature verification, UTXO accounting, state root verification, fee calculation, coinbase rules, and timestamp checks. The trust assumption is the binary author, not the peer — the checkpoint hash guarantees the block at that height matches the canonical chain. If the block hash at the checkpoint height does not match, the chain is rejected. Use --no-assume-valid to disable this optimization and verify full PoW for every block. --verify-all also disables assume-valid.
Global transaction rate limit slots are refunded when a transaction fails pre-check validation, full validation, mempool insertion, or is discarded due to a tip change during validation. Only transactions that successfully enter the mempool consume the slot permanently.
After receiving a TipResponse, the node verifies the claimed tip by requesting the header at the claimed height. The header must match the claimed block_id and height, pass PoW verification, and have a difficulty target consistent with the local chain. Only after verification is the peer's tip marked as confirmed. Unconfirmed peers cannot trigger IBD.
Peer Discovery
On startup, the node resolves seed.exfer.org via DNS to discover healthy peers. The DNS seed returns A records pointing to nodes that are reachable and synced (tip within 100 blocks of the network's best height). A seed crawler probes all known nodes every 10 minutes and updates the DNS record with the current healthy set.
If DNS resolution fails (no internet, DNS blocked, seed.exfer.org not configured), the node falls back to three hardcoded seed IPs. The --peers flag overrides both DNS and hardcoded seeds.
Peer Limits
- Maximum outbound peers: 8
- Maximum inbound peers: 256
- Maximum inbound per IP: 1
- Ping interval: 60 seconds
- Pong deadline: 15 seconds
Address Book
- Maximum size: 1,024 entries
- Accepted per Addr message: 16
- Addr response window: 30 seconds after sending GetAddr; Addr messages outside this window are dropped as unsolicited
- Per /16 subnet cap: 32 entries per IPv4 /16 prefix (first two octets)
- Per-peer contribution cap: a single peer may contribute at most 25% of the address book
- Multi-source preference: outbound connection selection prefers addresses seen from at least 2 independent sources; single-source addresses are used only as fallback (e.g., bootstrap from a single seed)
Security Considerations
Post-handshake traffic is authenticated via per-frame HMAC-SHA256 (truncated to 16 bytes). The session MAC key is derived from an X25519 Diffie-Hellman shared secret (computed from the authenticated Ed25519 identity keys converted to Montgomery form) mixed with the handshake transcript. An active MITM cannot forge valid frame MACs without possessing a peer's identity private key, because the DH shared secret is only computable by the two endpoints. Traffic is not encrypted — message contents are visible to passive observers.
Consequence for consensus: all received blocks and transactions also undergo full consensus validation (PoW, difficulty, script evaluation, UTXO checks) before acceptance. The HMAC provides a fast first-pass rejection of tampered traffic; consensus validation provides defense-in-depth.
Consequence for penalties: consensus-violation strikes (invalid blocks, invalid transactions) trigger disconnection and IP-level rate limiting, but do not ban the peer's cryptographic identity. Because post-handshake frames are now HMAC-authenticated, a MITM cannot inject invalid traffic to frame a peer. Identity-level bans are reserved for handshake-level violations (wrong genesis, failed authentication signature) where the cryptographic handshake itself proves the peer is the source of the violation.
Consequence for address book: Addr messages are only accepted within a 30-second window after sending a GetAddr request. Subnet diversity, per-peer contribution caps, and multi-source preference for outbound connections limit the impact of address-book poisoning. An attacker controlling many peers across diverse /16 subnets can still bias discovery; fully closing this requires out-of-band seed diversity and is a known residual risk.
Consequence for bandwidth: aggregate outbound response bandwidth is capped at 128 MiB/min globally (in addition to 16 MiB/min per peer) to prevent many concurrent peers from driving egress to exhaustion.
Encrypted transport (confidentiality) is planned for a future protocol version.
18. Mempool
Capacity: 8,192 transactions.
Admission: Full UTXO validation and script evaluation. Rejects: coinbase transactions, duplicate TxIds, double-spends with existing mempool entries, fee density below lowest existing entry when at capacity.
Fee density: fee × 1,000,000 / tx_cost (scaled integer). Higher density = higher priority.
Eviction: When at capacity, the lowest fee-density entry is evicted to make room for a higher-density transaction.
Block selection: Transactions are selected in descending fee-density order until the block size limit is reached.
Revalidation: After a chain reorganization, the mempool is purged of transactions that are no longer valid against the new UTXO set. Two-phase: first a cheap UTXO existence check, then full re-validation of survivors.
19. Genesis Block
Fixed values (production network):
| Field | Value |
|---|---|
| Version | 1 |
| Height | 0 |
| prev_block_id | 0000000000000000000000000000000000000000000000000000000000000000 |
| Timestamp | 1,773,536,400 (2026-03-15T01:00:00Z) |
| Difficulty target | 2^248 = 0100000000000000000000000000000000000000000000000000000000000000 |
| Nonce | 259 |
| tx_root | 96d29616a481eac5ffa35f3f7cf2add76ac921e733f72174d45035b5996341d3 |
| state_root | aafc1988635522e0fdaa4249ccda596127ff689eba8cd1de01a9cdaaf671e9a8 |
Block ID:
d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051
Serialized header (156 bytes):
0100000000000000000000000000000000000000000000000000000000000000000000
0000000000000000009004b66900000000010000000000000000000000000000000000
0000000000000000000000000000030100000000000096d29616a481eac5ffa35f3f7c
f2add76ac921e733f72174d45035b5996341d3aafc1988635522e0fdaa4249ccda5961
27ff689eba8cd1de01a9cdaaf671e9a8
Coinbase transaction (349 bytes):
- Input: prev_tx_id = all zeros, output_index = 0
- Output: value = 10,000,000,000 (100 EXFER), script =
[0x00; 32](unspendable) - Witness:
b"NIST Beacon 2026-03-14T22:23:00Z 561AA26B...881F81 — Designed, audited, and built by autonomous machines. A human provided minimal necessary support.", no redeemer
0100010000000000000000000000000000000000000000000000000000000000000000
000000000000e40b540200000020000000000000000000000000000000000000000000
000000000000000000000000000006014e49535420426561636f6e20323032362d3033
2d31345432323a32333a30305a20353631414132364234323134454538463341414434
4635423842443342343439444633353033433039363131313130453530414332384633
3933373942364438393034463833333034374546463631303943393442423539414434
4242333333353933303743373746373143324643334241364431303733414538383146
383120e280942044657369676e65642c20617564697465642c20616e64206275696c74
206279206175746f6e6f6d6f7573206d616368696e65732e20412068756d616e207072
6f7669646564206d696e696d616c206e656365737361727920737570706f72742e00
| Identifier | Value |
|---|---|
| TxId | 5e63e65ea2a30d9c874f16eccb366022bfe692d6d933470cb50107df7c2b04c6 |
| WtxId | 96d29616a481eac5ffa35f3f7cf2add76ac921e733f72174d45035b5996341d3 |
20. Constants
Consensus
| Constant | Value | Description |
|---|---|---|
| VERSION | 1 | Block version |
| PROTOCOL_VERSION | 5 | Network protocol version |
| TARGET_BLOCK_TIME_SECS | 10 | Target seconds between blocks |
| RETARGET_WINDOW | 4,320 | Blocks between difficulty adjustments |
| MAX_RETARGET_FACTOR | 4 | Maximum difficulty change per retarget |
| COINBASE_MATURITY | 360 | Blocks before coinbase is spendable |
| MAX_BLOCK_SIZE | 4,194,304 | Maximum block size in bytes |
| MAX_TX_SIZE | 1,048,576 | Maximum transaction size in bytes |
| MTP_WINDOW | 11 | Ancestor count for median time past |
| MAX_TIMESTAMP_DRIFT | 120 | Maximum seconds ahead of wall clock (policy) |
| MAX_TIMESTAMP_GAP | 604,800 | Maximum seconds between parent and child timestamps |
| BLOCK_HEADER_SIZE | 156 | Header size in bytes |
Emission
| Constant | Value | Description |
|---|---|---|
| BASE_REWARD | 100,000,000 | Minimum reward (1 EXFER) |
| DECAY_COMPONENT | 9,900,000,000 | Decaying component (99 EXFER) |
| HALF_LIFE | 6,307,200 | Blocks per halving (~2 years) |
| EXFER_UNIT | 100,000,000 | Exfers per 1 EXFER |
Proof of Work
| Constant | Value | Description |
|---|---|---|
| ARGON2_MEMORY_KIB | 65,536 | Memory parameter (64 MiB) |
| ARGON2_ITERATIONS | 2 | Time parameter |
| ARGON2_PARALLELISM | 1 | Parallelism parameter |
| ARGON2_OUTPUT_LEN | 32 | Output length in bytes |
Fee and Cost
| Constant | Value | Description |
|---|---|---|
| UTXO_LOOKUP_COST | 100 | Cost per input UTXO lookup |
| UTXO_CREATE_COST | 100 | Cost per output UTXO creation |
| SMT_DELETE_COST | 500 | Cost per SMT leaf deletion |
| SMT_INSERT_COST | 500 | Cost per SMT leaf insertion |
| STANDARD_SPEND_COST | 20,000 | Reference cost for dust calculation |
| MIN_FEE_DIVISOR | 100 | Divisor for minimum fee |
| DUST_THRESHOLD | 200 | Minimum output value in exfers |
| PUBKEY_HASH_EVAL_COST | 5,000 | Base cost per pubkey hash input (+ data-proportional Ed25519 charge) |
| OUTPUT_TYPECHECK_COST | 1,000 | Cost per script-locked output |
Script Limits
| Constant | Value | Description |
|---|---|---|
| MAX_WITNESS_SIZE | 65,535 | Maximum witness bytes per input |
| MAX_DATUM_SIZE | 4,096 | Maximum datum bytes per output |
| MAX_REDEEMER_SIZE | 16,384 | Maximum redeemer bytes per input |
| MAX_SCRIPT_MEMORY | 16,777,216 | Maximum script evaluation memory |
| MAX_SCRIPT_STEPS | 4,000,000 | Maximum steps per input |
| MAX_TX_SCRIPT_BUDGET | 20,000,000 | Maximum steps per transaction |
| MAX_SCRIPT_NODES | 65,535 | Maximum nodes in a program |
| MAX_LIST_LENGTH | 65,536 | Maximum list length |
| MAX_VALUE_DEPTH | 128 | Maximum value nesting depth |
Network
| Constant | Value | Description |
|---|---|---|
| MAX_MESSAGE_SIZE | 8,388,608 | Maximum network message size |
| MAX_OUTBOUND_PEERS | 8 | Outbound peer limit |
| MAX_INBOUND_PEERS | 64 | Inbound peer limit |
| MAX_INBOUND_PER_IP | 4 | Per-IP inbound limit |
| PING_INTERVAL_SECS | 60 | Keepalive interval |
| PONG_DEADLINE_SECS | 15 | Pong timeout |
| HANDSHAKE_TIMEOUT_SECS | 5 | Handshake timeout |
| MAX_GETBLOCKS_ITEMS | 64 | Max hashes per GetBlocks |
| MEMPOOL_CAPACITY | 8,192 | Maximum mempool entries |
| MAX_ADDR_ITEMS | 64 | Max addresses per Addr message |
| MAX_ADDR_BOOK_SIZE | 1,024 | Maximum address book entries |
| MAX_ADDR_PER_MSG_ACCEPT | 16 | Max addresses accepted per message |
| MAX_GETADDR_PER_CONN | 2 | Max GetAddr requests per connection |
| MAX_UNSOLICITED_ADDR_PER_MIN | 3 | Unsolicited Addr messages per minute |
| ADDR_FLUSH_INTERVAL_SECS | 300 | Address book flush interval |
| MAX_GETBLOCKS_RESPONSE | 8 | Max blocks per GetBlocks response |
| MAX_INV_ITEMS | 64 | Max items per Inv message |
Rate Limits
| Constant | Value | Description |
|---|---|---|
| MAX_BLOCKS_PER_MIN | 12 | Blocks per peer per minute |
| MAX_GLOBAL_BLOCKS_PER_MIN | 24 | Global blocks per minute |
| MAX_TXS_PER_MIN | 60 | Transactions per peer per minute |
| MAX_GLOBAL_TXS_PER_MIN | 200 | Global transactions per minute |
| MAX_PINGS_PER_MIN | 10 | Pings per peer per minute |
| MAX_REQUESTS_PER_MIN | 30 | Requests per peer per minute |
| MAX_UNSOLICITED_PER_MIN | 10 | Unsolicited messages per peer per minute |
| MAX_RESPONSE_BYTES_PER_MIN | 16,777,216 | Response bytes per peer per minute (16 MiB) |
| MAX_GLOBAL_RESPONSE_BYTES_PER_MIN | 134,217,728 | Global response bytes per minute (128 MiB) |
Peer Penalties
| Constant | Value | Description |
|---|---|---|
| MAX_INVALID_BLOCKS_PER_PEER | 3 | Invalid blocks before disconnect |
| MAX_INVALID_TXS_PER_PEER | 16 | Invalid transactions before disconnect |
| MAX_CONTROL_MSGS_DURING_IBD | 50 | Max interleaved non-response messages during IBD |
Orphan and Fork Handling
| Constant | Value | Description |
|---|---|---|
| MAX_ORPHAN_BLOCKS | 16 | Maximum orphan blocks cached |
| MAX_ORPHAN_BLOCK_SIZE | 4,194,304 | Maximum orphan block size (= MAX_BLOCK_SIZE) |
| MAX_ORPHAN_CACHE_BYTES | 67,108,864 | Total orphan cache size (64 MiB) |
| MAX_FORK_BLOCK_SIZE | 4,194,304 | Maximum fork block size (= MAX_BLOCK_SIZE) |
| MAX_FORK_BLOCKS | 128 | Maximum fork chain length for reorg |
| MAX_RETAINED_FORK_HEADERS | 10,000 | Maximum retained non-canonical headers after fork eviction |
Transaction Limits
| Constant | Value | Description |
|---|---|---|
| MIN_TX_SIZE | 50 | Minimum serialized transaction size |
| MAX_SPENT_UTXOS_SIZE | 16,777,216 | Maximum serialized undo metadata per block (16 MiB) |
Part II: Operational Interface
21. Transaction Construction
21.1 Simple Payment
Given: A set of spendable UTXOs controlled by key pair (sk, pk), a recipient address (Hash256), an amount.
Procedure:
-
Compute sender address:
address = domain_hash("EXFER-ADDR", pk). -
Select inputs: Choose UTXOs whose scripts match the sender address. Skip coinbase UTXOs with age < 360 blocks. Accumulate until
total_input ≥ amount + estimated_fee. -
Construct outputs:
- Output 0:
value = amount, script = recipient_address.bytes (32 bytes), datum = None, datum_hash = None. - Output 1 (change):
value = total_input - amount - fee, script = sender_address.bytes (32 bytes), datum = None, datum_hash = None. Omit if change < 200 (dust threshold); fold sub-dust change into fee.
- Output 0:
-
Estimate fee: Construct a preliminary transaction to compute tx_cost:
script_eval_cost = input_count × (5,000 + ceil_div(sig_message_bytes, 64) × 8)witness_deser_cost = input_count × ceil_div(96, 64) = input_count × 2utxo_io_cost = input_count × 100 + output_count × 100smt_cost = input_count × 500 + output_count × 500tx_deser_cost = ceil_div(serialized_size, 64)min_fee = ceil_div(tx_cost, 100)
If total_input < amount + min_fee, select additional inputs and recompute.
-
Serialize tx_header and tx_body:
- tx_header:
input_count(u16 LE) || output_count(u16 LE). - tx_body: for each input
prev_tx_id(32) || output_index(u32 LE), then for each output the canonical serialization (Section 5.4).
- tx_header:
-
Compute signing message:
"EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body. -
Sign: Ed25519 sign the message with sk. Signature is 64 bytes.
-
Construct witnesses: For each input:
witness = pk(32) || signature(64), redeemer = None. -
Final serialization:
tx_header || tx_body || witnesses. -
Compute TxId:
domain_hash("EXFER-TX", tx_header || tx_body). -
Compute WtxId:
domain_hash("EXFER-WTXID", full_serialization).
21.2 Script-Locked Output
To lock funds to a script program:
- Construct the program as a DAG of combinators using the builder interface (Section 22).
- Serialize the program (Section 6.6):
node_count(u32 LE) || root_index(u32 LE) || nodes.... - Place the serialized bytes in the output's
scriptfield. - Set
datumanddatum_hashas needed by the script's logic.
The script must be well-typed with Bool output, pass all output validation checks (Section 12, rule 12), and have length ≠ 32 bytes.
21.3 Spending from Script-Locked Output
- Deserialize the script from the output being spent.
- Determine the script's expected input shape.
- Construct the witness data: serialize the values the script expects to read via Witness nodes.
- Construct the redeemer if the script expects one (or if the output has datum_hash requiring a datum).
- Place in the transaction witness:
witness = serialized_witness_values,redeemer = redeemer_bytes. - The validator will build the input value
(witness_bytes, (redeemer_opt, (datum_opt, ())))and evaluate the script.
21.4 Coinbase Spending (Maturity Constraints)
Coinbase outputs cannot be spent until 360 blocks after the block containing the coinbase. When constructing a transaction that spends coinbase outputs, ensure current_height - coinbase_height ≥ 360.
22. Script Patterns
All patterns below specify:
- The exact combinator DAG construction
- The script commitment (how to compute the hash for the output script field)
- The witness format for spending
- The cost (steps and cells from static analysis)
22.1 Signature Lock
Purpose: Lock funds to a single Ed25519 public key.
Output script: domain_hash("EXFER-ADDR", pubkey) — exactly 32 bytes. This is the pubkey hash lock, not a script program.
Witness to unlock: pubkey(32 bytes) || signature(64 bytes) = 96 bytes. Redeemer: absent.
Cost: 5,000 + ceil_div(sig_message_bytes, 64) × 8 steps per input.
22.2 Multisig (N-of-M)
2-of-2 Multisig
DAG construction:
and(sig_check(pk_a), sig_check(pk_b))
where sig_check(pk) is:
Comp(
Pair(
Comp(Jet(TxSigHash), Unit), -- get signing digest
Pair(Const(pk_bytes), Witness) -- (message, (pubkey, sig))
),
Jet(Ed25519Verify)
)
and(a, b) = Comp(Pair(a, b), Case(Comp(Drop(Iden), Case(InjL(Unit), InjR(Unit))), InjL(Unit)))
Witness: Two signatures read by two Witness nodes: [sig_a_serialized][sig_b_serialized].
Cost: ~10,010 + data-proportional Ed25519 cost per verify (2 × 5,000 + ceil_div(msg_len, 64) × 8 + overhead), ~6 cells.
1-of-2 Multisig
DAG construction:
Comp(Witness, Case(sig_check(pk_a), sig_check(pk_b)))
Witness: [selector: Left(Unit) or Right(Unit)][signature]. Selector is a serialized Value.
2-of-3 Multisig
DAG construction:
Comp(Witness, Case(
Case(and(check_a, check_b), and(check_a, check_c)),
and(check_b, check_c)
))
Witness: [selector: Left(Left(Unit))=A+B, Left(Right(Unit))=A+C, Right(Unit)=B+C][sig_1][sig_2].
22.3 Hash Lock
Purpose: Lock funds to a SHA-256 preimage.
DAG construction:
Comp(
Pair(Comp(Witness, Jet(Sha256)), Const(expected_hash)),
Jet(EqHash)
)
Witness: [preimage_bytes] (serialized as Value::Bytes).
Cost: ~1,520 steps (Sha256: 1,000 + EqHash: 500 + overhead), 3 cells.
22.4 Timelock
Purpose: Lock funds until a specific block height.
height_gt(h) construction:
Comp(
Pair(Jet(BlockHeight), Const(U64(h))),
Jet(Gt64)
)
Combined with signature: and(height_gt(h), sig_check(pk)).
Witness: [signature].
Cost: ~5,030 steps (one Ed25519Verify + Gt64 + overhead).
22.5 HTLC (Atomic Swap)
Purpose: Hash-locked time-locked contract for cross-chain atomic swaps.
Parameters: sender_key, receiver_key, hash_lock (Hash256), timeout_height (u64).
DAG construction:
Comp(Witness, Case(
and(hash_eq(hash_lock), sig_check(receiver_key)), -- hash path
and(height_gt(timeout_height), sig_check(sender_key)) -- timeout path
))
Witness (hash path): [Left(Unit)][preimage_bytes][receiver_signature].
Witness (timeout path): [Right(Unit)][sender_signature].
Cost: ~6,530 steps (max branch: Sha256 + EqHash + Ed25519Verify + overhead).
22.6 Escrow
Purpose: Three-path dispute resolution.
Parameters: party_a, party_b, arbiter, timeout_height.
DAG construction:
Comp(Witness, Case(
Case(
and(sig_check(party_a), sig_check(party_b)), -- mutual agreement
sig_check(arbiter) -- arbiter decision
),
and(height_gt(timeout_height), sig_check(party_a)) -- timeout refund
))
Witness (mutual): [Left(Left(Unit))][sig_a][sig_b].
Witness (arbiter): [Left(Right(Unit))][sig_arbiter].
Witness (timeout): [Right(Unit)][sig_a].
Cost: ~10,040 steps (max branch: 2× Ed25519Verify + overhead).
22.7 Vault
Purpose: Primary key with timelock + emergency recovery key without timelock.
Parameters: primary_key, recovery_key, locktime (block height).
DAG construction:
Comp(Witness, Case(
and(height_gt(locktime), sig_check(primary_key)), -- normal (after locktime)
sig_check(recovery_key) -- recovery (anytime)
))
Witness (normal): [Left(Unit)][primary_signature].
Witness (recovery): [Right(Unit)][recovery_signature].
Cost: ~5,040 steps (max branch: Ed25519Verify + Gt64 + overhead).
22.8 Delegation
Purpose: Owner can always spend; delegate can spend before expiry.
Parameters: owner_key, delegate_key, expiry_height.
DAG construction:
Comp(Witness, Case(
sig_check(owner_key), -- owner (unrestricted)
and(sig_check(delegate_key), height_lt(expiry_height)) -- delegate (before expiry)
))
where height_lt(h) = Comp(Pair(Jet(BlockHeight), Const(U64(h))), Jet(Lt64)).
Witness (owner): [Left(Unit)][owner_signature].
Witness (delegate): [Right(Unit)][delegate_signature].
Cost: ~5,040 steps (max branch: Ed25519Verify + Lt64 + overhead).
23. Covenants
Covenants in Exfer are script programs that use introspection jets to constrain the spending transaction's structure (inputs, outputs, values).
23.1 Multisig Covenant
See Section 22.2. The 2-of-2, 1-of-2, and 2-of-3 patterns are implemented as covenant templates.
23.2 HTLC Covenant
See Section 22.5. Supports atomic cross-chain swaps.
23.3 Escrow Covenant
See Section 22.6. Three-path (mutual, arbiter, timeout) dispute resolution.
23.4 Vault Covenant
See Section 22.7. Primary + recovery key pattern with timelock.
23.5 Delegation Covenant
See Section 22.8. Time-limited delegation of spending authority.
24. Payment Channels
Payment channels enable off-chain value transfer between two parties. The on-chain footprint is a single funding UTXO.
24.1 Open
Funding transaction: Create a single output locked by a 2-of-2 multisig script:
and(sig_check(party_a), sig_check(party_b))
Initial state: {sequence: 0, balance_a: funding_amount, balance_b: 0}. Total = balance_a + balance_b.
24.2 Update
Off-chain: parties sign a new state {sequence: N+1, balance_a: new_a, balance_b: new_b} where new_a + new_b = total. Each update exchanges pre-signed dispute transactions that allow challenging stale states.
The sequence number is monotonically increasing. A state is newer if its sequence number is higher.
24.3 Cooperative Close
Both parties agree to close. Construct a closing transaction spending the funding UTXO with outputs:
Output 0: value = balance_a, script = domain_hash("EXFER-ADDR", pk_a)
Output 1: value = balance_b, script = domain_hash("EXFER-ADDR", pk_b)
Omit any output with value < 200 (dust threshold).
Witness: 2-of-2 multisig witness [sig_a][sig_b].
24.4 Unilateral Close
One party publishes a commitment transaction with two outputs:
Output 0: value = counterparty_balance, script = P2PKH(counterparty) -- immediate
Output 1: value = publisher_balance, script = close_script -- timelocked
close_script:
Case(
and(sig_check(party_a), sig_check(party_b)), -- cooperative (dispute path)
and(height_gt(close_height + dispute_window), sig_check(publisher)) -- finalize
)
The publisher's funds are locked for a dispute window. After the window expires, the publisher can claim with their signature alone.
24.5 Dispute
If a counterparty publishes a stale commitment (old state), the other party challenges by spending the close_script output via the cooperative path using a pre-signed dispute transaction.
dispute_script:
Case(
and(and(sig_check(party_a), sig_check(party_b)), height_lt(close_height + dispute_window)),
and(sig_check(party_a), sig_check(party_b)) -- cooperative override
)
Dispute window: A parameter of the channel (in blocks). During this window, the counterparty can submit a dispute transaction proving a newer state. After the window, the publisher's close becomes final.
Witness (challenge): [Left(Unit)][sig_a][sig_b].
Witness (cooperative): [Right(Unit)][sig_a][sig_b].
25. Cost Computation
25.1 Fee Formula (Complete)
For a transaction with I inputs and O outputs:
tx_cost =
script_eval_cost // varies per input
+ output_typecheck_cost // 1,000 per script-locked output
+ sum_i(ceil_div(witness_bytes_i, 64)) // witness deserialization
+ sum_i(ceil_div(redeemer_bytes_i, 64)) // redeemer deserialization
+ sum_j(ceil_div(datum_bytes_j, 64)) // datum deserialization
+ ceil_div(total_tx_bytes, 64) // transaction deserialization
+ I × 100 + O × 100 // UTXO I/O
+ I × 500 + O × 500 // SMT operations
+ sum_k(ceil_div(script_bytes_k, 64) × 10) // script validation (script-locked inputs only)
min_fee = ceil_div(tx_cost, 100)
25.2 Script Cost by Pattern
| Pattern | Steps (per input) | Cells |
|---|---|---|
| Pubkey hash (32-byte script) | 5,000 + ceil_div(sig_msg_bytes, 64) × 8 | 0 |
| 2-of-2 multisig | ~10,010 | ~6 |
| 1-of-2 multisig | ~5,020 | ~4 |
| 2-of-3 multisig | ~10,020 | ~8 |
| Hash lock | ~1,520 | ~3 |
| Timelock + signature | ~5,030 | ~4 |
| HTLC | ~6,530 | ~5 |
| Escrow | ~10,040 | ~8 |
| Vault | ~5,040 | ~4 |
| Delegation | ~5,040 | ~4 |
25.3 Cost Optimization Constraints
- Script cost is determined before execution. No estimation needed.
- Minimum-case cost check at output creation prevents permanently locked funds.
- Per-input cap: 4,000,000 steps. Per-transaction cap: 20,000,000 steps.
- Memory limit: 16 MiB per evaluation.
- Fee density (fee × 1,000,000 / tx_cost) determines mempool priority.
26. Jet Reference
26.1 Cryptographic
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| Sha256 | 0x0001 | Bytes | Hash256 | 1,000 | 1 | SHA-256(input). Runtime: 500 + len/64 × 8. |
| Ed25519Verify | 0x0002 | Product(Bytes, Product(Bytes, Bytes)) | Bool | 5,000 | 1 | ZIP-215 verify(msg, pk, sig). False if pk≠32, sig≠64, or pk is small-order. Runtime: 5,000 + ceil(msg_len/64) × 8. |
| SchnorrVerify | 0x0003 | Product(Bytes, Product(Bytes, Bytes)) | Bool | 5,000 | 1 | Reserved. Always fails. |
| MerkleVerify | 0x0004 | Product(Hash256, Product(Hash256, Bytes)) | Bool | 32,000 | 1 | Verify Merkle proof. Runtime: 500 + proof_len/33 × 500. |
26.2 Arithmetic (64-bit)
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| Add64 | 0x0100 | Product(U64, U64) | U64 | 10 | 1 | a + b. Error on overflow. |
| Sub64 | 0x0101 | Product(U64, U64) | U64 | 10 | 1 | a - b. Error if a < b. |
| Mul64 | 0x0102 | Product(U64, U64) | U64 | 10 | 1 | a × b. Error on overflow. |
| Div64 | 0x0103 | Product(U64, U64) | U64 | 10 | 1 | a / b. Error if b = 0. |
| Mod64 | 0x0104 | Product(U64, U64) | U64 | 10 | 1 | a mod b. Error if b = 0. |
| Eq64 | 0x0105 | Product(U64, U64) | Bool | 10 | 1 | a = b. |
| Lt64 | 0x0106 | Product(U64, U64) | Bool | 10 | 1 | a < b. |
| Gt64 | 0x0107 | Product(U64, U64) | Bool | 10 | 1 | a > b. |
26.3 Arithmetic (256-bit)
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| Add256 | 0x0200 | Product(U256, U256) | U256 | 50 | 1 | a + b. Error on overflow. |
| Sub256 | 0x0201 | Product(U256, U256) | U256 | 50 | 1 | a - b. Error if a < b. |
| Mul256 | 0x0202 | Product(U256, U256) | U256 | 50 | 1 | a × b. Error on overflow. |
| Div256 | 0x0203 | Product(U256, U256) | U256 | 50 | 1 | a / b. Error if b = 0. |
| Mod256 | 0x0204 | Product(U256, U256) | U256 | 50 | 1 | a mod b. Error if b = 0. |
| Eq256 | 0x0205 | Product(U256, U256) | Bool | 50 | 1 | a = b. |
| Lt256 | 0x0206 | Product(U256, U256) | Bool | 50 | 1 | a < b (big-endian). |
| Gt256 | 0x0207 | Product(U256, U256) | Bool | 50 | 1 | a > b (big-endian). |
26.4 Byte Operations
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| Cat | 0x0300 | Product(Bytes, Bytes) | Bytes | 100 | 1 | Concatenate. Runtime: 10 + total_len/8. |
| Slice | 0x0301 | Product(Bytes, Product(U64, U64)) | Bytes | 100 | 1 | source[start..start+len]. Clamps to bounds. Runtime: 10 + src_len/8. |
| Len | 0x0302 | Bytes | U64 | 10 | 0 | Byte count. |
| EqBytes | 0x0303 | Product(Bytes, Bytes) | Bool | 500 | 0 | Byte equality. Runtime: 10 + max(len_a, len_b)/8. |
| EqHash | 0x0304 | Product(Hash256, Hash256) | Bool | 500 | 0 | Hash equality. Runtime: 14. |
26.5 Introspection
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| TxInputs | 0x0400 | Unit | List(...) | 1,000 | 0 | All inputs. Runtime: 10 + n×10. |
| TxOutputs | 0x0401 | Unit | List(...) | 1,000 | 0 | All outputs. Runtime: 10 + n×10. |
| TxValue | 0x0402 | U64 | U64 | 10 | 0 | Input value by index. Error if OOB. |
| TxScriptHash | 0x0403 | U64 | Hash256 | 10 | 0 | Input script hash by index. Error if OOB. |
| TxInputCount | 0x0404 | Unit | U64 | 5 | 0 | Number of inputs. |
| TxOutputCount | 0x0405 | Unit | U64 | 5 | 0 | Number of outputs. |
| SelfIndex | 0x0406 | Unit | U64 | 5 | 0 | Current input index. |
| BlockHeight | 0x0407 | Unit | U64 | 5 | 0 | Current block height. |
| TxSigHash | 0x0408 | Unit | Bytes | 5 | 0 | Signing digest. Runtime: 5 + len/64. |
26.6 List Operations
| Jet | ID | Input | Output | Steps | Cells | Behavior |
|---|---|---|---|---|---|---|
| ListLen | 0x0500 | List(A) | U64 | 10 | 0 | Element count. |
| ListAt | 0x0501 | Product(List(A), U64) | Option(A) | 10 | 1 | Element at index. None if OOB. |
| ListSum | 0x0502 | List(U64) | U64 | 1,000 | 0 | Sum. 0 if empty. Error on overflow. Runtime: 10 + n. |
| ListAll | 0x0503 | List(Bool) | Bool | 1,000 | 0 | All true. True if empty. Runtime: 10 + n. |
| ListAny | 0x0504 | List(Bool) | Bool | 1,000 | 0 | Any true. False if empty. Runtime: 10 + n. |
| ListFind | 0x0505 | List(Bool) | Option(U64) | 1,000 | 0 | Index of first true. None if not found. Runtime: 10 + n. |
27. Datum and Redeemer Interface
Attaching Inline Datums
Set the output's datum field to the datum bytes. Serialization:
... || has_datum=0x01 || VarBytes(datum_bytes) || ...
Attaching Hash-Committed Datums
Compute datum_hash = SHA-256(datum_bytes). Set the output's datum_hash field. Serialization:
... || has_datum=0x00 || has_datum_hash=0x01 || datum_hash(32 bytes)
Providing Datums at Spend Time
When spending an output with datum_hash but no inline datum, the spender must provide the datum in the witness redeemer field. The validator computes SHA-256(redeemer) and verifies it equals datum_hash.
Resolution Logic
if output.datum is present:
if output.datum_hash is also present:
verify SHA-256(output.datum) = output.datum_hash
datum = output.datum
else if output.datum_hash is present:
require witness.redeemer is present
require len(witness.redeemer) ≤ 4,096
verify SHA-256(witness.redeemer) = output.datum_hash
datum = witness.redeemer
else:
datum = None
The resolved datum is passed to the script as the third element of the input tuple.
28. UTXO Selection
Constraint satisfaction problem:
Given:
- Available UTXOs:
{(outpoint_i, value_i, is_coinbase_i, height_i)} - Target amount:
A - Current height:
H - Recipient script
Select a subset S such that:
- For all UTXO in S: if
is_coinbase, thenH - height ≥ 360. sum(values in S) ≥ A + min_fee(tx with |S| inputs, estimated outputs).- If
sum(values in S) - A - fee ≥ 200: create a change output (additional output in fee calculation). - If
sum(values in S) - A - fee < 200and > 0: fold residual into fee. - Every output value ≥ 200 (dust threshold).
- Serialized transaction size ≤ 1,048,576 bytes.
The fee depends on the transaction structure, which depends on the fee. Iterate: estimate fee, construct transaction, recompute fee, adjust if needed.
29. Key Management
Key generation: Generate an Ed25519 key pair. The signing key is 32 bytes (seed). The verifying key (public key) is 32 bytes.
Address derivation:
address = domain_hash("EXFER-ADDR", pubkey)
Returns a 32-byte Hash256. This is used as the script field for pubkey hash locked outputs.
Signature construction:
- Compute signing message:
"EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body. - Ed25519 sign the message with the signing key.
- Signature is 64 bytes.
Witness format: pubkey(32 bytes) || signature(64 bytes) = 96 bytes total.
Wallet encryption (reference implementation):
- Algorithm: Argon2id key derivation + AES-256-GCM encryption.
- Argon2id parameters: m=262,144 KiB, t=3, p=1.
- File format:
EXFK(4 bytes) || version(1) || salt(16) || nonce(12) || ciphertext(48)= 81 bytes. - Ciphertext contains the 32-byte signing key + 16-byte GCM authentication tag.
30. Network Submission
To submit a transaction to the network:
- Serialize the transaction (Section 5.1).
- Construct a NewTx message:
msg_type=0x20 || payload_length(u32 LE) || serialized_transaction. - Send to one or more connected peers. On established (post-handshake) connections, the message is wrapped in the authenticated frame format (Section 17):
counter(u64 LE) || msg_type(0x20) || payload_length(u32 LE) || serialized_transaction || hmac(16 bytes).
Expected behavior:
- If valid: peers relay the transaction to their peers and add it to their mempools.
- If invalid: peers silently drop the transaction. No error response is sent.
Error conditions:
- Transaction fails validation (any rule in Section 12).
- Transaction is a duplicate (already in mempool or confirmed).
- Transaction spends outputs already spent by a mempool transaction.
- Mempool is full and transaction's fee density is too low.
Appendix A: Test Vectors
All hex values below are computed from the reference implementation. Implementations must produce identical outputs for correctness.
A.1 Domain-Separated Hashes
domain_hash(separator, data) = SHA-256(len(separator) || separator || data).
Each row: domain_hash(separator, [0x00]).
| Separator | Result |
|---|---|
| EXFER-SIG | c32adc238ff3a66535a9180383711c5b84528fd535ff7e1934372f0a30efbbe6 |
| EXFER-TX | 7cf80b71d07b2c0f8a0645a57c5d98c511fbe267503224a93c38968d4411ca03 |
| EXFER-TXROOT | f53925c44981d10789596e61503f829fd717420a4e77f7a1c58dc6dcbc09d2a7 |
| EXFER-STATE | 1cb15b3427e2260373722ae99aff98358e2c03bd2760b9493cf6d3fc30a54d7d |
| EXFER-ADDR | 48e0d24cf73d51393cd3102222cdf69d02394261d1c9ce6e4d599214c3a9d228 |
| EXFER-AGENT | e02ef4eaf362f7cb4bbf0cb1f7bdc3410db8e7d3cd3397a8fa0646933f14de12 |
| EXFER-SCRIPT | 0136103e88d1ccc06c639d3a2e99002941e61e691b775259268e0280c4bcca23 |
| EXFER-POW-P | 0e4b0b5d4652e52c57e77e90bac1fc8fb83e7feb932f5a1b1f398deda962c749 |
| EXFER-POW-S | 65974595a51e46671a67197774f0b2b2b11164b51b696cf9efd2d20dba16040c |
| EXFER-WTXID | 8a9366392187901fdeccaae02cacf9a63dbf7a60d112e4b13ca13230e3743613 |
| EXFER-AUTH | 89edf3fdddaa59667639e471be147918056ae1a00b8502a60fd9c8f6871e72c2 |
| EXFER-SESSION | 5e4c61773f051a55a8138785a3ac5987b233b4765c376229c8f4a717b4da36ca |
| EXFER-MERKLE | ce2039c9d6b0f0ea92b42b8a3a9c3b7b7d4e3bd8f1fedeb3d3e52dad820116ad |
| EXFER-MAC-IR | cafb477b835296bc0bb56ef0ebf24f513e5a2df9dd657c813cafa41732aca082 |
| EXFER-MAC-RI | 214b628b02f189b484f32ee032c5c98a1b309e57673165041d47417e8be0eb8b |
Note: The EXFER-AUTH, EXFER-SESSION, EXFER-MAC-IR, and EXFER-MAC-RI rows above use raw SHA-256(separator || [0x00]), not domain_hash. They omit the length-prefix byte. The remaining separators in this table use the standard domain_hash construction with the length prefix.
A.2 Genesis Block
Serialized header (156 bytes):
0100000000000000000000000000000000000000000000000000000000000000000000
0000000000000000009004b66900000000010000000000000000000000000000000000
0000000000000000000000000000030100000000000096d29616a481eac5ffa35f3f7c
f2add76ac921e733f72174d45035b5996341d3aafc1988635522e0fdaa4249ccda5961
27ff689eba8cd1de01a9cdaaf671e9a8
block_id = SHA-256(header_bytes):
d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051
Coinbase transaction (349 bytes):
0100010000000000000000000000000000000000000000000000000000000000000000
000000000000e40b540200000020000000000000000000000000000000000000000000
000000000000000000000000000006014e49535420426561636f6e20323032362d3033
2d31345432323a32333a30305a20353631414132364234323134454538463341414434
4635423842443342343439444633353033433039363131313130453530414332384633
3933373942364438393034463833333034374546463631303943393442423539414434
4242333333353933303743373746373143324643334241364431303733414538383146
383120e280942044657369676e65642c20617564697465642c20616e64206275696c74
206279206175746f6e6f6d6f7573206d616368696e65732e20412068756d616e207072
6f7669646564206d696e696d616c206e656365737361727920737570706f72742e00
- TxId =
5e63e65ea2a30d9c874f16eccb366022bfe692d6d933470cb50107df7c2b04c6 - WtxId =
96d29616a481eac5ffa35f3f7cf2add76ac921e733f72174d45035b5996341d3 - tx_root = WtxId (single-transaction block)
- state_root =
aafc1988635522e0fdaa4249ccda596127ff689eba8cd1de01a9cdaaf671e9a8
A.3 Argon2id PoW (Genesis Block)
Using the genesis header bytes from A.2:
pw = domain_hash("EXFER-POW-P", header)
= 3e5bd47e30df181035ecb70f9ad0ba16c48fd8f7e96f5e5b82cd8ff8d843e44c
salt = domain_hash("EXFER-POW-S", header)
= d28d116248882eb65ad03605dce15f9920e21978e221ca18b4f43886d0521d73
pow = Argon2id(pw, salt, m=65536, t=2, p=1, len=32)
= 00c0782180c6270ff26b8d19deb26f65dcf144753d0d2dffc7b604552789a21c
target = 0100000000000000000000000000000000000000000000000000000000000000
pow < target = true (valid PoW)
A.4 Serialized Transaction
1-input, 1-output transaction. Input: prev_tx_id = [0xAA; 32], output_index = 0. Output: value = 1,000,000,000 (10 EXFER), script = [0xBB; 32], no datum, no datum_hash. Witness: [0xCC; 96], no redeemer.
Serialized (183 bytes):
01000100aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
0000000000ca9a3b000000002000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbb00006000cccccccccccccccccccccccccccccccccccccccccc
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
cccccccccc00
- TxId =
a2867b18dc2f273e0befe9946673a83586412eee009ff49196a484d7c7e925c0 - WtxId =
37b6a8127c4a9b802c17482c208d4efe289c318f5bebcbf8c8b0809d5969d636
A.5 Merkle Root Computation
Using synthetic WtxId hashes: h_i = domain_hash("EXFER-WTXID", [i]) for i = 1..4.
h_1 = 9beb7854b427d2f24abe3be7a8fa2af3f4a3751a17bd7647b41ac17c2dcdc80f
h_2 = e823db473776b2bcd056cc8491258f8cac56e86c0346995208e4974c9e4ab0b6
h_3 = cb00e7c119309a7fec156bae7a6da7d4f9e353644b64d170643e106c6c912d04
h_4 = 9475c87b5ff677fd4349b6cece2cd8e9a903a8847ab32ce00afc217f63b57219
| Transaction count | Merkle root |
|---|---|
| 1 | 9beb7854b427d2f24abe3be7a8fa2af3f4a3751a17bd7647b41ac17c2dcdc80f |
| 2 | b5f5507bd7ff50450c1c818c90a0abad59a5c805926fa94b3ee8a1e6a346873a |
| 3 | 3d6a38ee7cb8ac6f1f0d75b3a473ebf9212ee13ebab2307e432c0526d64ab5f7 |
| 4 | 8ff248a73262d32dc0ed7322fd61bbab0fc8bb95f9e84b22bbfe87f963e212a2 |
A.6 Script Serialization and Merkle Commitment
Program: Single Iden node (identity function).
Serialized (9 bytes): 010000000000000000
- node_count = 1 (
01000000) - root_index = 0 (
00000000) - node[0] = tag 0x00 (Iden) (
00)
Merkle hash: domain_hash("EXFER-SCRIPT", [0x00]) = 0136103e88d1ccc06c639d3a2e99002941e61e691b775259268e0280c4bcca23
A.7 State Root (Sparse Merkle Tree)
Empty SMT root (depth 256): b178c245c947ea7e21ecede07728941a6ab1b706143c06873baff8ebd6de6308
Canonical output used for all UTXOs below: value = 100,000,000 (1 EXFER), script = [0x00; 32], no datum, no datum_hash. Serialized (44 bytes): 00e1f50500000000200000000000000000000000000000000000000000000000000000000000000000000000.
UTXO 1: tx_id = [0xAA; 32], output_index = 0, height = 0, coinbase = true.
leaf_key = 47193d83874af8362d4a875497f76093ac9599585bc653f6c1e996f46c669166
leaf_value = 64dd17646ff5b528a303394bfcf769ef28e0e2f11679e002e625d4f51e994f3b
state_root = a9a2fcf06b7ea20b487714a71d17888ca7a2a86c4364a5287a43535662273a48
UTXO 2 (cumulative): tx_id = [0xBB; 32], output_index = 0, height = 1, coinbase = false.
leaf_key = 9314ba260f6c674f842c967f428359031986e567f1df590a644974b7fc8cb6a7
leaf_value = f23c28768873e140f0516f0bd87918d94f9cd0f7acd8a1cc68d53dae4663fe24
state_root = c891bd8ad650ef06f44fd2689ab0466f5b3b9355f5dc37a1bc43e30c9a1facce
UTXO 3 (cumulative): tx_id = [0xCC; 32], output_index = 0, height = 2, coinbase = false.
leaf_key = ecbf1c33256b2f0baadfe2b02c7d181e551a325bdf7952324a70bd644ed73669
leaf_value = fba36f58a7f6184e06de985aa9ab7c5baa02ea30dd000657cef9257ae9e05060
state_root = 9fc927b51f0e58a529d90404a584b3c77448e55ab9fe97e4b626a2e85eb53ae4
A.8 Block Reward
| Height | Reward (exfers) |
|---|---|
| 0 | 10,000,000,000 |
| 1 | 9,999,998,912 |
| 100 | 9,999,891,228 |
| 1,000 | 9,998,912,280 |
| 4,320 | 9,995,301,790 |
| 10,000 | 9,989,127,892 |
| 43,200 | 9,953,117,900 |
| 100,000 | 9,891,814,300 |
| 6,307,200 | 5,050,000,000 |
| 12,614,400 | 2,575,000,000 |
| 18,921,600 | 1,337,500,000 |
| 63,072,000 | 109,667,968 |
| 630,720,000 | 100,000,000 |
A.9 Emission LUT Endpoints
LUT[0] = 18,446,744,073,709,551,616 (2^64)
LUT[4096] = 9,223,758,693,993,446,757
K = 18,443,622,869,203,936,790
A.10 Difficulty Retarget
At genesis target 2^248, expected_time = 43,190 seconds:
Double speed (actual_time = 2 × expected_time = 86,380):
new_target = 2^248 × 86380 / 43190 = 2^249
= 0200000000000000000000000000000000000000000000000000000000000000
Half speed (actual_time = expected_time / 2 = 21,595):
new_target = 2^248 × 21595 / 43190 = 2^247
= 0080000000000000000000000000000000000000000000000000000000000000
A.11 Cost Calculation
1-input, 1-output transaction, pubkey hash lock, 96-byte witness:
sig_message_bytes = len("EXFER-SIG" || genesis_block_id || tx_header || tx_body)
ed25519_data_cost = ceil_div(sig_message_bytes, 64) × 8
script_eval_cost = 5,000 + ed25519_data_cost (per pubkey hash input)
output_typecheck_cost = 0 (32-byte script)
witness_deser_cost = 2 (ceil_div(96, 64))
datum_deser_cost = 0
tx_deser_cost = ceil_div(total_serialized_bytes, 64)
utxo_io_cost = 200 (1 × 100 + 1 × 100)
smt_cost = 1,000 (1 × 500 + 1 × 500)
tx_cost = script_eval_cost + 0 + 2 + 0 + tx_deser_cost + 200 + 1,000
min_fee = ceil_div(tx_cost, 100)
For the transaction in A.4 (183 bytes): tx_deser_cost = ceil_div(183, 64) = 3. The signing message is 9 + 32 + tx_header + tx_body bytes; ed25519_data_cost = ceil_div(sig_message_bytes, 64) × 8. tx_cost = 5,000 + ed25519_data_cost + 0 + 2 + 0 + 3 + 200 + 1,000.
Appendix B: Domain Separator Catalog
| Separator | Byte Encoding | Usage |
|---|---|---|
| EXFER-SIG | b"EXFER-SIG" (9 bytes) | Transaction signing message prefix; followed by genesis_block_id(32) to bind signatures to this chain |
| EXFER-TX | b"EXFER-TX" (8 bytes) | TxId computation |
| EXFER-TXROOT | b"EXFER-TXROOT" (12 bytes) | Merkle tree internal nodes |
| EXFER-STATE | b"EXFER-STATE" (11 bytes) | SMT leaf key derivation |
| EXFER-ADDR | b"EXFER-ADDR" (10 bytes) | Address (pubkey hash) derivation |
| EXFER-AGENT | b"EXFER-AGENT" (11 bytes) | Agent identity derivation |
| EXFER-SCRIPT | b"EXFER-SCRIPT" (12 bytes) | Script Merkle commitment (program serialization) |
| EXFER-MERKLE | b"EXFER-MERKLE" (12 bytes) | MerkleVerify jet internal node hashing |
| EXFER-POW-P | b"EXFER-POW-P" (11 bytes) | PoW password derivation |
| EXFER-POW-S | b"EXFER-POW-S" (11 bytes) | PoW salt derivation |
| EXFER-WTXID | b"EXFER-WTXID" (11 bytes) | Witness-committed transaction hash |
| EXFER-AUTH | b"EXFER-AUTH" (10 bytes) | Peer authentication transcript |
| EXFER-SESSION | b"EXFER-SESSION" (13 bytes) | Session key derivation from transcript and DH secret |
| EXFER-MAC-IR | b"EXFER-MAC-IR" (12 bytes) | Directional MAC key derivation: initiator → responder |
| EXFER-MAC-RI | b"EXFER-MAC-RI" (12 bytes) | Directional MAC key derivation: responder → initiator |
All domain-separated hashes use the prefix-free construction: SHA-256(len_byte || separator || data), except EXFER-AUTH, EXFER-SESSION, EXFER-MAC-IR, and EXFER-MAC-RI, which use raw SHA-256(separator || data) without the length-prefix byte. These four are session-scoped handshake and key-derivation operations, not content-addressed consensus hashes.
Appendix C: Worked Examples
C.1 Constructing a Simple Payment
Scenario: Alice (pk_a) sends 500 EXFER to Bob (pk_b), with one UTXO worth 1000 EXFER.
Step 1: Compute addresses.
addr_a = domain_hash("EXFER-ADDR", pk_a)
addr_b = domain_hash("EXFER-ADDR", pk_b)
Step 2: Build transaction body.
tx_header:
[0x01, 0x00] -- input_count = 1 (u16 LE)
[0x02, 0x00] -- output_count = 2 (u16 LE)
Input 0:
prev_tx_id (32 bytes)
output_index (u32 LE)
Output 0 (to Bob, 500 EXFER = 50,000,000,000 exfers):
value: [0x00, 0x74, 0x3B, 0xA4, 0x0B, 0x00, 0x00, 0x00] -- 50,000,000,000 u64 LE
VarBytes(addr_b): [0x20, 0x00] || addr_b (32 bytes)
has_datum: [0x00]
has_datum_hash: [0x00]
Output 1 (change to Alice):
value: (total_input - 50,000,000,000 - fee) as u64 LE
VarBytes(addr_a): [0x20, 0x00] || addr_a (32 bytes)
has_datum: [0x00]
has_datum_hash: [0x00]
Step 3: Compute signing message.
sig_msg = "EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body
Step 4: Sign.
signature = Ed25519_sign(sk_a, sig_msg) -- 64 bytes
Step 5: Build witness.
VarBytes([pk_a (32) || signature (64)]): [0x60, 0x00] || pk_a || signature
has_redeemer: [0x00]
Step 6: Compute identifiers.
TxId = domain_hash("EXFER-TX", tx_header || tx_body)
WtxId = domain_hash("EXFER-WTXID", tx_header || tx_body || witnesses)
Step 7: Verify fee.
fee = total_input - 50,000,000,000 - change_value
sig_msg_len = len("EXFER-SIG" || genesis_block_id || tx_header || tx_body)
ed25519_data_cost = ceil_div(sig_msg_len, 64) × 8
tx_cost = (5,000 + ed25519_data_cost) + 0 + 2 + 0 + ceil_div(total_bytes, 64) + 200 + 1,000
min_fee = ceil_div(tx_cost, 100)
assert fee ≥ min_fee
C.2 Constructing an HTLC
Scenario: Alice locks 10 EXFER. Bob can claim with preimage of H. Alice reclaims after block 100,000.
Step 1: Build the HTLC script program.
Using the builder:
sig_check_bob = Comp(Pair(Comp(Jet(TxSigHash), Unit), Pair(Const(pk_bob), Witness)), Jet(Ed25519Verify))hash_check = Comp(Pair(Comp(Witness, Jet(Sha256)), Const(H)), Jet(EqHash))hash_path = and(hash_check, sig_check_bob)sig_check_alice = [similar to sig_check_bob with pk_alice]timeout_check = Comp(Pair(Jet(BlockHeight), Const(U64(100000))), Jet(Gt64))timeout_path = and(timeout_check, sig_check_alice)root = Comp(Witness, Case(hash_path, timeout_path))
Step 2: Serialize the program. Apply Section 6.6 serialization.
Step 3: Create the output.
value = 1,000,000,000 (10 EXFER)
script = serialized_program_bytes
datum = None
datum_hash = None
Step 4: Bob claims (hash path).
Witness data (serialized Values in order of Witness node evaluation):
- Selector:
Left(Unit)→[0x01, 0x00] - Preimage:
Bytes(preimage)→[0x05, len_le4, preimage_bytes] - Bob's signature:
Bytes(sig)→[0x05, 0x40, 0x00, 0x00, 0x00, sig_64_bytes]
Redeemer: None.
Step 5: Verify cost.
The static cost of the HTLC script determines the minimum fee for the spending transaction.
CLI reference
exfer exposes its functionality through five top-level subcommands.
This page lists each one and its purpose; for end-to-end recipes see
Getting Started and
Use Exfer.
exfer <command>
Commands:
init Initialize a new Exfer node (wallet + node + optional mining)
node Run a full node
mine Run the miner (full node + block production)
wallet Wallet operations
script Script operations (HTLC, multisig, vault, escrow, delegation)
help Print help for a command
Every command supports --help.
exfer init
One-shot bootstrap. Creates a wallet, starts a node, optionally enables mining.
exfer init # interactive passphrase
EXFER_PASS=... exfer init --passphrase-env EXFER_PASS --json
exfer init --mine # also start mining
exfer init --no-passphrase # unencrypted wallet (testing only)
exfer node
Run a full node without mining.
exfer node \
--datadir ~/.exfer \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
| Flag | Purpose |
|---|---|
--datadir | Where chain + state live (default data) |
--bind | P2P bind address (default 0.0.0.0:9333) |
--peers | Manually specify peers (repeatable). Defaults to DNS seed |
--rpc-bind | Optional JSON-RPC endpoint |
--repair-perms | Auto-fix node_identity.key permissions on startup |
--verify-all | Re-verify PoW for all replayed blocks (slow) |
--no-assume-valid | Disable the pre-checkpoint PoW skip during IBD |
exfer mine
Same as exfer node plus block production. Use --miner-pubkey so the
private key never lives on the mining host.
exfer mine \
--datadir ~/.exfer \
--miner-pubkey <YOUR_PUBKEY_HEX> \
--rpc-bind 127.0.0.1:9334 \
--repair-perms
See Solo mine.
exfer wallet
Subcommands:
| Subcommand | Purpose |
|---|---|
generate | Create a new keypair |
info | Show address + pubkey of a wallet file |
balance | Sum of spendable UTXOs for the wallet's address |
send | Build, sign, and broadcast a payment |
See Getting Started for full examples.
exfer script
Subcommands grouped by script pattern:
HTLC
htlc-lockhtlc-claimhtlc-reclaim
See HTLC.
Multisig
multisig2of2-lock,multisig2of2-spendmultisig1of2-lock,multisig1of2-spendmultisig2of3-lock,multisig2of3-spend
See Multisig.
Vault
vault-lockvault-spendvault-recover
See Vault.
Escrow
escrow-lockescrow-releaseescrow-arbitrateescrow-reclaim
See Escrow.
Delegation
delegation-lockdelegation-owner-spenddelegation-delegate-spend
See Delegation.
Global conventions
--jsonis supported on every subcommand that produces output. Always prefer it for scripted use.--rpc <URL>routes UTXO lookups and submission through a remote RPC node, so you can spend without running a full local node.--datadir <PATH>for local-database operations.--wallet <PATH>for any command that signs. The CLI prompts for the passphrase unless the wallet was created with--no-encrypt.
For the authoritative flag list of any subcommand, run:
exfer <command> --help
exfer <command> <subcommand> --help
FAQ
What is Exfer?
A proof-of-work cryptocurrency built from scratch in Rust. It uses Argon2id memory-hard mining, an extended-UTXO model (Bitcoin-style, but with a richer scripting language), and ships native HTLC / multisig / vault / escrow / delegation script patterns.
See Introduction for the 60-second pitch.
Is it a fork of Bitcoin / Litecoin / something?
No. It's a from-scratch implementation in Rust. The data model is extended-UTXO and several conventions (10 s block time, address-as-hash) will look familiar to Bitcoin developers, but the wire format, script language, hashing scheme, and PoW algorithm are different. The methods exposed by the JSON-RPC interface are also not Bitcoin-compatible.
What problem is it solving?
Per the project's own framing, Exfer aims to be infrastructure for autonomous machine-to-machine commerce: a chain where an agent can build a transaction, compute its exact cost statically, and know with certainty that it will validate. No gas estimation, no mempool priority auction, no Turing-complete VM to reason about.
That's an opinionated take. Whether it succeeds is an empirical question about adoption, not a technical claim this site can settle.
Who's behind it?
Open-source contributors at github.com/ahuman-exfer. No formal foundation. License is MIT.
Has it been audited?
No formal third-party security audit at this writing. The protocol is relatively young. Treat the software accordingly:
- For exploration, learning, and small-scale use, it's fine.
- For substantial value, take the usual precautions: back up your wallet, use vaults or multisig for cold storage, run your own node.
Why Argon2id mining?
Memory-hard PoW makes ASICs much less of a runaway advantage than SHA-256-based mining. The intent is to keep mining accessible to commodity CPU hardware. See How exfer mining works for the parameters and the trade-offs.
What's the supply schedule?
- Initial block reward: 100 EXFER
- Half-life: 6 307 200 blocks (~2 years)
- Minimum reward: 1 EXFER
- 10 s target block time, retargets every 4 320 blocks
The schedule asymptotes at 1 EXFER per block. See How exfer mining works and Protocol specification §11 for the formula.
How is EXFER different from satoshis?
EXFER is the user-facing unit. exfers (lower case) is the base unit
in the protocol. 1 EXFER = 100 000 000 exfers, same ratio as
Bitcoin's sats. The CLI accepts both forms — --amount "10 EXFER" and
--amount 1000000000 mean the same thing.
Can I run a node on a Raspberry Pi?
Yes, in principle. Sync from cold will be slow because Argon2id verify is memory-bound and a Pi's memory bandwidth is modest, but it works. For mining, expect a small hash rate. See Sync the chain and How mining works.
Why are there only a few public RPC nodes?
Because the chain is young. The list on the Live Nodes page is community-maintained and best-effort. For production integrations run your own.
How do I add a node to the public list?
Open an issue or PR on the
exfer-docs site source
(once that repo exists). The list is human-curated, not registered
on-chain.
How do I contact someone?
- GitHub issues for bugs: https://github.com/ahuman-exfer/exfer/issues
- GitHub Discussions for questions: https://github.com/ahuman-exfer/exfer/discussions
- See Contributing for the broader picture.
Is there a token sale / pre-mine / VC backing?
Not as of this writing. Coins enter circulation via mining only.
Is Exfer privacy-preserving?
It's a transparent UTXO chain — every transaction value, sender script, and recipient script is public. No built-in mixer, no zero-knowledge shielded pool. If privacy matters for your use case, plan for it at the application layer (fresh addresses per transaction, CoinJoin-style batching, etc.) and treat the public ledger as adversary-visible.
Glossary
Short definitions for terms used throughout this site.
Address
A 32-byte hash (64 hex chars) of a locking script. Where you receive
payments. Same string is accepted as the script_hex parameter of
get_script_utxos. Not the same as the
public key.
Argon2id
The memory-hard hash function Exfer uses for proof of work. Parameters:
m=64 MiB, t=2, p=1. Designed to resist ASIC speedup. See
How exfer mining works.
Assume-valid
Optimization where blocks at or below the hardcoded checkpoint height
(130 000) skip Argon2id PoW verification during IBD. All other
validation (signatures, UTXO accounting, state roots, coinbase rules,
timestamps) is always performed. Disable with --no-assume-valid.
Block
The unit of consensus. Contains a header (PoW, links to previous block) and a list of transactions. Target block time: 10 seconds.
Coinbase
The special first transaction in a block that pays the block reward and fees to the miner. Coinbase outputs are unspendable for 360 blocks (coinbase maturity).
Coinbase maturity
The number of confirmations a coinbase output needs before it can be spent: 360 blocks (~1 hour).
Confirmation
Each additional block built on top of the one containing your
transaction is one confirmation. tip_height - tx_block_height + 1.
Difficulty target
A threshold the block PoW hash must be below. Retargets every 4 320 blocks to keep block time near 10 s.
Dust
An output below 200 exfers. Skipped by the wallet's input-selection logic and discouraged at the protocol level.
EXFER / exfers
EXFER (upper case) is the user-facing unit. exfers (lower case) is
the base unit. 1 EXFER = 100 000 000 exfers. The CLI accepts both
formats.
Exfer Script
The transaction-condition language. A total functional combinator language — every script terminates and every cost is statically computable before execution. See Protocol specification §6.
Genesis block
The first block. Hash:
d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051.
Height
The number of blocks between genesis and a given block. Genesis is height 0.
HTLC (Hash Time-Locked Contract)
A payment claimable by the recipient on revealing a preimage, or refundable to the sender after a timeout. The primitive behind atomic swaps. See HTLC payments.
IBD (Initial Block Download)
The cold-sync process for a fresh node: download headers, then bodies, then validate.
Mempool
The local pool of unconfirmed transactions a node keeps until they're included in a block (or evicted).
Multisig
Locking script requiring N-of-M signatures to unlock. Exfer ships 2-of-2, 1-of-2, and 2-of-3 patterns. See Multisig.
Node identity
An Ed25519 keypair (node_identity.key) used by the P2P layer to
authenticate a node to its peers. Distinct from any wallet keys.
OutPoint
A reference to a specific output of a specific transaction:
{tx_id, output_index}. Inputs in a new transaction reference the
OutPoints they consume.
Preimage
The secret a recipient reveals to claim an HTLC. The lock specifies
hash_lock = SHA256(preimage).
PoW (Proof of Work)
The computational lottery a miner runs to produce a valid block. Exfer uses Argon2id.
Reorg (re-organization)
The chain switching from one fork to a longer competing fork. A transaction confirmed on the losing fork can become unconfirmed (or land in a different block). See Confirmation depth & reorg semantics.
RPC
The JSON-RPC 2.0 HTTP interface a node optionally exposes via
--rpc-bind. Seven methods total. See
JSON-RPC reference.
Script hash
The 32-byte SHA-256 of a canonical locking script. Addresses are script hashes — a plain pay-to-pubkey address is the hash of the trivial "check signature against this pubkey" script.
Tip
The highest-height block of the local node's best chain.
Tx (transaction)
A signed message that consumes one or more existing UTXOs and creates one or more new UTXOs.
UTXO (Unspent Transaction Output)
An output that hasn't been spent yet. Your balance is the sum of UTXOs locked to your address.
Brand assets
Logos, wordmarks, and the full brand kit for Exfer. Use these in articles, exchange listings, integrator dashboards, presentations, anywhere you need to display the project's identity.
All assets are released under the same MIT license as the upstream project. Don't modify the colors or geometry beyond resizing — keep the brand consistent across the ecosystem.
Quick reference
| When you need… | Use |
|---|---|
| A square avatar (Twitter/X, Discord, GitHub org) | Round token |
| An app icon (launcher, store listings) | Square favicon |
| A wide header / signature lockup | Wordmark |
| Just the symbol in copy | Mark only |
| Just the typography | Text only |
| A press kit / overview sheet | Full brand sheet |
Round token
The signature Exfer identity — a round black token with the X mark centered. Already has transparent corners; the round shape is the intended silhouette.
Smaller pre-rendered sizes:
Square favicon
A black square with the X glyph filling most of the canvas — fills more of the visible pixels in a browser tab than the round token does.
Horizontal wordmark
X mark + EXFER typography in a horizontal lockup. Designed for dark
backgrounds — the glyphs are white with cyan accents. For light
backgrounds, invert (CSS filter: invert(1) hue-rotate(180deg) works
well).
Mark only
Just the X glyph + corner squares, no surrounding circle. Use when text context already makes clear what project this is.
Text only
The EXFER wordmark typography by itself, no symbol. Pair with the mark for custom layouts.
Full brand sheet
The original brand-kit reference showing every variant — horizontal lockup, four app-icon sizes (512 / 128 / 64 / 24), dark and light treatments, and the tagline. Useful for press kits, slide decks, or internal design references.
Quick usage tips
- Background contrast matters. The wordmark and mark are designed with white + cyan glyphs that read well on dark backgrounds. On light backgrounds use CSS inversion or request a dark variant.
- Don't recolor the X. The two-tone (white + cyan) is part of the identity.
- Don't add gradients or shadows. The geometry is flat-design.
- Keep at least 10% padding when placing the wordmark next to other text or imagery.
License
MIT, same as the Exfer source. Attribution is appreciated when used in articles or third-party tooling.
Contributing
This site is community-maintained and welcomes contributions.
Reporting bugs / suggesting changes
- Bugs in the Exfer node / CLI: https://github.com/ahuman-exfer/exfer/issues
- Bugs or typos on this docs site: open an issue / PR on the
exfer-docsrepository.
When filing a bug, please include:
exfer --version- Exact command + flags
- Full error message + log context (
journalctl -u exferis great) - OS / distro / filesystem
Asking questions
For "is this the right way to do X?" questions, GitHub Discussions: https://github.com/ahuman-exfer/exfer/discussions
For real-time chat, look at the project README for the current community channel.
Contributing docs
The flow is the same as for any GitHub-hosted docs:
- Fork the repository.
- Edit the markdown under
src/. - Verify locally with
mdbook build(andmdbook serve --openfor live preview). - Open a PR.
Things that get merged quickly:
- Typo fixes, broken-link repairs.
- Clearer explanations of existing topics.
- New community node entries in
nodes.toml. - New script-pattern walkthroughs that match the existing tone.
Things that take longer:
- Major restructuring of the sidebar.
- Adding entirely new top-level sections.
- Changes to the deployment / hosting setup.
Style notes
- Optimize for someone who skimmed the intro and is now on this page. Don't reintroduce; do link back.
- Show runnable commands, not pseudocode.
- Explain the why at least once per page, then move to the how.
- Prefer terse over comprehensive. If a section is "nobody cares", cut it.
License
This documentation, like the upstream Exfer source, is MIT-licensed.
Acknowledgements
- The upstream Exfer maintainers, for shipping working software.
- Everyone running a community RPC node — the Live Nodes page would be empty without you.
- The community pool / bridge operators — separately maintained but part of the same ecosystem.