Skip to content
snapshot

Configuration

Layered, validated config — defaults < file < env < per-call; nothing is hard-coded. The values shown are this node's REAL resolved defaults (serialized from the Rust GridConfig); the file / env / per-call layers override these at runtime.

Every setting the node runs with, and where each value comes from. Config is layered: a built-in default, overridden by a file, then environment variables, then a per-query override.

Impact: Operators tune behavior without touching code — nothing is hard-coded — and you can see exactly what is in effect right now.

Resolution order
Each layer overrides the one before it. The last writer wins, so a single call argument can trump the file and the environment.
  1. 1Built-in defaults

    Compiled-in, safe values — this is exactly the resolved layer shown below. The grid runs with zero config.

  2. 2p2p.toml file

    Operator file, found via P2P_CONFIG or --config. Overrides defaults.

  3. 3P2P_* environment

    Per-process overrides, ideal for secrets and containers.

  4. 4Per-call override

    Argument on a single call, e.g. a p2p_query() parameter.

Effective configuration
The full resolved GridConfig as this node sees it — 20 sections, serialized live from the Rust config crate. This snapshot is the default layer, so every value is its built-in default; the file, environment and per-call layers override these at runtime.

Example p2p.toml (documented)
The real, fully-commented p2p.example.toml shipped with the project. Every key is optional — omit it to inherit the documented default shown above. Unknown keys are a hard error (fail fast).
# =============================================================================
# duckdb-p2p — example node configuration
# =============================================================================
# Layered precedence (lowest -> highest):
#   1. built-in defaults (shown below)
#   2. this TOML file              (P2P_CONFIG=/path/to/p2p.toml, or pass --config)
#   3. environment variables       (P2P_* — see crates/config/src/lib.rs)
#   4. per-call SQL parameters      (args to p2p_query / p2p_share / p2p_join)
#
# Every value here is optional; omit a key to inherit the documented default.
# `deny_unknown_fields` is on, so a typo'd key is a hard error (fail fast).
# -----------------------------------------------------------------------------

[protocol]
# Protocol version this node advertises and the minimum it will accept.
# Compatibility: same MAJOR required; MINOR/PATCH may differ (newer side
# downgrades to the negotiated common version). Peers below min_supported_version
# or with a different major are cleanly rejected with a typed error.
version                         = "1.0.0"
min_supported_version           = "1.0.0"
# Require peers to report an identical DuckDB engine version to participate in a
# quorum (result-determinism). Off by default.
require_matching_engine_version = false

[network]
# QUIC bind address. Port 0 = OS-assigned ephemeral port (handy for tests).
bind_addr = "127.0.0.1:0"
# advertised_addr = "203.0.113.10:9494"   # externally reachable addr, if NAT'd
idle_timeout_ms       = 30000
connect_timeout_ms    = 10000
keepalive_ms          = 10000            # must be < idle_timeout_ms
max_concurrent_bidi_streams = 256        # backpressure: cap in-flight streams
stream_receive_window = 8388608          # 8 MiB per-stream flow-control window
receive_window        = 67108864         # 64 MiB per-connection window
result_chunk_bytes    = 262144           # 256 KiB result streaming chunk size

# -----------------------------------------------------------------------------
# Transport performance tuning (low latency + high throughput, all configurable)
# See ARCHITECTURE.md "Transport performance tuning" for the full rationale.
# -----------------------------------------------------------------------------
[transport.quic]
gso          = true                       # UDP Generic Segmentation Offload on TX
                                          # (biggest throughput lever; quinn-udp
                                          #  auto-disables if unsupported)
gro          = true                       # desire UDP Generic Receive Offload
                                          # (quinn-udp enables automatically; advisory)
congestion   = "cubic"                    # "bbr" | "cubic" | "newreno"
pacing       = true                       # NOTE: Quinn always paces; off = advisory
# stream_receive_window_bytes     = 8388608   # override [network].stream_receive_window
# connection_receive_window_bytes = 67108864  # override [network].receive_window
send_window_bytes           = 67108864    # 64 MiB connection send window
max_concurrent_uni_streams  = 256         # cap on bulk result fan-out streams
enable_0rtt                 = false       # 0-RTT/session resumption (see caveats)
session_ticket_lifetime_secs = 43200      # 12h resumption ticket lifetime
[transport.quic.bdp]
# Auto-size flow-control windows from bandwidth-delay product when enabled;
# overrides the explicit windows above so large results aren't window-limited.
enabled        = false
bandwidth_mbps = 1000                     # target link bandwidth (Mbit/s)
rtt_ms         = 50                        # target round-trip time (ms)

[transport.result]
parallelism       = 1                     # concurrent uni-streams per large result
                                          # (1 = inline; overridable per p2p_query call)
# chunk_bytes     = 262144                # override [network].result_chunk_bytes
parallel_min_bytes = 1048576              # only fan out results >= 1 MiB
max_result_bytes  = 2147483648            # reject a result manifest larger than 2 GiB
                                          # (untrusted winner; bypasses the frame cap —
                                          # clamped to p2p_proto::MAX_RESULT_BYTES = 8 GiB)
max_result_parts  = 1024                  # max parallel result streams accepted per result

[transport.compression]
algorithm     = "none"                    # "none" | "lz4" | "zstd"
                                          # default off on loopback/LAN; for WAN
                                          # prefer "lz4" (cheap) or "zstd" (ratio)
level         = 3                          # zstd level 1..=22 (lz4/none ignore)
min_size_bytes = 65536                    # only compress results >= 64 KiB

[identity]
# key_path = "/etc/duckdb-p2p/node.key"  # PKCS#8 Ed25519 PEM; omit = ephemeral
pinning_mode = "tofu"                     # "tofu" | "allowlist"
allowlist    = []                         # node ids trusted when pinning_mode="allowlist"
                                          # env: P2P_IDENTITY_PINNING_MODE,
                                          #      P2P_IDENTITY_ALLOWLIST (comma list),
                                          #      P2P_IDENTITY_KEY_PATH

# -----------------------------------------------------------------------------
# Closure posture — the single PRIVATE / ENTERPRISE switch (see docs/PRIVATE_MODE.md)
# -----------------------------------------------------------------------------
# "public" (default) = the zero-config grid: TOFU pinning allowed, an ungrouped
# host serves everyone, soft (declared) labels. "private" = a fully closed
# company grid: outsiders can neither connect/impersonate nor be served. Setting
# mode = "private" makes the node FAIL TO START unless ALL of the following hold
# (fail-closed — a misconfigured private node never leaks):
#   * identity.pinning_mode = "allowlist" with a NON-EMPTY identity.allowlist
#     (the member roster — outsiders are refused at the mTLS layer);
#   * membership.group_enforcement = "token" (cryptographic group proof, never a
#     soft declared label) with a membership.group_issuers entry per group;
#   * membership.networks set to an explicit, non-"default" name.
# It also turns on, at runtime: fail-closed discovery (drop unknown-labeled
# peers), require-grouped-host (an ungrouped host refuses to serve), and a
# default-deny requester roster (serve only identity.allowlist members). The
# application↔transport identity binding (an offer's requester_id must equal the
# authenticated mTLS peer) is ALWAYS on, in both modes (it is a correctness fix).
# env: P2P_SECURITY_MODE = public|private
[security]
mode = "public"                           # "public" | "private"

# -----------------------------------------------------------------------------
# Request-scoping / routing labels (architecture §7.5) — logical partition,
# groups, region. Default-off (serves the implicit "default" partition,
# ungrouped, no region) so a zero-config node is unconstrained. In PRIVATE mode
# these become the company's closed pool (see docs/PRIVATE_MODE.md).
# -----------------------------------------------------------------------------
[membership]
networks          = ["default"]           # logical grid partition(s); PRIVATE: set a
                                          #   non-"default" name. env: P2P_MEMBERSHIP_NETWORKS
groups            = []                     # group memberships served/claimed; empty =
                                          #   ungrouped/public. env: P2P_MEMBERSHIP_GROUPS
# region          = "eu"                   # data-residency hint. env: P2P_MEMBERSHIP_REGION
group_enforcement = "soft"                # "soft" (declared) | "token" (cryptographic
                                          #   proof). PRIVATE requires "token".
                                          #   env: P2P_MEMBERSHIP_GROUP_ENFORCEMENT
region_trust      = "declared"            # "declared" | "attested". env: P2P_MEMBERSHIP_REGION_TRUST
# group_token     = "<json CapabilityToken>"  # this node's OWN group proof, presented as a
                                          #   requester under the token tier.
                                          #   env: P2P_MEMBERSHIP_GROUP_TOKEN
# region_token    = "<json CapabilityToken>"  # this node's region-attestation proof (host side)
# Trusted group issuer pubkeys (group -> hex ed25519). Required per group under
# the token tier. env: P2P_MEMBERSHIP_GROUP_ISSUERS = "finance=<hex>,ops=<hex>"
# [membership.group_issuers]
# finance = "ab12...<64 hex>"
# [membership.region_issuers]                # trusted region issuers (region -> hex), attested tier
# eu = "cd34...<64 hex>"

[discovery]
mode = "static"                           # "static" (MVP) | "kademlia" (libp2p
                                          # Kademlia DHT + gossipsub; scales)
bootstrap = []                            # entry peers only; never in data path.
                                          # kademlia mode: libp2p multiaddrs incl.
                                          # peer id, e.g.
                                          # "/ip4/203.0.113.10/tcp/9595/p2p/12D3Koo..."
listen_addrs = []                         # libp2p listen multiaddrs for the
                                          # discovery overlay (distinct from the
                                          # QUIC data-plane network.bind_addr).
                                          # [] = ephemeral loopback TCP port.
                                          # e.g. ["/ip4/0.0.0.0/tcp/9595"]
candidate_sample_size = 16                # HARD cap on workers contacted per job
                                          # (keeps fan-out bounded at any swarm size)
[discovery.kademlia]
replication_factor = 20                   # k-bucket size
query_parallelism  = 3                    # alpha
record_ttl_secs    = 3600

[discovery.gossip]
topic               = "duckdb-p2p/caps/1" # gossipsub topic for signed cap ads
heartbeat_ms        = 5000                # capability-ad republish interval
fanout              = 6
capability_ttl_secs = 30                  # ads older than this are rejected

# -----------------------------------------------------------------------------
# Global NAT traversal (architecture §8 "Networking & NAT traversal")
# -----------------------------------------------------------------------------
# Lets two nodes behind home/office NATs on DIFFERENT networks worldwide connect
# DIRECTLY, with NO central server and NO fixed IP/URL. `identify` is always on;
# these gate the optional libp2p behaviours layered on the discovery overlay:
#   * autonat      — learn if we're publicly reachable + discover our external addr
#   * dcutr        — coordinated hole punching: upgrade a relayed link to a DIRECT
#                    one through NATs (works over QUIC/UDP). Requires relay_client.
#   * relay_client — Circuit Relay v2 client + AutoRelay: when hole punching fails
#                    (symmetric NAT), route through VOLUNTEER relay peers
#                    auto-selected from the network (never a central server).
#   * act_as_relay — volunteer THIS node as a Circuit Relay v2 server for others.
#   * mdns         — zero-config peer discovery on the same LAN.
# NOTE (law of distributed systems): at least one reachable entry point
# (discovery.bootstrap seed or a relay) is required to JOIN the swarm — but it is
# replaceable, owns nothing, and is never in the query data path.
[discovery.nat]
autonat            = true
dcutr              = true                 # requires relay_client = true
relay_client       = true
act_as_relay       = false                # opt in to volunteer as a relay
mdns               = true                 # LAN zero-config discovery
mdns_query_interval_secs = 300            # how often mDNS re-queries the LAN
external_addresses = []                   # e.g. ["/ip4/203.0.113.10/udp/9595/quic-v1"]
relays             = []                    # explicit relay multiaddrs (incl. /p2p/<id>);
                                          #  [] = AutoRelay picks relays from the network
max_relays         = 3                    # max simultaneous relay reservations (AutoRelay cap)

[discovery.nat.relay_limits]              # caps applied only when act_as_relay = true
max_reservations          = 128
max_reservations_per_peer = 4
reservation_duration_secs = 3600
max_circuits              = 16
max_circuits_per_peer     = 4
max_circuit_duration_secs = 120
max_circuit_bytes         = 131072        # 128 KiB

[scheduler]
replicas              = 3                  # k workers to race (>= quorum)
quorum                = 2                  # matching hashes required to accept
verify_mode           = "quorum"          # "fast" | "quorum"
offer_timeout_ms      = 2000
dispatch_timeout_ms   = 30000
# --- Resilience / re-dispatch (architecture §8/§11) ---
attempt_deadline_ms       = 60000          # requester per-attempt deadline; a silent
                                           #  attempt past this is inconclusive (job
                                           #  fault, NO provider penalty) -> re-dispatch
max_retries               = 0              # max (re)dispatch attempts; 0 = UNLIMITED
                                           #  (route around dead/timed-out nodes until done)
max_total_duration_ms     = 0              # optional wall-clock cap on the whole loop (0 = none)
backoff_initial_ms        = 200            # exponential backoff start
backoff_max_ms            = 5000           # backoff ceiling
backoff_jitter_frac       = 0.5            # jitter fraction [0,1] to de-sync retry storms
retry_budget_max_tokens   = 32             # global retry/hedge token bucket (burst cap; 0 = unlimited)
retry_budget_refill_per_sec = 4            # token refill rate (tokens/sec)
progress_interval_ms      = 2000           # requester's expected heartbeat interval
progress_stall_multiplier = 5              # stall if no progress within interval * this
max_inflight_jobs     = 64                 # requester-side concurrency semaphore

# -----------------------------------------------------------------------------
# Host (worker) execution deadline + progress/heartbeat streaming (resilience)
# -----------------------------------------------------------------------------
# The host abandons a job that exceeds job_timeout_ms (the requester then
# re-dispatches), and streams a progress/heartbeat update every
# progress_interval_ms while executing — the progress update IS the liveness
# signal the requester watches for stalls.
[worker]
job_timeout_ms      = 60000                # host execution deadline (0 = none)
progress_interval_ms = 2000                # how often the host streams progress (0 = off)

# -----------------------------------------------------------------------------
# Liveness / failure detection (architecture §8): phi-accrual + SWIM
# -----------------------------------------------------------------------------
# A phi-accrual failure detector over heartbeat/gossip intervals plus SWIM-style
# indirect probing, layered on the libp2p gossip overlay. Unhealthy/suspect peers
# are excluded from candidate selection. Off-path until a liveness view is wired
# into the coordinator, so a node with no liveness wiring behaves as before.
[liveness.phi]
enabled            = true
convict_threshold  = 8.0                   # phi at/above which a peer is convicted (~8-12)
window_size        = 100                   # sliding window of recent heartbeat intervals
min_std_ms         = 50.0                  # floor on interval std-dev (avoid over-confidence)
acceptable_pause_ms = 0.0                  # extra slack added to the mean interval
first_interval_ms  = 5000.0                # bootstrap interval before enough samples

[liveness.swim]
enabled                   = true
indirect_probe_count      = 3              # k random peers asked to indirect-probe a suspect
probe_timeout_ms          = 1000           # direct-probe timeout
indirect_probe_timeout_ms = 2000           # each indirect (relayed) probe timeout

[budget]
memory_bytes         = 4294967296          # 4 GiB donated
threads              = 2
max_jobs             = 3
per_job_memory_bytes = 1073741824          # 1 GiB default per-job lease
per_job_threads      = 1
data_classes         = ["public"]          # public | internal | sensitive

[trust]
min_trust                 = 0.7            # soft score gate [0,1]
min_attestation           = "L0"           # hard gate: L0 | L1 | L2
reputation_half_life_secs = 604800         # 7 days recency half-life
canary_rate               = 0.05           # fraction of jobs that are canaries
incorrect_penalty         = 0.5
bootstrap_trust           = 0.1            # trust for brand-new identities
# store_path              = "/var/lib/duckdb-p2p/trust.redb"  # persist reputation
                                            # across restarts (embedded redb).
                                            # omit = bounded in-memory (default)

[trust.weights]                            # effective_trust soft-score weights
alpha_reputation = 0.7
beta_age         = 0.1
gamma_voucher    = 0.1
delta_stake      = 0.1

[sybil]
pow_difficulty_bits = 16                   # leading zero bits for identity PoW
min_stake           = 0
vouch_weight        = 0.05

[storage]
provider            = "local-fake"         # default credential provider: local-fake | s3 | az | gcs
                                           # (use "s3" for AWS *and* self-hosted MinIO / S3-compatible)
# endpoint          = "minio.local:9000"   # S3-compatible / MinIO host:port (host only; scheme via use_ssl)
# region            = "us-east-1"          # MinIO accepts any region; must be set
# url_style         = "path"               # "path" for MinIO/most self-hosted, "vhost" for AWS
# use_ssl           = false                # MinIO dev is often plain HTTP; true in production
credential_ttl_secs = 900                  # scoped credential lifetime
key_ttl_secs        = 900                  # sealed data-key lifetime

# --- Data sources & formats (architecture §4 / §9.2 / §9.4) ------------------
# Master switch: allow worker NETWORK EGRESS for remote object-storage reads.
# When false (default) the execution engine stays in the strict/local lockdown
# (enable_external_access=false). DuckDB's enable_external_access is all-or-
# nothing for the network — it CANNOT restrict egress to specific endpoints — so
# enabling remote access REQUIRES complementary OS-level egress filtering (the
# OS sandbox is separately deferred; see §9.4).
enable_remote_access = false
# Fail engine init if a preload_extensions entry can't be loaded. Required
# extensions are pre-loaded at init, NEVER INSTALL/LOAD'd at query time.
require_extensions   = true
# DuckDB extensions pre-loaded at engine init. For real cloud reads enable e.g.
# ["httpfs", "aws", "azure", "parquet", "json", "delta", "iceberg"]. (parquet
# and json are also statically linked in the bundled build.)
preload_extensions   = []
# Formats workers will serve (extensible; unknown values tolerated).
enabled_formats      = ["csv", "json", "parquet"]
# Storage/credential providers enabled on this node.
enabled_providers    = ["local-fake"]
# Local directories DuckDB may read even with external access disabled
# (DuckDB `allowed_directories`); used for local fixtures. e.g. ["/srv/data"].
allowed_local_paths  = []
# Per-provider option overrides (endpoint/region/url_style/use_ssl/...), keyed
# by id. These are NON-SECRET connection knobs only — the access key / secret
# are NEVER placed here; they are delivered per job, sealed (encrypted) to the
# worker (see "Encrypted credentials" below).
# [storage.provider_options.s3]            # AWS S3
# region    = "us-east-1"
# endpoint  = "s3.amazonaws.com"
# Self-hosted MinIO / S3-compatible (reads Delta/Parquet/CSV/JSON/Iceberg):
# [storage.provider_options.s3]
# endpoint  = "minio.local:9000"
# url_style = "path"                        # MinIO requires path-style addressing
# use_ssl   = false                         # plain HTTP for a local MinIO; true with TLS
# region    = "us-east-1"
# [storage.provider_options.gcs]
# endpoint  = "storage.googleapis.com"
#
# Encrypted credentials (the MinIO access key / secret) — NEVER in config:
#   * Requester side: put the key/secret in a CloudCredential, seal it to the
#     selected worker's X25519 sealing public key (from its attestation /
#     capability record) with `p2p_node::sealed_credential("s3", pubkey, &cred,
#     "bucket/prefix/", ttl)`. The token is `sealed:v1:<hex>` — ciphertext only.
#   * Worker side: the engine opens it just-in-time at setup with its sealing
#     key (StorageSetup::with_sealing) and mints a prefix-scoped CREATE SECRET.
#     Plaintext never persists; at rest only 0600 files / sealed-to-worker blobs.
# Per-format reader options, keyed by format id:
# [storage.format_options.parquet]
# hive_partitioning = "true"

# -----------------------------------------------------------------------------
# Local-first execution planner (architecture §4 data plane / §11 scheduler)
# -----------------------------------------------------------------------------
# A node can run a query entirely in its OWN locked-down in-process DuckDB for
# FREE — no bidding, escrow, quorum or payment — because it trusts its own
# machine. This section governs WHEN that free local path is chosen over a grid
# dispatch. A pre-flight, metadata-only data-size estimate (Parquet footers,
# Delta _delta_log stats, Iceberg manifests, CSV/JSON object size + sampling) is
# translated into an estimated PEAK WORKING-SET memory and compared against the
# node's CURRENT headroom (budget.memory_bytes * ram_fraction minus memory in
# use by concurrent local jobs). A job that blows past the threshold mid-flight
# is aborted and re-dispatched to the grid (adaptive fail-over).
[planner]
enabled                   = true           # false = always dispatch to the grid
local_execution_enabled   = true           # false = REMOTE-ONLY mode: never run a
                                            # query on this machine (even tiny ones);
                                            # always route to the grid. Hard gate that
                                            # overrides `prefer` (incl. per-call
                                            # prefer => 'local'). Thin-client friendly:
                                            # a node that never called p2p_share works
                                            # as a pure requester. No hosts => clear
                                            # NoCandidates error (no local fallback).
prefer                    = "auto"          # default routing: local | remote | auto
                                            # (per call: p2p_query(..., prefer => 'local'))
                                            # set "remote" for a sticky prefer-the-grid default
ram_fraction              = 0.6             # alpha: fraction of budget.memory_bytes
                                            # usable locally (headroom basis)
max_concurrent_local_jobs = 4               # local saturation -> route to grid
size_threshold_bytes      = 268435456       # 256 MiB hard cap on local scanned bytes
spill_tolerance_bytes     = 536870912       # 512 MiB the peak working set may exceed
                                            # RAM headroom via out-of-core spill (0=never)
max_local_latency_ms      = 10000           # latency budget for the local path (0=off)

# -----------------------------------------------------------------------------
# OS-level execution sandbox (architecture §9.4)
# -----------------------------------------------------------------------------
# The boundary AROUND job execution, complementing DuckDB's own lockdown
# (enable_external_access / lock_configuration / allowed_directories / ephemeral
# temp). DuckDB CANNOT scope network egress to specific endpoints — the OS must.
# SECURE BY DEFAULT for the HOST serving path: enabled + process_per_job default
# ON, so a node that serves OTHER peers' jobs runs them OS-sandboxed in a child
# process. This does NOT affect the requester's OWN local self-run path (it runs
# in-process and needs no protection from yourself). backend = auto degrades to a
# no-op on platforms with no OS sandbox, so a node never crashes; where OS
# confinement can't be applied the host fails SAFE (keeps the DuckDB SQL lockdown,
# refuses to serve remote-access jobs unconfined).
[sandbox]
enabled        = true                      # master switch (false = no-op sandbox)
backend        = "auto"                    # auto | none | rlimit | cgroups-seccomp |
                                           #  macos-seatbelt | windows-jobobject |
                                           #  android | ios
                                           #  auto (OS-agnostic): Linux->cgroups+seccomp,
                                           #  macOS->Seatbelt, Windows->Job Objects,
                                           #  Android->app sandbox+seccomp, iOS->app
                                           #  sandbox (in-process only), else rlimit
process_per_job = true                     # run each FOREIGN job in an OS-sandboxed
                                           #  child (needs P2P_JOB_EXEC; else falls
                                           #  back to in-process under the lockdown)
egress_mode    = "inherit_storage"         # inherit_storage | explicit
                                           #  inherit_storage derives the allow-list
                                           #  from [storage] endpoints/providers
egress_allowlist = []                      # extra host[:port] entries (appended in
                                           #  inherit_storage mode; the whole list
                                           #  in explicit mode)
temp_dir_policy = "ephemeral"              # ephemeral | inherit | custom
# temp_dir      = "/var/tmp/duckdb-p2p"    # REQUIRED when temp_dir_policy = "custom"

[sandbox.limits]
# A field set to 0 = unlimited / inherit (no cap installed for that resource).
mode                = "inherit_budget"     # inherit_budget | explicit
                                           #  inherit_budget: RAM/threads from [budget]
memory_bytes        = 0                    # RLIMIT_AS (explicit mode only)
cpu_seconds         = 0                    # RLIMIT_CPU wall of CPU time (0 = off)
max_file_size_bytes = 0                    # RLIMIT_FSIZE max created-file size (0 = off)
max_open_files      = 0                    # RLIMIT_NOFILE fd cap (0 = inherit)
max_processes       = 0                    # RLIMIT_NPROC cap (explicit mode only)

[limits]
receipt_cache_per_worker = 256             # bounded reputation history per worker
trust_store_capacity     = 100000          # LRU-evicted workers
peer_cache_capacity      = 50000           # LRU-evicted peers
worker_pool_size         = 8               # inbound job concurrency semaphore
connection_pool_size     = 1024

# -----------------------------------------------------------------------------
# Blockchain economic / settlement layer (TON) — see docs/BLOCKCHAIN_ECONOMICS.md
# -----------------------------------------------------------------------------
# OFF by default: with enabled = false the node behaves exactly as today — every
# job is FREE, touches NO chain (no escrow/stake/anchor/fees) — yet is STILL
# scored (quorum/canary verification + signed receipts + reputation). Settlement
# and scoring are decoupled: turning the chain off never turns scoring off.
#
# Per-job payment mode resolves (highest precedence first):
#   1. per-call SQL override   p2p_query(..., payment => 'free'|'paid'|'auto')
#   2. data-class policy        ('auto' => public:free, internal/sensitive:paid)
#   3. economics.default_payment (below)
#   4. economics.enabled        (false => always free, no chain)
[economics]
enabled           = false                  # master switch (false = free, no chain)
settlement        = "noop"                 # "noop" | "mock" | "channel" | "onchain" (SQL: noop|mock|ton)
custody           = "noncustodial"         # v1: code-governed contracts, no platform wallet
accounting_unit   = "ton"                  # v1: price AND settle natively in TON (no oracle)
chain             = "ton"
network           = "testnet"              # "testnet" (safe default) | "mainnet" (real funds)
mainnet_confirmed = false                  # explicit opt-in required to use mainnet
default_payment   = "auto"                 # free | paid | auto (per-call overridable)
# fee_recipient = "EQ...treasury"          # platform fee-recipient (REQUIRED once enabled = true
                                           # AND settlement = "channel"|"onchain")

# Per-network settings — keep BOTH networks configured at once; the active one is
# selected by `network`. Manage these via SQL: p2p_contracts(...) / p2p_wallet(...).
# RPC/explorer default per network (testnet -> testnet.toncenter.com / testnet.tonviewer.com;
# mainnet -> toncenter.com / tonviewer.com) and are overridable here.
# [economics.testnet]
# rpc          = "https://testnet.toncenter.com/api/v2/"
# explorer     = "testnet.tonviewer.com"
# api_key_file = "/path/outside/repo/testnet.api_key"   # 0600 file ref, never the raw key
# [economics.testnet.contracts]
# stake_vault   = "kQ..."
# job_escrow    = "kQ..."
# record_anchor = "kQ..."
# global_params = "kQ..."   # platform-wide params (§12); address is STABLE (edited in place)
# [economics.testnet.wallet]
# address       = "kQ..."
# mnemonic_file = "/path/outside/repo/testnet.mnemonic" # 0600 file ref, never the raw mnemonic
# [economics.mainnet] … same shape …

# Pricing (role-appropriate): providers advertise unit_price; requesters cap with max_bid.
[economics.pricing]
unit_price = 0                             # whole TON per reference unit (0 = free)
max_bid    = 0                             # whole TON budget cap (0 = no cap)

[economics.stake]
min_stake               = 0                # 0 = permissionless/free tier (public only)
min_stake_internal      = 100              # per-class minimums (whole TON)
min_stake_sensitive     = 1000
stake_cap               = 100000          # ranking ceiling (anti-centralization)
unbonding_secs          = 604800          # MUST be >= slashing.challenge_window_secs
receipt_jetton          = true            # mint 1:1 TEP-74 stake-receipt jetton (§8.5)
receipt_transfer_locked = true            # receipt non-transferable while bonded (anti-exit)

[economics.ranking]
w_quality = 0.6
w_stake   = 0.30                           # diminishing + capped (StakeFactor, §5.2); doubled so
                                           # stake has real pull — safe because it is reliability-gated
w_price   = 0.25
stake_reliability_floor = 0.5              # stake earns ranking credit only as a node's verified-success
                                           # rate clears this floor (0 below it, ramps to full at 1.0), so
                                           # stake amplifies reliable nodes and never rescues bad ones
exploration_rate       = 0.0               # cold-start ε (§5.2/§6): 0 = pure exploitation;
                                           # a small value (e.g. 0.1) samples new honest nodes
exploration_saturation = 20                # obs count at which the exploration bonus hits 0

[economics.quality]                        # provider quality score Q weights (§4.1)
w_success      = 0.5
w_latency      = 0.2                        # size-normalized latency (allowance scales with bytes)
w_throughput   = 0.2                        # throughput-as-rate (bytes/ms), log-scaled
w_completion   = 0.1
latency_ref_ms = 5000
bytes_ref      = 1073741824                # 1 GiB reference for log throughput scaling
throughput_ref_bytes_per_ms = 0            # 0 => derive ref rate from bytes_ref / latency_ref_ms

[economics.reputation]                     # confidence-aware reputation priors (§4.1/§7.3)
prior_alpha  = 1.0                         # Beta pseudo-successes
prior_beta   = 2.0                         # Beta pseudo-failures (pessimistic about thin history)
confidence_z = 1.96                        # Wilson lower-bound z (≈95%); 0 = posterior mean

[economics.selection]
n_public                = 3                # default N for public/low-value jobs
n_default               = 5                # default N for internal/high-value jobs
n_max                   = 10               # ceiling on requester overrides
requester_overridable   = true
checksum_min            = 3                # min matching results to accept (safety floor)
checksum_allow_degraded = true            # if < checksum_min respond, proceed + flag

[economics.fees]
platform_fee_pct              = 0.15       # phi: platform/admin cut (15%)
verification_surcharge_pct    = 0.05
participation_commission_frac = 0.05       # kappa: fixed cut of winner payout per agreeing node (5%)
bonus_aggressiveness          = 0.5        # rho: escrow slack funding the perf bonus
lambda_quality                = 0.5        # blend of quality vs speed in the bonus
lambda_speed                  = 0.5

[economics.slashing]
slash_wrong_result_pct = 0.15
slash_cheat_pct        = 1.0
slash_downtime_pct     = 0.02
slash_equivocation_pct = 0.5
slash_failed_commitment_pct = 0.1          # FINE for a broken commitment: accepted a PAID
                                           #  job then failed to deliver a valid result while
                                           #  the job was feasible (quorum reached / another
                                           #  node delivered). Distinct from a wrong-result slash.
challenge_window_secs  = 86400             # optimistic-settlement window
slash_to_challenger    = 0.4               # split of slashed funds (must sum to 1.0)
slash_to_redundancy    = 0.3
slash_to_burn          = 0.2
slash_to_treasury      = 0.1

[economics.records]
epoch_secs        = 60                      # how often the receipt Merkle root is anchored
anchor_quorum_pct = 0.66                    # stake-weighted signers needed to accept a root

# -----------------------------------------------------------------------------
# Anti-abuse / robustness layer (architecture "Abuse resistance")
# -----------------------------------------------------------------------------
# Defends the grid against reputation-griefing and other abuse. Defaults preserve
# today's behavior where a change would be observable: the scoring-altering pieces
# (requester-trust weighting, pre-flight cost gating, free-mode rate limiting,
# auto-blocking, gossip peer scoring) default OFF; only the always-safe pieces
# (provable-fault attribution and non-determinism detection, which never ADD a
# penalty) default ON. Deny-lists are managed via SQL: p2p_block / p2p_unblock /
# p2p_blocklist (persisted to blocklist.toml).
[antiabuse]
enabled = true                             # master switch (false = disables every sub-mechanism)

[antiabuse.fault_attribution]
# Penalize a provider ONLY for provable PROVIDER fault (result disagrees with a
# verified quorum, downtime, equivocation). Requester/job-caused failures
# (infeasible / too expensive / resource-exceeded / malformed / missing data)
# apply ZERO provider penalty. If >= job_consensus_fraction of the selected
# providers fail the SAME way with no quorum, blame the JOB, not the providers.
enabled               = true
job_consensus_fraction = 0.67

[antiabuse.requester_trust]
# Trust-weighted score impact ("newer sender -> less effect"): a job's effect on
# a provider's score is multiplied by w(requester) in (0,1] from the requester's
# own reputation + age. New/unproven requester => w ≈ 0 (esp. for negatives);
# established => w -> 1. Asymmetric (gates negatives hardest). Primary defense
# against the heavy-query reputation-griefing attack. OFF by default (when off,
# every requester has weight 1.0 — today's behavior).
enabled               = false
negative_floor_weight = 0.0                # floor weight for a brand-new requester's PENALTY
positive_floor_weight = 0.5                # floor weight for its reputation CREDIT (asymmetric)
age_saturation        = 50                 # requester observations at which weight saturates to 1.0

[antiabuse.cost_gate]
# Pre-flight cost gating at admission: the worker rejects an over-budget query up
# front (a rejection is NOT an execution failure and never affects score). OFF by
# default.
enabled                = false
max_cost_hint_rows     = 0                 # reject offers with cost_hint_rows above this (0 = no cap)
max_working_set_factor = 1.0               # reject when est. peak working set > per-job mem * factor

[antiabuse.nondeterminism]
# Detect non-deterministic queries (random(), now()/current_*, unordered LIMIT)
# that can't reach a stable quorum hash, mark the job non-verifiable, and apply
# no provider penalty. ON by default (deterministic queries are unaffected).
enabled = true

[antiabuse.free_rate_limit]
# Per-requester-identity rate limiting for FREE jobs (anti-spam). Paid jobs are
# prioritized and bypass this limiter. Optional small PoW for free requesters.
# OFF by default.
enabled               = false
max_free_per_window   = 30
window_secs           = 60
max_tracked_requesters = 10000             # bounded, LRU-evicted
require_pow_bits      = 0                   # optional PoW difficulty for free requesters (0 = none)
prioritize_paid       = true

[antiabuse.blocklist]
# Local deny-list policy. Entries are managed via SQL (p2p_block/p2p_unblock/
# p2p_blocklist) and persisted to blocklist.toml. This governs the AUTOMATIC
# triggers and which external sources are honored. Each node decides on its own —
# no central authority.
auto_block_enabled     = false             # auto-block a worker below the trust floor
auto_block_trust_floor = 0.0               # trust floor for auto-blocking (0 = disabled)
honor_gossip_signals   = false             # act on signed, gossiped abuse signals
honor_global_params    = false             # consult the on-chain GlobalParams governance blocklist

[antiabuse.gossip]
# Eclipse / gossip hardening for the libp2p discovery overlay.
peer_scoring      = false                  # gossipsub peer scoring (penalize misbehaving peers)
diverse_bootstrap = true                   # prefer a diverse bootstrap/relay set (anti-eclipse)
SQL admin surface
The whole grid is driven from SQL — these are the business-user entry points. Zero-config quickstart: install the extension and the calls below work out-of-the-box against the built-in defaults; no file or environment setup is required to run your first query.
StatementWhat it does
SELECT * FROM p2p_info();Node identity, version, and connected peer count — the health check.
SELECT * FROM p2p_query('SELECT …', data_class:='internal');Run a query across the grid, tagging the data sensitivity class.
CALL p2p_share('view_name', 's3://…');Publish a named logical view backed by an object-store path.
SELECT * FROM p2p_join(…);Distributed join across remote shared datasets.
CALL p2p_set('transport.compression', 'zstd');Override a config key at runtime (call-layer precedence).
CALL p2p_network('testnet');Switch the settlement network; mainnet requires explicit opt-in.
Network mode
testnet
Which TON network settlement transactions are signed against. Read live from economics.network.
testnetcurrent

Play money. Stakes, escrow and slashing all run, but no real-value funds are ever at risk. The default for development.

mainnet opt-in

Real funds. Disabled unless explicitly enabled, so a stray config can never accidentally move value.

Switching to mainnet requires an explicit opt-in — set network = "mainnet" and flip mainnet_confirmed = true (now false). There is no implicit upgrade path from testnet.

Pluggable trait seams
Extensibility points — implement the trait to add a backend.
  • DataFormatMaps a format to the DuckDB extensions it needs (parquet, delta, iceberg, …) and loads them on demand.
  • StorageProviderTurns a ScopedCredential into a CREATE SECRET. Impls: S3, Azure, Gcs, Https, Local.
  • query engineSwap mock ↔ a locked-down DuckDB executor, feature-gated at build time.
  • settlementPluggable economics backend: noop, mock, or the on-chain TON layer.

Notes

config

Every key above is validated on load; an unknown key or out-of-range value fails fast at startup rather than silently degrading at runtime.