Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Exfer

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.

Use with an AI agent
Paste this, then ask your actual question.
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:

Works with Claude, ChatGPT, Cursor, Codex, or any agent that can fetch URLs. Already on Exfer's wavelength — the docs at exfer.org are written so an LLM can ingest them whole.

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 timeQuick start
No installWeb wallet at https://exfer.dev, explorer at https://explorer.exfer.dev
Local walletCreate a wallet
Receive a paymentReceive a payment
Send a paymentSend a payment
Run a full nodeInstall
MineHow Exfer mining works
Build an exchange / wallet / explorerIntegrate Exfer
Need API detailsJSON-RPC reference

A few quick facts

  • Genesis block: d7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051
  • Network ports: P2P 9333, JSON-RPC 9334 (optional)
  • Units: 1 EXFER = 100 000 000 exfers
  • Initial block reward: 100 EXFER, halving roughly every 2 years (6 307 200 blocks), 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) and SKILL.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/tcp for P2P and 9334/tcp for 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 paidReceive a payment walks through sharing the address and watching for the incoming transaction.
  • Pay someoneSend a payment, once you have some EXFER in the wallet.
  • Survive rebootsRun as a service makes the node start automatically and restart on failure.
  • Don't lose the walletBackup & 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"
}
FieldWhat it's for
fileThe path to the encrypted key file. Back this up.
address64-hex (32 bytes). Share this with people who pay you.
pubkeyThe 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-pubkey instead. 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 caseConfirmationsWall time
Casual receipt30~5 min
Standard value60~10 min
Large / irreversible360~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

FlagWhat it does
--walletPath to your encrypted .key file
--toRecipient's 64-hex address
--amountAmount to send. Accepts "10 EXFER" or "1000000000" (exfers)
--feeTransaction fee. Default 0.001 EXFER (100_000 exfers). Higher = faster inclusion under load
--rpcRPC node URL. Fetches your UTXOs and submits the tx
--datadirUse a local node instead of RPC (--rpc and --datadir are mutually exclusive)
--jsonMachine-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_000 exfers) 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

  1. exfer wallet send decrypts your wallet using the passphrase.
  2. Calls get_address_utxos over RPC to find your spendable outputs.
  3. Selects enough UTXOs to cover amount + fee (skips immature coinbases).
  4. Builds an unsigned transaction with one output to --to, one change output back to you, and a fee output to the miner.
  5. Signs every input locally with your Ed25519 private key.
  6. Calls send_raw_transaction with the signed tx hex.
  7. Returns the tx_id to 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 with exfer 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):

PathPurpose
~/.exfer/wallet.keyEncrypted wallet keypair (only if you used exfer init)
~/.exfer/node_identity.keyUnique 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

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:

PhaseApprox. duration on a typical VPS
Header chaina 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 exfer instance is running, or that port is taken. See Troubleshooting.
  • Permissions on node_identity.key are too open — pass --repair-perms to 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 9334 only to localhost (127.0.0.1:9334:9334). The RPC has no authentication — exposing it on 0.0.0.0 is dangerous on shared hosts.

Resource expectations

A small VPS is plenty for a full node:

ResourceComfortableTight
RAM1 GB512 MB
Disk50 GB20 GB (grows over time)
CPU1 vCPUshared-CPU is OK
Network10 Mbps1 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

FileLose 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.keyYour 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 --delete after 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:

  1. Read the upstream CHANGELOG for migration notes.
  2. Snapshot chain/ and state/.
  3. 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):

  1. Reinstall the binary on a new host.
  2. Restore wallet.key from your offline backup.
  3. Run exfer node (or exfer mine) — a fresh node_identity.key is generated, the chain re-syncs from peers.
  4. 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:

  1. Another exfer instance is already running. Stop it:
    sudo systemctl stop exfer
    pgrep -af exfer
    
  2. Another program owns the port. Identify it:
    sudo ss -ltnp | grep 9333
    
  3. Pick a different port for this instance:
    exfer node --bind 0.0.0.0:19333
    
    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.

"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:

  1. 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.
  2. Are you still in IBD? During cold-bootstrap, header sync runs first; bodies follow. The tip height only advances once bodies start validating.
  3. Check disk space. A full disk silently breaks RocksDB writes.
    df -h ~/.exfer
    
  4. 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 0x prefix (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 10 is 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 --version output.
  • 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

ConstantValue
PoW algorithmArgon2id
Memory cost (m)64 MiB
Time cost (t)2 iterations
Parallelism (p)1 lane
Target block time10 s
Difficulty retargetevery 4 320 blocks
Initial block reward100 EXFER
Reward half-life6 307 200 blocks (~2 years)
Minimum reward1 EXFER
Coinbase maturity360 blocks (~1 hour)
Genesis blockd7b6805c8fd793703db88102b5aed2600af510b79e3cb340ca72c1f762d1e051

Hash rate expectations

Argon2id is memory-bandwidth bound, not CPU-bound. Two things determine your throughput:

  1. Cache-line throughput / memory bandwidth — DDR4-3200 dual channel is meaningfully faster than single-channel DDR3.
  2. 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):

HardwareApprox. 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:

  1. Look at recent blocks via get_block in the RPC explorer — note the timestamps and difficulty.
  2. Compare your local attempts/sec against the implied network rate.
  3. 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 exfer binary (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
FlagWhat it does
--datadirWhere chain + state live. Same as exfer node
--miner-pubkeyThe pubkey that gets the coinbase reward. No private key needed on this host.
--rpc-bindOptional: expose RPC on the given address. Use 127.0.0.1 only.
--repair-permsAuto-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 $DATADIR shows growth, no full filesystem.
  • Time is correct: out-of-sync clocks cause peers to reject your blocks. timedatectl status should say "NTP synchronized: yes".
  • Pubkey is right: a typo in --miner-pubkey means 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.

PoolURLType
LuckyPoolhttps://exfer.luckypool.io/community-run
NinjaRaiderhttps://ninjaraider.com/exfer-pplnsPPLNS

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 to H.
  • 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

  • timeout too tight. If the lock tx isn't even confirmed by the time timeout hits, 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.
  • timeout too 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.
  • preimage mismatch. The hash you advertise must be exactly SHA256(preimage_bytes). Common bug: hashing the hex string of the preimage instead of the bytes. The example above uses xxd -r -p | shasum -a 256 to 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.

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.

PatternRequired signersTypical use
2-of-2both partiesjoint custody, payment channels, two-party agreement
1-of-2either partyshared account with two key holders, backup access
2-of-3any two of threecommittee 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 (from exfer 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.
  • 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 wantAdd this many blocks
1 hour360
6 hours2 160
1 day8 640
1 week60 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_height when 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 --locktime on spend. The script binds the locktime in the spending witness — you must repeat the same --locktime you 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, --timeout must match the lock-time values exactly on every spend path. The script binds them in the witness; mismatched values produce script-eval failures.
  • Picking --timeout too short. If the trade isn't done by timeout, 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 --expiry too 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 / --delegate on 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:

  1. Generate deposit addresses per user.
  2. Detect deposits as they confirm.
  3. Credit user balances after enough confirmations.
  4. Process withdrawals by signing and broadcasting transactions.
  5. 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:

TierConfirmationsWall time
Small deposits30~5 min
Standard deposits60~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:

  1. Fetches UTXOs of the hot wallet via get_address_utxos.
  2. Builds an unsigned transaction.
  3. Signs locally. The private key never leaves the machine running this command.
  4. 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:

LayerWhat it holdsKey location
Hot walletenough for daily withdrawalsonline, encrypted at rest, automated signing
Warm walletrolling reserve refilling hotonline but isolated, manual signing
Cold storagebulk reservesoffline, 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

For wallet developers

Building a wallet (CLI, mobile, web, hardware integration)? You need to:

  1. Derive keys — single key per wallet, or HD-style many keys from one seed.
  2. Read state — fetch UTXOs and confirmations via RPC.
  3. Build transactions — serialize correctly so the network accepts them.
  4. Sign — Ed25519 over a canonical message.
  5. 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:

MethodPurpose
get_balanceCheap "any value here?" check
get_address_utxosItemized spendable outputs (for building txs)
get_transactionStatus of a specific tx
get_block_heightCompute 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:

  1. 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.
  2. Build outputs.
    • One output to recipient (amount exfers, locked by recipient's address script).
    • One change output back to sender (total_input - amount - fee exfers, locked by sender's address script). Skip if change would be below dust threshold; the surplus becomes additional fee.
  3. Serialize unsigned tx. Follow the canonical byte layout from Protocol spec §5.
  4. Compute signing message. Domain-separated hash over the canonical unsigned tx. See Protocol spec §3 for the domain string.
  5. Sign each input with the corresponding private key, producing Ed25519 signatures.
  6. Embed signatures in the witness fields, producing the final canonical serialized tx.
  7. 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>-lock and -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.rs or 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

For block explorers

You want to ingest the full Exfer chain, store it queryable, and surface it to users on a web UI. Concretely:

  1. Run an archive node — every block, every transaction, available indefinitely.
  2. Index block / transaction / address data into a search-friendly store (Postgres, Elasticsearch, ClickHouse, whatever).
  3. Surface a web UI with the usual explorer features.
  4. 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_template field 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_block returns only the txid list; the bodies require N additional get_transaction calls 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

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:

https://exfer.dev

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 .key file — 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:

https://explorer.exfer.dev

Use it to:

  • Look up a transaction by its tx_id and 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:

  1. Build against your own node's RPC (or a community node) for programmatic checks.
  2. 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

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-Length header 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

LimitValueScope
Concurrent connections32Server-wide
Per-connection read timeout30 sPer request
Default request body64 KiBAll methods unless overridden
send_raw_transaction body2.5 MiBPer request
get_script_utxos body200 KiBPer request
Response body8 MiBPer response
Response headers64 KiBPer response
send_raw_transaction rate60 / min / IPSliding window
UTXO scan rate (balance/utxos)30 / min / IPSliding window
UTXO scan concurrency1 globallySerialized to protect tip-write lock
UTXO list size1000 entriesSee 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:

CodeNameMeaning
-32700Parse errorMalformed JSON or framing
-32601Method not foundUnknown method
-32602Invalid paramsBad hex, wrong length, missing field, tx invalid, block missing
-32603Internal errorStorage / 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.

FieldTypeNotes
Address32-byte hex (64 chars)An address is a script hash. The same string is accepted as the script_hex parameter in get_script_utxos.
Hash25632-byte hex (64 chars)Transaction IDs, block IDs, merkle roots — all 32-byte SHA-256.
Amountunsigned integerDenominated in exfers (base units). 1 EXFER = 100_000_000 exfers.
Heightunsigned 64-bitGenesis 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.

MethodPurpose
get_block_heightCurrent chain tip
get_blockBlock header + txid list by height or hash
get_transactionLook up a transaction by txid
get_balanceSpendable balance of an address
get_address_utxosItemize unspent outputs of an address
get_script_utxosItemize UTXOs locked by a non-address script
send_raw_transactionBroadcast 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: true is 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: true outputs are not yet spendable if tip_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_len is 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 as get_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:

DepthWall time (~)Notes
110 sBlock-inclusion confirmation; reorg-fragile
305 minSurvives short reorg activity under normal conditions
6010 minConservative for most non-trivial value transfers
360~1 hrMatches 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

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

TypeWire sizeEncoding
u81 byteunsigned integer
u162 byteslittle-endian
u324 byteslittle-endian
u648 byteslittle-endian
Hash25632 bytesraw bytes (big-endian as a 256-bit number)
VarBytes2 + length bytesu16 LE length prefix, then data
Bool flag1 byte0x00 = 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.

OffsetSizeFieldTypeDescription
04versionu32Protocol version. Must be 1.
48heightu64Block height. Genesis = 0.
1232prev_block_idHash256Parent block's ID. Genesis = all zeros.
448timestampu64Unix timestamp (seconds since epoch).
5232difficulty_targetHash256Full 256-bit PoW target.
848nonceu64Miner-chosen value for PoW search.
9232tx_rootHash256Merkle root of transaction hashes.
12432state_rootHash256Sparse 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:

  1. If zero transactions: root = all-zero Hash256.
  2. If one transaction: root = that transaction's WtxId.
  3. If odd count: duplicate the last hash.
  4. Pair adjacent hashes and compute parent: domain_hash("EXFER-TXROOT", left || right).
  5. 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 = true
  • Option(A) = Sum(Unit, A) — Left(Unit) = None, Right(a) = Some(a)
  • Bytes = List(Bound(256)) — variable-length byte string
  • Hash256 = Bound(0) — sentinel for 256-bit hash (opaque to type system, handled by jets)
  • U64 = Bound(u64_max) — 64-bit unsigned integer
  • U256 — 256-bit unsigned integer, a nominal type with a dedicated Type::U256 variant. U256 is not an alias for Product(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

CombinatorNotationType Rule
IdenA → AIdentity function
UnitA → UnitDiscard input, return unit
Comp(f, g)A → Cf: 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) → Cf: A→C — project first
Drop(f)Product(A, B) → Cf: 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) → Cf: A→C, g: B→C — branch on tag
Fold(f, z, k)Product(Bound(k), A) → Bz: A→B, f: Product(A, B)→B — bounded iteration
ListFold(f, z)Product(List(A), B) → Bz: B→B, f: Product(A, B)→B — list iteration
Jet(id)per jetNative operation (Section 7)
WitnessUnit → TRead value from witness data at evaluation time
MerkleHidden(hash)Pruned subtree placeholder (cannot be evaluated)
Const(v)Unit → TConstant 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).

CombinatorStepsCells
Iden10
Unit10
Comp(f, g)cost(f).steps + cost(g).steps + 1cost(f).cells + cost(g).cells
Pair(f, g)cost(f).steps + cost(g).steps + 1cost(f).cells + cost(g).cells + 1
Take(f)cost(f).steps + 1cost(f).cells
Drop(f)cost(f).steps + 1cost(f).cells
InjL(f)cost(f).steps + 1cost(f).cells + 1
InjR(f)cost(f).steps + 1cost(f).cells + 1
Case(f, g)max(cost(f).steps, cost(g).steps) + 1max(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).stepsjet_cost(id).cells
Witness10
MerkleHidden00
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:

TagCombinatorData
0x00Iden
0x01Comp(f, g)f(u32 LE) g(u32 LE)
0x02Unit
0x03Pair(f, g)f(u32 LE) g(u32 LE)
0x04Take(f)f(u32 LE)
0x05Drop(f)f(u32 LE)
0x06InjL(f)f(u32 LE)
0x07InjR(f)f(u32 LE)
0x08Case(f, g)f(u32 LE) g(u32 LE)
0x09Fold(f, z, k)f(u32 LE) z(u32 LE) k(u64 LE)
0x0AListFold(f, z)f(u32 LE) z(u32 LE)
0x0BJet(id)id(u32 LE)
0x0CWitness
0x0DMerkleHidden(h)hash(32 bytes)
0x0EConst(v)value_len(u32 LE) value_bytes

Value serialization tags:

TagValueData
0x00Unit
0x01Left(v)value
0x02Right(v)value
0x03Pair(a, b)a then b
0x04List(vs)count(u32 LE) then elements
0x05Bytes(bs)length(u32 LE) then data
0x06U64(n)n(u64 LE)
0x07U256(d)data(32 bytes, big-endian)
0x08Bool(b)0x00=false, 0x01=true
0x09Hash(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 h directly (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

CategoryID RangeJets
Cryptographic0x0001–0x0004Sha256, Ed25519Verify, SchnorrVerify, MerkleVerify
Arithmetic (64-bit)0x0100–0x0107Add64, Sub64, Mul64, Div64, Mod64, Eq64, Lt64, Gt64
Arithmetic (256-bit)0x0200–0x0207Add256, Sub256, Mul256, Div256, Mod256, Eq256, Lt256, Gt256
Byte Operations0x0300–0x0304Cat, Slice, Len, EqBytes, EqHash
Introspection0x0400–0x0408TxInputs, TxOutputs, TxValue, TxScriptHash, TxInputCount, TxOutputCount, SelfIndex, BlockHeight, TxSigHash
List Operations0x0500–0x0505ListLen, 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) × 8 steps.

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) × 8 steps.

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) × 500 steps.

7.3 Arithmetic Jets (64-bit)

All take Product(U64, U64) input.

JetIDOutputBehaviorError
Add640x0100U64a + bOverflow if result > u64 max
Sub640x0101U64a - bOverflow if a < b
Mul640x0102U64a × bOverflow if result > u64 max
Div640x0103U64a / b (floor)DivisionByZero if b = 0
Mod640x0104U64a mod bDivisionByZero if b = 0
Eq640x0105Boola = b
Lt640x0106Boola < b
Gt640x0107Boola > 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.

JetIDOutputBehaviorError
Add2560x0200U256a + bOverflow if result ≥ 2^256
Sub2560x0201U256a - bOverflow if a < b
Mul2560x0202U256a × bOverflow if result ≥ 2^256
Div2560x0203U256a / b (floor)DivisionByZero if b = 0
Mod2560x0204U256a mod bDivisionByZero if b = 0
Eq2560x0205Boola = b
Lt2560x0206Boola < b (big-endian)
Gt2560x0207Boola > 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 / 8 steps.

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 / 8 steps.

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) / 8 steps.

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 × 10 steps.

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 × 10 steps.

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 / 64 steps.

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_length steps.

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_length steps.

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_length steps.

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_length steps.

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):

  1. Witness must be exactly 96 bytes: pubkey(32) || signature(64).
  2. Redeemer must be absent.
  3. Compute domain_hash("EXFER-ADDR", pubkey). Must equal the script.
  4. Signing message: "EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body.
  5. Reject the pubkey if it is a small-order (weak) Ed25519 point — such keys can validate signatures across unrelated messages.
  6. 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):

  1. Deserialize the script. Re-serialize and verify byte-for-byte equality (canonical check).
  2. Type-check the program. Root must output Bool.
  3. Strict type edge check: all composition edges have exact type matches (no Unit wildcards in internal edges).
  4. Reject scripts containing unimplemented jets, MerkleHidden nodes, or heterogeneous list constants.
  5. Root input type must be compatible with the runtime input shape (Section 8.5).
  6. DAG depth must not exceed 128.
  7. Minimum-case cost must not exceed 4,000,000 steps.
  8. Resolve datum (Section 8.3).
  9. Build script input value (Section 8.5).
  10. Compute cost with actual list sizes from the transaction.
  11. Reject if cost exceeds 4,000,000 steps.
  12. Evaluate with budget = 4,000,000 steps, computed cells.
  13. 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_target is 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):

HeightReward (exfers)
010,000,000,000
19,999,998,912
1009,999,891,228
1,0009,998,912,280
4,3209,995,301,790
10,0009,989,127,892
43,2009,953,117,900
100,0009,891,814,300
6,307,2005,050,000,000
12,614,4002,575,000,000
18,921,6001,337,500,000
63,072,000109,667,968
630,720,000100,000,000

12. Transaction Validation

A non-coinbase transaction is valid if and only if all of the following hold:

  1. At least one input.
  2. At least one output.
  3. Witness count equals input count.
  4. No duplicate inputs (same outpoint referenced twice).
  5. Each input references an existing UTXO.
  6. Script validation passes for every input (Section 8.2).
  7. Value conservation: sum(input_values) ≥ sum(output_values), computed with u128 intermediates. Both sums must fit in u64.
  8. 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.
  9. Dust threshold: Every output value ≥ 200 exfers.
  10. Coinbase maturity: Inputs spending coinbase outputs must have age ≥ 360 blocks.
  11. Size limit: Serialized transaction ≤ 1,048,576 bytes (1 MiB).
  12. 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.
  13. 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

  1. Exactly one input with sentinel outpoint: prev_tx_id = all zeros.
  2. output_index encodes height: output_index = height as u32. Height > u32::MAX is invalid.
  3. Reward: sum(output_values) = block_reward(height) + total_fees. Exact equality required.
  4. Dust threshold: Every output value ≥ 200 exfers.
  5. 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).
  6. Position 0 in the block's transaction list.
  7. Output script validity: Same rules as non-coinbase outputs (Section 12, rule 12).
  8. Size limit: ≤ 1,048,576 bytes.
  9. 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
  1. script_eval_cost: Sum over all inputs. Per input: 5,000 + ceil_div(sig_message_bytes, 64) × 8 if the spent output's script is a pubkey hash (32 bytes), where sig_message_bytes is the length of the domain-separated signing message; actual runtime step count from evaluation if the spent output's script is a script lock.
  2. output_typecheck_cost: 1,000 per non-pubkey-hash output (0 for 32-byte scripts).
  3. witness_deser_cost: sum over inputs of ceil_div(witness_bytes, 64) + ceil_div(redeemer_bytes, 64).
  4. datum_deser_cost: sum over outputs of ceil_div(datum_bytes, 64).
  5. tx_deser_cost: ceil_div(total_serialized_tx_bytes, 64).
  6. utxo_io_cost: input_count × 100 + output_count × 100.
  7. smt_cost: input_count × 500 + output_count × 500.
  8. 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:

  1. Header is 156 bytes (implied by deserialization).
  2. Version = 1.
  3. Height = parent.height + 1 (genesis: height = 0).
  4. prev_block_id = parent.block_id() (genesis: all zeros).
  5. PoW valid: Argon2id hash < difficulty_target.
  6. Difficulty target matches expected value from retarget algorithm.
  7. Timestamp > MTP (median of up to 11 ancestor timestamps).
  8. Timestamp ≤ wall_clock + 120 seconds (policy, skipped during initial block download).
  9. Timestamp ≤ parent.timestamp + 604,800 (7-day gap limit, consensus).
  10. tx_root matches computed Merkle root of WtxIds.
  11. state_root matches computed SMT root after applying all transactions.
  12. First transaction is coinbase; no other transaction has sentinel outpoint.
  13. Coinbase valid (Section 13).
  14. No duplicate TxIds in the block.
  15. No double-spends within the block (no two non-coinbase transactions spend the same outpoint).
  16. Block size ≤ 4,194,304 bytes (4 MiB).
  17. Each non-coinbase transaction valid (Section 12).
  18. 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):

  1. Higher cumulative work is preferred.
  2. If equal work: higher height is preferred.
  3. 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

IDNamePayload
0x01HelloHandshake message (see below)
0x02Pingempty
0x03Pongempty
0x10NewBlockSerialized Block
0x11GetBlocksu32 LE count, then count × Hash256
0x12BlockResponseSerialized Block
0x13GetTipempty
0x14TipResponseheight(u64 LE) block_id(Hash256) cumulative_work([u8; 32])
0x15Invu32 LE count, then count × Hash256
0x16GetAddrempty
0x17Addru32 LE count, then count × AddrEntry
0x18AuthAcksignature(64 bytes)
0x20NewTxSerialized Transaction
0x21GetHeadersstart_height(u64 LE) max_count(u32 LE)
0x22Headersu32 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:

  1. Initiator sends Hello with nonce_a, pubkey_a, sig=[0; 64].
  2. Responder verifies version and genesis. Sends Hello with nonce_b, echo=nonce_a, pubkey_b, sig_b over transcript(role=0x00).
  3. Initiator verifies echo, verifies sig_b, sends AuthAck with sig_a over transcript(role=0x01).
  4. Responder verifies sig_a. Connection established.

Timeout: 5 seconds for handshake completion.

Rate Limits

ResourceLimitWindow
Blocks per peer12per minute
Global blocks24per minute
Transactions per peer60per minute
Global transactions200per minute
Pings per peer10per minute
Requests per peer30per minute
Unsolicited messages per peer10per minute
Response bytes per peer16 MiBper minute
Global response bytes128 MiBper minute
Invalid blocks per peer3per minute
Invalid transactions per peer16per 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):

FieldValue
Version1
Height0
prev_block_id0000000000000000000000000000000000000000000000000000000000000000
Timestamp1,773,536,400 (2026-03-15T01:00:00Z)
Difficulty target2^248 = 0100000000000000000000000000000000000000000000000000000000000000
Nonce259
tx_root96d29616a481eac5ffa35f3f7cf2add76ac921e733f72174d45035b5996341d3
state_rootaafc1988635522e0fdaa4249ccda596127ff689eba8cd1de01a9cdaaf671e9a8

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
IdentifierValue
TxId5e63e65ea2a30d9c874f16eccb366022bfe692d6d933470cb50107df7c2b04c6
WtxId96d29616a481eac5ffa35f3f7cf2add76ac921e733f72174d45035b5996341d3

20. Constants

Consensus

ConstantValueDescription
VERSION1Block version
PROTOCOL_VERSION5Network protocol version
TARGET_BLOCK_TIME_SECS10Target seconds between blocks
RETARGET_WINDOW4,320Blocks between difficulty adjustments
MAX_RETARGET_FACTOR4Maximum difficulty change per retarget
COINBASE_MATURITY360Blocks before coinbase is spendable
MAX_BLOCK_SIZE4,194,304Maximum block size in bytes
MAX_TX_SIZE1,048,576Maximum transaction size in bytes
MTP_WINDOW11Ancestor count for median time past
MAX_TIMESTAMP_DRIFT120Maximum seconds ahead of wall clock (policy)
MAX_TIMESTAMP_GAP604,800Maximum seconds between parent and child timestamps
BLOCK_HEADER_SIZE156Header size in bytes

Emission

ConstantValueDescription
BASE_REWARD100,000,000Minimum reward (1 EXFER)
DECAY_COMPONENT9,900,000,000Decaying component (99 EXFER)
HALF_LIFE6,307,200Blocks per halving (~2 years)
EXFER_UNIT100,000,000Exfers per 1 EXFER

Proof of Work

ConstantValueDescription
ARGON2_MEMORY_KIB65,536Memory parameter (64 MiB)
ARGON2_ITERATIONS2Time parameter
ARGON2_PARALLELISM1Parallelism parameter
ARGON2_OUTPUT_LEN32Output length in bytes

Fee and Cost

ConstantValueDescription
UTXO_LOOKUP_COST100Cost per input UTXO lookup
UTXO_CREATE_COST100Cost per output UTXO creation
SMT_DELETE_COST500Cost per SMT leaf deletion
SMT_INSERT_COST500Cost per SMT leaf insertion
STANDARD_SPEND_COST20,000Reference cost for dust calculation
MIN_FEE_DIVISOR100Divisor for minimum fee
DUST_THRESHOLD200Minimum output value in exfers
PUBKEY_HASH_EVAL_COST5,000Base cost per pubkey hash input (+ data-proportional Ed25519 charge)
OUTPUT_TYPECHECK_COST1,000Cost per script-locked output

Script Limits

ConstantValueDescription
MAX_WITNESS_SIZE65,535Maximum witness bytes per input
MAX_DATUM_SIZE4,096Maximum datum bytes per output
MAX_REDEEMER_SIZE16,384Maximum redeemer bytes per input
MAX_SCRIPT_MEMORY16,777,216Maximum script evaluation memory
MAX_SCRIPT_STEPS4,000,000Maximum steps per input
MAX_TX_SCRIPT_BUDGET20,000,000Maximum steps per transaction
MAX_SCRIPT_NODES65,535Maximum nodes in a program
MAX_LIST_LENGTH65,536Maximum list length
MAX_VALUE_DEPTH128Maximum value nesting depth

Network

ConstantValueDescription
MAX_MESSAGE_SIZE8,388,608Maximum network message size
MAX_OUTBOUND_PEERS8Outbound peer limit
MAX_INBOUND_PEERS64Inbound peer limit
MAX_INBOUND_PER_IP4Per-IP inbound limit
PING_INTERVAL_SECS60Keepalive interval
PONG_DEADLINE_SECS15Pong timeout
HANDSHAKE_TIMEOUT_SECS5Handshake timeout
MAX_GETBLOCKS_ITEMS64Max hashes per GetBlocks
MEMPOOL_CAPACITY8,192Maximum mempool entries
MAX_ADDR_ITEMS64Max addresses per Addr message
MAX_ADDR_BOOK_SIZE1,024Maximum address book entries
MAX_ADDR_PER_MSG_ACCEPT16Max addresses accepted per message
MAX_GETADDR_PER_CONN2Max GetAddr requests per connection
MAX_UNSOLICITED_ADDR_PER_MIN3Unsolicited Addr messages per minute
ADDR_FLUSH_INTERVAL_SECS300Address book flush interval
MAX_GETBLOCKS_RESPONSE8Max blocks per GetBlocks response
MAX_INV_ITEMS64Max items per Inv message

Rate Limits

ConstantValueDescription
MAX_BLOCKS_PER_MIN12Blocks per peer per minute
MAX_GLOBAL_BLOCKS_PER_MIN24Global blocks per minute
MAX_TXS_PER_MIN60Transactions per peer per minute
MAX_GLOBAL_TXS_PER_MIN200Global transactions per minute
MAX_PINGS_PER_MIN10Pings per peer per minute
MAX_REQUESTS_PER_MIN30Requests per peer per minute
MAX_UNSOLICITED_PER_MIN10Unsolicited messages per peer per minute
MAX_RESPONSE_BYTES_PER_MIN16,777,216Response bytes per peer per minute (16 MiB)
MAX_GLOBAL_RESPONSE_BYTES_PER_MIN134,217,728Global response bytes per minute (128 MiB)

Peer Penalties

ConstantValueDescription
MAX_INVALID_BLOCKS_PER_PEER3Invalid blocks before disconnect
MAX_INVALID_TXS_PER_PEER16Invalid transactions before disconnect
MAX_CONTROL_MSGS_DURING_IBD50Max interleaved non-response messages during IBD

Orphan and Fork Handling

ConstantValueDescription
MAX_ORPHAN_BLOCKS16Maximum orphan blocks cached
MAX_ORPHAN_BLOCK_SIZE4,194,304Maximum orphan block size (= MAX_BLOCK_SIZE)
MAX_ORPHAN_CACHE_BYTES67,108,864Total orphan cache size (64 MiB)
MAX_FORK_BLOCK_SIZE4,194,304Maximum fork block size (= MAX_BLOCK_SIZE)
MAX_FORK_BLOCKS128Maximum fork chain length for reorg
MAX_RETAINED_FORK_HEADERS10,000Maximum retained non-canonical headers after fork eviction

Transaction Limits

ConstantValueDescription
MIN_TX_SIZE50Minimum serialized transaction size
MAX_SPENT_UTXOS_SIZE16,777,216Maximum 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:

  1. Compute sender address: address = domain_hash("EXFER-ADDR", pk).

  2. Select inputs: Choose UTXOs whose scripts match the sender address. Skip coinbase UTXOs with age < 360 blocks. Accumulate until total_input ≥ amount + estimated_fee.

  3. 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.
  4. 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 × 2
    • utxo_io_cost = input_count × 100 + output_count × 100
    • smt_cost = input_count × 500 + output_count × 500
    • tx_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.

  5. 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).
  6. Compute signing message: "EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body.

  7. Sign: Ed25519 sign the message with sk. Signature is 64 bytes.

  8. Construct witnesses: For each input: witness = pk(32) || signature(64), redeemer = None.

  9. Final serialization: tx_header || tx_body || witnesses.

  10. Compute TxId: domain_hash("EXFER-TX", tx_header || tx_body).

  11. Compute WtxId: domain_hash("EXFER-WTXID", full_serialization).

21.2 Script-Locked Output

To lock funds to a script program:

  1. Construct the program as a DAG of combinators using the builder interface (Section 22).
  2. Serialize the program (Section 6.6): node_count(u32 LE) || root_index(u32 LE) || nodes....
  3. Place the serialized bytes in the output's script field.
  4. Set datum and datum_hash as 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

  1. Deserialize the script from the output being spent.
  2. Determine the script's expected input shape.
  3. Construct the witness data: serialize the values the script expects to read via Witness nodes.
  4. Construct the redeemer if the script expects one (or if the output has datum_hash requiring a datum).
  5. Place in the transaction witness: witness = serialized_witness_values, redeemer = redeemer_bytes.
  6. 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

PatternSteps (per input)Cells
Pubkey hash (32-byte script)5,000 + ceil_div(sig_msg_bytes, 64) × 80
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

JetIDInputOutputStepsCellsBehavior
Sha2560x0001BytesHash2561,0001SHA-256(input). Runtime: 500 + len/64 × 8.
Ed25519Verify0x0002Product(Bytes, Product(Bytes, Bytes))Bool5,0001ZIP-215 verify(msg, pk, sig). False if pk≠32, sig≠64, or pk is small-order. Runtime: 5,000 + ceil(msg_len/64) × 8.
SchnorrVerify0x0003Product(Bytes, Product(Bytes, Bytes))Bool5,0001Reserved. Always fails.
MerkleVerify0x0004Product(Hash256, Product(Hash256, Bytes))Bool32,0001Verify Merkle proof. Runtime: 500 + proof_len/33 × 500.

26.2 Arithmetic (64-bit)

JetIDInputOutputStepsCellsBehavior
Add640x0100Product(U64, U64)U64101a + b. Error on overflow.
Sub640x0101Product(U64, U64)U64101a - b. Error if a < b.
Mul640x0102Product(U64, U64)U64101a × b. Error on overflow.
Div640x0103Product(U64, U64)U64101a / b. Error if b = 0.
Mod640x0104Product(U64, U64)U64101a mod b. Error if b = 0.
Eq640x0105Product(U64, U64)Bool101a = b.
Lt640x0106Product(U64, U64)Bool101a < b.
Gt640x0107Product(U64, U64)Bool101a > b.

26.3 Arithmetic (256-bit)

JetIDInputOutputStepsCellsBehavior
Add2560x0200Product(U256, U256)U256501a + b. Error on overflow.
Sub2560x0201Product(U256, U256)U256501a - b. Error if a < b.
Mul2560x0202Product(U256, U256)U256501a × b. Error on overflow.
Div2560x0203Product(U256, U256)U256501a / b. Error if b = 0.
Mod2560x0204Product(U256, U256)U256501a mod b. Error if b = 0.
Eq2560x0205Product(U256, U256)Bool501a = b.
Lt2560x0206Product(U256, U256)Bool501a < b (big-endian).
Gt2560x0207Product(U256, U256)Bool501a > b (big-endian).

26.4 Byte Operations

JetIDInputOutputStepsCellsBehavior
Cat0x0300Product(Bytes, Bytes)Bytes1001Concatenate. Runtime: 10 + total_len/8.
Slice0x0301Product(Bytes, Product(U64, U64))Bytes1001source[start..start+len]. Clamps to bounds. Runtime: 10 + src_len/8.
Len0x0302BytesU64100Byte count.
EqBytes0x0303Product(Bytes, Bytes)Bool5000Byte equality. Runtime: 10 + max(len_a, len_b)/8.
EqHash0x0304Product(Hash256, Hash256)Bool5000Hash equality. Runtime: 14.

26.5 Introspection

JetIDInputOutputStepsCellsBehavior
TxInputs0x0400UnitList(...)1,0000All inputs. Runtime: 10 + n×10.
TxOutputs0x0401UnitList(...)1,0000All outputs. Runtime: 10 + n×10.
TxValue0x0402U64U64100Input value by index. Error if OOB.
TxScriptHash0x0403U64Hash256100Input script hash by index. Error if OOB.
TxInputCount0x0404UnitU6450Number of inputs.
TxOutputCount0x0405UnitU6450Number of outputs.
SelfIndex0x0406UnitU6450Current input index.
BlockHeight0x0407UnitU6450Current block height.
TxSigHash0x0408UnitBytes50Signing digest. Runtime: 5 + len/64.

26.6 List Operations

JetIDInputOutputStepsCellsBehavior
ListLen0x0500List(A)U64100Element count.
ListAt0x0501Product(List(A), U64)Option(A)101Element at index. None if OOB.
ListSum0x0502List(U64)U641,0000Sum. 0 if empty. Error on overflow. Runtime: 10 + n.
ListAll0x0503List(Bool)Bool1,0000All true. True if empty. Runtime: 10 + n.
ListAny0x0504List(Bool)Bool1,0000Any true. False if empty. Runtime: 10 + n.
ListFind0x0505List(Bool)Option(U64)1,0000Index 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:

  1. For all UTXO in S: if is_coinbase, then H - height ≥ 360.
  2. sum(values in S) ≥ A + min_fee(tx with |S| inputs, estimated outputs).
  3. If sum(values in S) - A - fee ≥ 200: create a change output (additional output in fee calculation).
  4. If sum(values in S) - A - fee < 200 and > 0: fold residual into fee.
  5. Every output value ≥ 200 (dust threshold).
  6. 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:

  1. Compute signing message: "EXFER-SIG" || genesis_block_id(32) || tx_header || tx_body.
  2. Ed25519 sign the message with the signing key.
  3. 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:

  1. Serialize the transaction (Section 5.1).
  2. Construct a NewTx message: msg_type=0x20 || payload_length(u32 LE) || serialized_transaction.
  3. 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]).

SeparatorResult
EXFER-SIGc32adc238ff3a66535a9180383711c5b84528fd535ff7e1934372f0a30efbbe6
EXFER-TX7cf80b71d07b2c0f8a0645a57c5d98c511fbe267503224a93c38968d4411ca03
EXFER-TXROOTf53925c44981d10789596e61503f829fd717420a4e77f7a1c58dc6dcbc09d2a7
EXFER-STATE1cb15b3427e2260373722ae99aff98358e2c03bd2760b9493cf6d3fc30a54d7d
EXFER-ADDR48e0d24cf73d51393cd3102222cdf69d02394261d1c9ce6e4d599214c3a9d228
EXFER-AGENTe02ef4eaf362f7cb4bbf0cb1f7bdc3410db8e7d3cd3397a8fa0646933f14de12
EXFER-SCRIPT0136103e88d1ccc06c639d3a2e99002941e61e691b775259268e0280c4bcca23
EXFER-POW-P0e4b0b5d4652e52c57e77e90bac1fc8fb83e7feb932f5a1b1f398deda962c749
EXFER-POW-S65974595a51e46671a67197774f0b2b2b11164b51b696cf9efd2d20dba16040c
EXFER-WTXID8a9366392187901fdeccaae02cacf9a63dbf7a60d112e4b13ca13230e3743613
EXFER-AUTH89edf3fdddaa59667639e471be147918056ae1a00b8502a60fd9c8f6871e72c2
EXFER-SESSION5e4c61773f051a55a8138785a3ac5987b233b4765c376229c8f4a717b4da36ca
EXFER-MERKLEce2039c9d6b0f0ea92b42b8a3a9c3b7b7d4e3bd8f1fedeb3d3e52dad820116ad
EXFER-MAC-IRcafb477b835296bc0bb56ef0ebf24f513e5a2df9dd657c813cafa41732aca082
EXFER-MAC-RI214b628b02f189b484f32ee032c5c98a1b309e57673165041d47417e8be0eb8b

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 countMerkle root
19beb7854b427d2f24abe3be7a8fa2af3f4a3751a17bd7647b41ac17c2dcdc80f
2b5f5507bd7ff50450c1c818c90a0abad59a5c805926fa94b3ee8a1e6a346873a
33d6a38ee7cb8ac6f1f0d75b3a473ebf9212ee13ebab2307e432c0526d64ab5f7
48ff248a73262d32dc0ed7322fd61bbab0fc8bb95f9e84b22bbfe87f963e212a2

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

HeightReward (exfers)
010,000,000,000
19,999,998,912
1009,999,891,228
1,0009,998,912,280
4,3209,995,301,790
10,0009,989,127,892
43,2009,953,117,900
100,0009,891,814,300
6,307,2005,050,000,000
12,614,4002,575,000,000
18,921,6001,337,500,000
63,072,000109,667,968
630,720,000100,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

SeparatorByte EncodingUsage
EXFER-SIGb"EXFER-SIG" (9 bytes)Transaction signing message prefix; followed by genesis_block_id(32) to bind signatures to this chain
EXFER-TXb"EXFER-TX" (8 bytes)TxId computation
EXFER-TXROOTb"EXFER-TXROOT" (12 bytes)Merkle tree internal nodes
EXFER-STATEb"EXFER-STATE" (11 bytes)SMT leaf key derivation
EXFER-ADDRb"EXFER-ADDR" (10 bytes)Address (pubkey hash) derivation
EXFER-AGENTb"EXFER-AGENT" (11 bytes)Agent identity derivation
EXFER-SCRIPTb"EXFER-SCRIPT" (12 bytes)Script Merkle commitment (program serialization)
EXFER-MERKLEb"EXFER-MERKLE" (12 bytes)MerkleVerify jet internal node hashing
EXFER-POW-Pb"EXFER-POW-P" (11 bytes)PoW password derivation
EXFER-POW-Sb"EXFER-POW-S" (11 bytes)PoW salt derivation
EXFER-WTXIDb"EXFER-WTXID" (11 bytes)Witness-committed transaction hash
EXFER-AUTHb"EXFER-AUTH" (10 bytes)Peer authentication transcript
EXFER-SESSIONb"EXFER-SESSION" (13 bytes)Session key derivation from transcript and DH secret
EXFER-MAC-IRb"EXFER-MAC-IR" (12 bytes)Directional MAC key derivation: initiator → responder
EXFER-MAC-RIb"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:

  1. sig_check_bob = Comp(Pair(Comp(Jet(TxSigHash), Unit), Pair(Const(pk_bob), Witness)), Jet(Ed25519Verify))
  2. hash_check = Comp(Pair(Comp(Witness, Jet(Sha256)), Const(H)), Jet(EqHash))
  3. hash_path = and(hash_check, sig_check_bob)
  4. sig_check_alice = [similar to sig_check_bob with pk_alice]
  5. timeout_check = Comp(Pair(Jet(BlockHeight), Const(U64(100000))), Jet(Gt64))
  6. timeout_path = and(timeout_check, sig_check_alice)
  7. 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):

  1. Selector: Left(Unit)[0x01, 0x00]
  2. Preimage: Bytes(preimage)[0x05, len_le4, preimage_bytes]
  3. 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
FlagPurpose
--datadirWhere chain + state live (default data)
--bindP2P bind address (default 0.0.0.0:9333)
--peersManually specify peers (repeatable). Defaults to DNS seed
--rpc-bindOptional JSON-RPC endpoint
--repair-permsAuto-fix node_identity.key permissions on startup
--verify-allRe-verify PoW for all replayed blocks (slow)
--no-assume-validDisable 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:

SubcommandPurpose
generateCreate a new keypair
infoShow address + pubkey of a wallet file
balanceSum of spendable UTXOs for the wallet's address
sendBuild, sign, and broadcast a payment

See Getting Started for full examples.

exfer script

Subcommands grouped by script pattern:

HTLC

  • htlc-lock
  • htlc-claim
  • htlc-reclaim

See HTLC.

Multisig

  • multisig2of2-lock, multisig2of2-spend
  • multisig1of2-lock, multisig1of2-spend
  • multisig2of3-lock, multisig2of3-spend

See Multisig.

Vault

  • vault-lock
  • vault-spend
  • vault-recover

See Vault.

Escrow

  • escrow-lock
  • escrow-release
  • escrow-arbitrate
  • escrow-reclaim

See Escrow.

Delegation

  • delegation-lock
  • delegation-owner-spend
  • delegation-delegate-spend

See Delegation.

Global conventions

  • --json is 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?

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 lockupWordmark
Just the symbol in copyMark only
Just the typographyText only
A press kit / overview sheetFull 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.

Exfer round token
1162 × 1162 — PNG, RGBA
Download exfer-token-1162.png

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.

Exfer square favicon
256 × 256 — PNG
Download favicon.png

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).

Exfer wordmark
872 × 265 — PNG, RGBA (transparent background)
Download exfer-wordmark.png

Mark only

Just the X glyph + corner squares, no surrounding circle. Use when text context already makes clear what project this is.

Exfer mark
264 × 265 — PNG, RGBA (transparent background)
Download exfer-mark.png

Text only

The EXFER wordmark typography by itself, no symbol. Pair with the mark for custom layouts.

EXFER text
533 × 79 — PNG, RGBA (transparent background)
Download exfer-text.png

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.

Exfer brand kit overview
1254 × 1254 — JPEG
Download exfer-brand-kit.jpg

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

When filing a bug, please include:

  • exfer --version
  • Exact command + flags
  • Full error message + log context (journalctl -u exfer is 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:

  1. Fork the repository.
  2. Edit the markdown under src/.
  3. Verify locally with mdbook build (and mdbook serve --open for live preview).
  4. 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.