Repository navigation
Expand file tree
/
Copy pathconfig.example.yaml
More file actions
436 lines (415 loc) · 23 KB
/
Copy pathconfig.example.yaml
File metadata and controls
436 lines (415 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# Arcade configuration
# Copy to config.yaml and adjust for your environment.
# All values can be overridden with ARCADE_ prefixed env vars
# (e.g. ARCADE_LOG_LEVEL, ARCADE_KAFKA_BROKERS).
# Service mode: all, api-server, bump-builder, propagation
mode: all
log_level: info
# Bitcoin network everything in arcade attaches to. One of:
# mainnet — production Bitcoin SV
# testnet — public testnet
# teratestnet — teranode scaling testnet
# regtest — local/private network; requires operator-supplied
# p2p.bootstrap_peers when datahub_discovery is on, and
# auto-disables chaintracks_server (no regtest genesis header).
# Both the libp2p datahub discovery client and the embedded chaintracks
# instance derive their pubsub topic and bootstrap peers from this value.
network: mainnet
# callback_url: "https://example.com/callback"
# Bearer token the Merkle Service must present in the Authorization header on
# every POST /api/v1/merkle-service/callback. REQUIRED when merkle_service.url
# is set: arcade refuses to start otherwise, since an unset token would let any
# unauthenticated caller submit forged status updates for any txid. Generate a
# high-entropy random value (e.g. `openssl rand -hex 32`) and configure the
# matching token on the Merkle Service. See issue #76 / finding F-018.
# callback_token: "your-callback-token"
api:
host: 0.0.0.0
port: 8080
# Arcade does not create Kafka topics; the broker must already have them
# (auto-create on first publish is typically disabled in production, and
# arcade.propagation must exist with at least propagation.partitions
# partitions — arcade fails closed at startup otherwise). See
# docs/production-kafka.md for the full topic list, partition guidance,
# and ready-to-paste rpk / kafka-topics.sh commands.
kafka:
brokers:
- localhost:9092
consumer_group: arcade
max_retries: 5
buffer_size: 10000
# Minimum partitions required on hot-path topics (transaction, propagation).
# Set to the replica count of the largest horizontally-scaled consumer when
# running in Kubernetes so arcade fails fast on misconfigured topics. Leave
# at 0 for standalone / single-replica deployments.
# min_partitions: 0
# Persistence backend. `backend` dispatches construction in storefactory.New;
# only the matching sub-block is read, so unused sections (pebble, postgres,
# mongodb) can be left out entirely.
store:
backend: aerospike
aerospike:
# One or more seed hosts. Port is optional and defaults to 3000.
hosts:
- localhost:3000
# Aerospike namespace arcade writes into. Must exist on the cluster with
# enough storage for arcade_transactions, arcade_bumps, arcade_stumps,
# arcade_submissions, arcade_leases, and arcade_datahub_endpoints sets.
namespace: arcade
# Max records per BatchGet/BatchWrite call. Larger batches reduce RTT
# overhead but raise per-op latency and memory pressure.
batch_size: 500
# Per-node connection pool size (ConnectionQueueSize). Tune to roughly
# the peak concurrent goroutines hitting the store.
pool_size: 256
# Total deadline for scan/query operations (e.g. GetStatusesSince).
query_timeout_ms: 8000
# Total deadline for single-record reads/writes and batch operations.
op_timeout_ms: 3000
# Per-attempt socket timeout. Should be <= op_timeout_ms; the client
# may retry on socket timeout up to the op deadline.
socket_timeout_ms: 5000
# Alternative: external PostgreSQL. Only read when backend: postgres.
# postgres:
# dsn: postgres://user:pass@host:5432/arcade
# max_conns: 16
# # Ceiling for the boot-time schema apply when it actually has to run
# # (fresh database or schema change). Restarts with a current schema
# # verify a stored checksum and skip DDL entirely, so they boot
# # instantly even under heavy DB load. See issue #278.
# schema_apply_timeout_ms: 300000
# Alternative: MongoDB. Only read when backend: mongodb. A standalone
# mongod is sufficient (no multi-document transactions are used); a
# replica set is only needed for HA. BUMP/STUMP payloads live in GridFS.
# Reads must reach a PRIMARY: this backend's guards (status lattice,
# still-anchored-to-this-block, manifest-versus-file) are read-then-write,
# so a readPreference of secondary/secondaryPreferred in the URI can let
# them pass on stale data — losing status transitions and deleting blobs
# under concurrency. Arcade warns at startup if the URI asks for one.
# mongodb:
# uri: mongodb://localhost:27017
# database: arcade
# # Connect + server-selection deadline for the initial handshake.
# connect_timeout_ms: 10000
# # Deadline for single-document reads/writes and counts.
# op_timeout_ms: 3000
# # Deadline for limit-bounded queries: lists, pages, bulk-write chunks.
# # Anything whose size scales with the data (iterators, per-block txid
# # scans, the census aggregate) runs under the caller's context instead.
# query_timeout_ms: 8000
# # Deadline for EnsureIndexes at boot; building indexes on a restored,
# # populated collection can take minutes (cf. postgres
# # schema_apply_timeout_ms).
# index_timeout_ms: 300000
# max_pool_size: 64
# # Documents per bulk-write chunk and per $in on the write paths (max
# # 10000; read-path $in chunks are a fixed 1000).
# batch_size: 500
# datahub_urls:
# - "https://datahub1.example.com"
# - "https://datahub2.example.com"
teranode:
auth_token: ""
merkle_service:
# Merkle Service base URL. Leaving this empty disables the Merkle integration
# (standalone profiles). When set, the top-level `callback_token` becomes
# REQUIRED — arcade fails to start otherwise so that the inbound callback
# endpoint never runs unauthenticated. See issue #76 / finding F-018.
url: ""
auth_token: ""
p2p:
seeds: []
# Peer discovery via go-teranode-p2p-client. When enabled, arcade subscribes
# to the canonical node_status topic (e.g. teranode/bitcoin/1.0.0/mainnet-node_status)
# and appends each peer's advertised datahub URL to the propagation endpoint
# list. Statically configured `datahub_urls` remain first in the list;
# discovered URLs are additive and deduplicated (including trailing-slash
# variants). Network and its bootstrap peers are derived from the top-level
# `network` value above; bootstrap_peers below only needs to be set to
# override or extend the built-in defaults (e.g. a private network).
# datahub_discovery: false
# listen_port: 9905
# dht_mode: off # "server", "client", or "off" (library default is "off")
# storage_path: "" # defaults to <top-level storage_path>/p2p; stores p2p_key.hex for stable peer ID
# bootstrap_peers: [] # override the built-in defaults; also feeds chaintracks
# (unless chaintracks.p2p.msgbus.bootstrappeers is set explicitly)
# enable_mdns: false
# allow_private_urls: false # accept discovered URLs on loopback/RFC1918/link-local/CGNAT
# addresses, including DNS names that RESOLVE to those ranges. Unresolvable
# hostnames (e.g. a peer announcing its cluster-internal service name) are
# rejected regardless. Statically configured datahub_urls are never validated.
health:
port: 8081
# OpenTelemetry export to an external OTLP collector (traces + metrics; the
# existing Prometheus /metrics scrape keeps working unchanged either way).
# Disabled by default: with enabled=false, no OTEL providers are created and
# no network connections are opened, so runtime behaviour is identical to a
# build with no OTEL support at all.
#
# endpoint may be left blank when enabled=true: the exporters then fall back
# to the standard OTEL_EXPORTER_OTLP_ENDPOINT (or the more specific
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
# environment variables, e.g. for a node-local collector reached via the
# Kubernetes downward API host IP:
# OTEL_EXPORTER_OTLP_ENDPOINT=http://$(HOST_IP):4317
# telemetry:
# enabled: false
# endpoint: "" # host:port of the OTLP collector; see env fallback above
# protocol: grpc # "grpc" (default, port 4317) or "http" (port 4318)
# insecure: false # skip TLS; typically true for an in-cluster/local collector
# service_name: arcade
# namespace: "" # service.namespace resource attribute (e.g. downstream solution name)
# traces: true
# metrics: true
# logs: false # OTLP log export is not implemented; logs always ship via stdout JSON
# sample_ratio: 1.0 # ParentBased(TraceIDRatioBased) sampler ratio, 0.0-1.0
# export_timeout_ms: 10000
# Intake-time transaction validator. Also backs the ARC-compatible
# GET /policy endpoint (issue #212): the size/sigop limits below are what
# intake enforces and what /policy advertises.
# validator:
# # min_fee_per_kb is the intake fee floor in satoshis/kB. Default 0 means
# # "not operator-configured": arcade then dynamically tracks the lowest fee
# # the network will accept — the minimum advertised across peer node_status
# # announcements — and BOTH enforces it at intake and advertises it via
# # GET /policy (issue #212). Until a peer is heard it uses the built-in
# # 100 sat/kB floor. Set a non-zero value to pin and enforce an exact floor.
# # An observed fee is floored at 1 sat/kB — peer announcements are
# # unauthenticated, so one peer advertising 0 must not be able to disable
# # fee enforcement for the whole instance. Only accept_zero_fee below can
# # produce a 0 floor. Only peers whose datahub URL is registered count
# # toward the observation, so /policy's inputs are the endpoints /health
# # shows.
# min_fee_per_kb: 0
# # accept_zero_fee, when true, pins the floor to 0 sat/kB so any tx
# # (including fee=0) is accepted at intake, and /policy advertises 0. This
# # is the ONLY route to a zero floor; network observation cannot reach it.
# # Intended for teratestnet/regtest where the upstream Teranode is also
# # configured to accept zero-fee txs. Leave false on mainnet and testnet.
# # accept_zero_fee: false
# # GET /policy size limits, in bytes. Default 0 means "not
# # operator-configured", exactly as for min_fee_per_kb: arcade then tracks
# # the LARGEST limit any peer advertises in its node_status fee_policy, so a
# # transaction is rejected at intake only when no node on the network would
# # accept it. Observation can only widen the limit from the built-in default
# # shown below, never narrow it, so one misconfigured peer cannot cause false
# # rejections. Set a non-zero value to pin and enforce an exact limit.
# # max_tx_size_policy: 0 # built-in default until a peer is heard: 10485760
# # max_script_size_policy: 0 # built-in default until a peer is heard: 500000
# # max_tx_sigops_counts_policy is NOT tracked from the network: 0 already
# # means unlimited here (the BSV post-Genesis default), so it cannot double
# # as an "unset" sentinel.
# # max_tx_sigops_counts_policy: 0
# # standard_format_supported: true
# # observed_fee_ttl_ms bounds how long a peer's node_status-advertised policy
# # stays eligible for the network computation (default 15 min). A peer not
# # re-heard within this window is dropped.
# # observed_fee_ttl_ms: 900000
# # observed_fee_refresh_ms is how often the api-server recomputes the
# # network-derived policy and updates intake (default 30 s). Applies to every
# # dimension left unpinned; a pinned dimension is left alone.
# # observed_fee_refresh_ms: 30000
# # disable_finality_check turns off the nLockTime/BIP113 intake gate
# # that rejects provably non-final txs with an actionable reason
# # (issue #245). The gate needs a chain source: the embedded
# # chaintracks (mode=all) or chaintracks.mode=remote pointed at the
# # chaintracks pod (note: the URL must include the /chaintracks path
# # prefix). Without one it silently fails open, so this switch is only
# # an emergency escape hatch.
# # disable_finality_check: false
# SSRF guard for client-supplied callback URLs. The api-server rejects
# X-CallbackUrl values whose host is loopback / link-local / RFC1918 /
# cloud-metadata, and the webhook delivery client refuses to dial those
# IPs at connection time (catches DNS rebinding). Set
# allow_private_ips: true if your deployment intentionally posts
# callbacks to internal services (testing, k8s service DNS, intranet
# webhooks). See finding F-017 / issue #75.
#
# max_body_bytes caps the size of an inbound POST
# /api/v1/merkle-service/callback request body (JSON, with embedded STUMP
# payload). Default 16 MiB is generous over a realistic STUMP delivery
# while bounding worst-case memory if a peer is malicious or malfunctioning.
# Oversize requests are rejected with 413 Payload Too Large. See finding
# F-019 / issue #77.
# callback:
# allow_private_ips: false
# max_body_bytes: 16777216 # 16 MiB
# events tunes the in-process status-update fan-out. subscriber_buffer is
# the channel capacity each Subscribe call mints (one per consumer — SSE
# manager and webhook delivery). When a consumer can't drain fast enough
# the publisher logs "subscriber channel full, dropping update" and drops.
# Default 4096 absorbs typical bursts; raise up to ~65536 if you see drops.
# Going beyond ~65536 just delays the drop rather than addressing the
# slow-consumer root cause. Memory cost is roughly
# subscribers × subscriber_buffer × ~500 bytes per slot.
# Subscribers are live-only: their consumer groups start at the topic head,
# and history missed while a pod was down recovers via store-backed paths
# (SSE Last-Event-ID catchup, webhook retry reaper), not Kafka replay.
# events:
# subscriber_buffer: 4096
# propagation:
# # Number of partitions arcade requires on arcade.propagation, and the
# # number of propagation replicas that can do work (one dep-aware
# # dispatcher per partition claim; extra replicas idle as standbys).
# # Checked fail-closed at startup: a topic with FEWER partitions than this
# # aborts the boot. More is allowed — the producer hashes over the topic's
# # actual partition count, so extra partitions carry real traffic and get
# # their own dispatchers. Widen the topic BEFORE raising this; see
# # docs/production-kafka.md "Widening an existing deployment".
# partitions: 1
#
# # Fast-path NETWORK requeue: a tx with no network verdict is retried
# # in-memory up to retry_max_attempts times, retry_backoff_ms apart,
# # before parking at PENDING_RETRY. At the defaults that is a ~10s
# # window. Merkle /watch failures do not consume this budget; registration
# # must succeed before broadcast, and those retries are a separate domain
# # that reuses this same backoff without a second knob.
# retry_max_attempts: 5
# retry_backoff_ms: 2000
#
# # Durable retry: once parked, a tx is drained by the reaper in
# # next_retry_at order with exponential backoff from
# # pending_retry_backoff_ms, doubling to pending_retry_max_backoff_ms.
# # After pending_retry_max_attempts it is terminalized REJECTED — the
# # give-up case is a tx spending an outpoint that never reaches the
# # network, which can neither succeed nor be condemned by an ancestor.
# # The default 288 attempts is ~24h at the 5-minute cap.
# pending_retry_backoff_ms: 5000
# pending_retry_max_backoff_ms: 300000
# pending_retry_max_attempts: 288
#
# # POST /txs chunk caps. A flushed batch is split into chunks holding at
# # most teranode_max_batch_size transactions AND at most
# # teranode_max_batch_bytes of raw transaction bytes (the body is their
# # plain concatenation). Teranode refuses a request that reaches 1024
# # transactions or 32 MiB — both checked strictly, before each read —
# # with a bare 400 carrying no per-tx verdict, so keep both caps under
# # those limits; the defaults leave headroom. A single transaction larger
# # than the byte cap is still broadcast, alone. A chunk a peer still
# # refuses by shape (400 "too much data" / "too many transactions", or a
# # 413 body limit) is narrowed by halving rather than requeued blind.
# teranode_max_batch_size: 1000
# teranode_max_batch_bytes: 16777216
#
# # Per-endpoint circuit-breaker for the datahub URL list. Endpoints that
# # accumulate `failure_threshold` consecutive failures are marked unhealthy
# # and excluded from broadcasts; a background probe hits GET /health on
# # unhealthy endpoints every `probe_interval_ms` to detect recovery. The
# # `min_healthy_endpoints` value is advisory — a WARN is logged when the
# # healthy count drops below it, but broadcasts are never blocked by it.
# endpoint_health:
# failure_threshold: 3
# probe_interval_ms: 30000
# probe_timeout_ms: 2000
# min_healthy_endpoints: 0
# # How often each pod's teranode.Client polls the shared store for new
# # datahub URLs registered by the p2p-client pod. Lower = faster cross-pod
# # convergence; higher = less store load. The first refresh runs
# # synchronously on Start (capped at 2s) so a freshly started pod sees
# # the current registry before serving traffic.
# refresh_interval_ms: 30000
# # Peer-discovered datahub URLs not re-announced (node_status) within
# # this window are no longer used, so a URL a peer stopped advertising
# # is not probed forever or re-admitted on restart. Configured URLs are
# # never aged out. Keep well above the ~10s announcement interval.
# discovered_ttl_ms: 3600000
bump_builder:
# Grace window after BLOCK_PROCESSED before reading STUMPs, to allow for
# late merkle-service retries of any STUMPs that initially got a 5xx.
grace_window_ms: 30000
# Maximum response body size, in bytes, accepted from a DataHub
# /block/<hash> endpoint. The endpoint serves block metadata (header +
# subtree-hash list + coinbase tx + coinbase BUMP), so the default
# 1 GiB leaves two-plus orders of magnitude of headroom over realistic
# Teranode payloads while bounding memory against a hostile or
# malfunctioning endpoint. Set <= 0 to use the built-in default.
# Mitigates F-007.
# datahub_max_block_bytes: 1073741824
#
# Write-time anchor guard (issue #279): refuse to mark a block's txs MINED
# when chaintracks' active chain provably has a DIFFERENT block at its
# height (same-height competition loser). Fails open on any uncertainty.
# false restores legacy last-writer-wins behavior as a rollback lever.
# anchor_guard_enabled: true
#
# Anchor reconciler (issue #279): heals transactions anchored to orphaned
# blocks — re-anchors them to the active-chain block containing them (via
# the canonical block's stored compound BUMP). Runs in the bump-builder
# mode, lease-gated across replicas.
# reconciler:
# enabled: true
# interval_ms: 30000 # orphan-queue tick cadence
# batch_size: 5000 # txids per store write / bulk event
# blocks_per_tick: 4 # orphaned blocks reconciled per tick
# neighborhood_depth: 6 # heights above an orphan searched for re-binned txs
# max_defer_attempts: 10 # ticks to wait for a missing canonical BUMP
# startup_full_scan: true # one-off sweep for stale anchors (bounded, see below)
#
# # Startup full-scan horizon (issue #282). The sweep is BOUNDED so it does
# # not re-orphan a long chain's entire history oldest-first (which starves
# # the actual recent incident). It considers only heights within
# # full_scan_depth of the active tip. Default 144 (~1 day of BSV blocks);
# # a chain shorter than the horizon is swept whole (cheap).
# full_scan_depth: 144
# # Operator-targeted recovery: when either bound is > 0 the startup scan
# # runs over the explicit [min, max] height range instead of the depth
# # horizon (max 0 = up to the tip). E.g. min=760 max=770 heals just the
# # height-764 incident from its already-stored canonical BUMP; min=1
# # restores the legacy whole-history sweep. Both default 0.
# full_scan_min_height: 0
# full_scan_max_height: 0
#
# # Fallback when a block's canonical BUMP is unavailable at the defer cap
# # (issue #282). Default false: PARK the block — leave its txs MINED
# # against the orphan (a later canonical BUMP re-anchors them) rather than
# # un-mining them. true restores the pre-#282 revert-all-to-SEEN fallback.
# revert_when_unreconcilable: false
# Shared on-disk root for arcade state. Chaintracks headers default to
# <storage_path>/chaintracks/.
# storage_path: "~/.arcade"
# Embedded block-header tracker (go-chaintracks). Serves routes under
# /chaintracks/v1/* and /chaintracks/v2/* on the api-server. Default is on
# so the refactor is a drop-in replacement for the original arcade binary.
# Set enabled: false in environments without outbound P2P.
chaintracks_server:
enabled: true
# Tie-scan (issues #279, #339): on each tip update, re-verify the
# block_processing rows within tie_scan_depth of the tip against the
# active chain in both directions — mark same-height losers orphaned (the
# case where chaintracks emits NO ReorgEvent: an equal-chainwork alternate
# never becomes tip) and reset orphaned rows that are the active-chain
# block at their height again to active (the flip-flop where that loser
# wins the next block). 0 disables. Debounced by tie_scan_min_interval_ms;
# the reorg-event handler always runs one scan regardless of the debounce.
# tie_scan_depth: 20
# tie_scan_min_interval_ms: 5000
# go-chaintracks library config. See that library's docs for the full set.
# Defaults are inherited from the library's SetDefaults — uncomment to override.
#
# In single-binary (mode=all) and the dedicated chaintracks pod, leave mode at
# the default "embedded" so this process owns the header store and the P2P
# subscription. In a microservice deployment every OTHER consumer pod
# (bump-builder) should run mode="remote" with url pointed at the chaintracks
# pod's HTTP endpoint — otherwise each pod spins up its own embedded
# ChainManager and hits the upstream node N times. arcade additionally skips
# the chaintracks library entirely in modes that don't read from it
# (api-server, sse, propagation, p2p-client, watchdog), so those pods need no
# chaintracks config at all.
# chaintracks:
# mode: embedded # or "remote" with url set
# url: "" # required when mode=remote, e.g. http://chaintracks:8083
# storage_path: "" # embedded mode only; defaults to <storage_path>/chaintracks
# bootstrap_url: "" # embedded mode only
# bootstrap_mode: api # embedded mode only
# # Bootstrap peers default to the network-derived list, falling back to
# # p2p.bootstrap_peers when that's set — configure peers once at the p2p
# # block and both the p2p-client and chaintracks pick them up. Only set
# # the explicit override below when chaintracks needs a different peer
# # set than the p2p-client (rare). The upstream msgbus.Config struct has
# # no mapstructure tags, so the YAML key is the camelCase-flattened name
# # ("bootstrappeers", not "bootstrap_peers") — viper matches case-
# # insensitively but does not translate snake_case.
# p2p:
# msgbus:
# bootstrappeers:
# - /dns4/peer.example.cluster.local/tcp/9905/p2p/12D3Koo...