Skip to content

Repository files navigation

Frigate logo

Frigate Electrum Server

Frigate is an Electrum server for Silent Payments (BIP352). It performs Silent Payments scanning server-side using ephemeral client keys, returning discovered transactions to clients over an extension to the Electrum JSON-RPC protocol. Scanning runs in-database with optional GPU acceleration, supporting both single-user instances on commodity hardware and multi-user public servers.

Features

  • Server-side Silent Payments scanning with ephemeral (in-RAM) client keys, following the Remote Scanner approach in the BIP352 Index Server Specification.
  • GPU-accelerated scanning with CUDA (NVIDIA), Metal (Apple), and OpenCL (Intel/AMD/NVIDIA) backends, selected automatically at runtime.
  • In-database EC point computation via a custom DuckDB extension wrapping UltrafastSecp256k1.
  • Low-latency mempool ingestion via Bitcoin Core's ZMQ sequence publisher.

Quick Start

  1. Install Bitcoin Core 28 or higher with txindex=1 in bitcoin.conf. (28+ is required for Electrum protocol 1.6; earlier versions work for 1.3–1.5.)
  2. Download the latest Frigate release for your platform from the Releases page (.deb / .rpm for Linux, .dmg for macOS, .zip for Windows). Releases are signed — see GPG Key.
  3. Start Frigate:
    /opt/frigate/bin/frigate                            # Linux .deb / .rpm
    /Applications/Frigate.app/Contents/MacOS/Frigate    # macOS
  4. With Bitcoin Core on the same host using default settings, no further configuration is required. Frigate creates ~/.frigate/config.toml on first start.
  5. Frigate indexes from Taproot activation (mainnet) up to the chain tip before serving Silent Payments queries. Watch the log for Electrum server listening on tcp://... for the readiness signal.
  6. (Optional) Verify with the bundled CLI:
    /opt/frigate/bin/frigate-cli

For non-mainnet networks, pass -n testnet|testnet4|signet|regtest.

Configuration

Frigate stores its configuration in ~/.frigate/config.toml on macOS and Linux, and %APPDATA%\Frigate\config.toml on Windows. A default configuration file is created on first startup. For indexing, Frigate needs access to the Bitcoin Core RPC, which requires txindex=1 in bitcoin.conf.

With Bitcoin Core running on the same machine with default settings, Frigate will connect automatically with no configuration changes required.

# Frigate configuration

[core]
connect = true
# server = "http://127.0.0.1:8332"
# authType = "COOKIE"            # COOKIE or USERPASS
# dataDir = "/home/bitcoin/.bitcoin"
# auth = "user:password"         # only needed for USERPASS
# zmqSequenceEndpoint = "tcp://127.0.0.1:28336"   # bitcoind -zmqpubsequence endpoint for low-latency mempool ingestion
# rpcRequestTimeoutSeconds = 60                   # per-RPC read timeout (raise on slow/remote bitcoind, lower on fast LAN)
# rpcBatchSize = 100                              # max sub-requests per JSON-RPC array batch (mempool fill)

[index]
# startHeight = 0                # default: 709632 on mainnet (Taproot activation), 0 on testnet
# cacheSize = "10M"              # scriptPubKey cache entries (default: 10M, ~4GB RAM)

[scan]
# batchSize = 300000             # rows per GPU dispatch (reduce if scanning hangs on older GPUs)
# computeBackend = "AUTO"        # AUTO, GPU, or CPU
# dbThreads = 4                  # limit DuckDB threads (reduces CPU load when computeBackend = "CPU")
# memoryLimit = "8GB"            # cap DuckDB memory usage (default: 80% of system RAM)
# maxLabels = 10                 # maximum number of labels accepted per silent payments subscription
# maxSubscriptions = 100         # maximum number of silent payments subscriptions per connection
# metricsEnabled = true          # hourly privacy-preserving aggregate log lines of scan stats and server usage (default: true)

[server]
# host = "ssl://xyz.com:50002"   # advertised in server.features; use array for multiple. Omit to advertise nothing.
# tcp = "tcp://0.0.0.0:50001"    # plaintext listener bind URL; omit (or "") to disable. Default if neither tcp nor ssl is set.
# ssl = "ssl://0.0.0.0:50002"    # SSL listener bind URL; omit to disable
# sslCert = "cert.pem"           # PEM certificate (chain allowed); bare filename resolves under Frigate's home dir, or use an absolute path
# sslKey  = "key.pem"            # PEM-encoded PKCS#8 private key; bare filename resolves under Frigate's home dir, or use an absolute path
# sslReloadSeconds = 300         # how often to check the certificate and key files, reloading them without a restart when renewed;
#                                # 0 or less uses the default (reloading does nothing unless the files change)
# donationAddress = ""           # address served for server.donation_address
# bannerFile = "banner.txt"      # text served for server.banner (re-read when changed); bare filename resolves under Frigate's home dir
# adminPort = 50003              # loopback-only admin JSON-RPC port offering getinfo, for health checks; 0 or absent disables it
# shutdownDrainSeconds = 10      # on shutdown, how long sessions may take to finish the request they are handling
# healthStatsEnabled = true      # log backend and index health every 5 minutes, without counts (usage is logged hourly, per [scan] metricsEnabled)
# backendElectrumServer = "tcp://localhost:60001"   # backend must listen on a port distinct from Frigate's tcp/ssl listeners above
# backendRequestTimeoutSeconds = 120                # per-request timeout on each session's backend connection; expiry closes that connection
# backendReconnectMaxBackoffSeconds = 60            # maximum delay between attempts to reconnect a session's lost backend connection
# backendPingIntervalSeconds = 60                   # keepalive ping on a session's idle backend connection
# backendSslCertFile = "backend.pem"                # pin an ssl:// backend to exactly this PEM certificate (overrides backendSslVerify)
# backendSslVerify = false                          # verify an ssl:// backend's certificate against trusted CAs and its hostname

[limits]
# maxConnections = 1000                # global concurrent session cap
# maxConnectionsPerIp = 12             # per client IP (IPv6: per /64)
# maxRequestBytes = 1000000            # max JSON-RPC line length; oversized requests disconnect
# maxBatchSize = 345                   # max requests per JSON-RPC batch array
# sessionTimeoutSeconds = 600          # idle disconnect; Electrum clients ping every ~60s
# maxSubscriptionsPerIp = 75000        # scripthash subscriptions per client IP
# maxSubscriptions = 10000000          # global scripthash subscription cap
# notificationQueueSize = 1000         # notifications (headers, silent payments) a client may fall behind before it is disconnected;
#                                      # raise for clients on slow links receiving very large silent payments scans
# silentPaymentsSubscribeBurst = 10              # silent payments subscribes a session may make at once
# silentPaymentsSubscribeIntervalSeconds = 10    # then one per interval; subscribes beyond the burst are delayed, never rejected
# excludedSubnets = ["127.0.0.1/32", "::1/128"]   # clients exempt from per-IP limits, the batch cap and pacing; add your LAN for a home server

Core

Set connect = false to run Frigate without connecting to Bitcoin Core. This is useful if an index has already been built and you just want to serve queries against it. The authType can be COOKIE (default) or USERPASS. For cookie authentication, set dataDir to the Bitcoin Core data directory if it is not in the default location. For user/password authentication, set auth to user:password.

When zmqSequenceEndpoint is set, Frigate subscribes to Bitcoin Core's ZMQ sequence publisher for low-latency mempool ingestion (sub-100ms instead of up to 5s). This requires Bitcoin Core to be started with -zmqpubsequence=tcp://...:<port> matching this endpoint, and a Bitcoin Core build with ZMQ support compiled in. Release binaries from bitcoincore.org include ZMQ support; a from-source build needs the ZeroMQ development library (e.g. libzmq3-dev) and an explicit -DWITH_ZMQ=ON, since CMake silently disables ZMQ if the library is not found. Verify support with bitcoin-cli getzmqnotifications — a -32601 Method not found error means the binary has no ZMQ support.

Configuring zmqSequenceEndpoint is strongly recommended whenever backendElectrumServer is configured. Without this configured, the backend may notify the client of a new transaction via scripthash before Frigate's silent payments notification lands — causing wallets to briefly display incorrect amounts.

The rpcRequestTimeoutSeconds setting controls the per-RPC read timeout against Bitcoin Core (default: 60). Raise it for a slow or remote node if you see read timeouts in the log; the default is fine for a co-located Bitcoin Core. The rpcBatchSize setting caps the number of sub-requests per JSON-RPC array batch used during initial mempool indexing (default: 100); the default suits most deployments.

Index

Indexing speed is greatly affected by looking up the scriptPubKeys of spent outputs. To improve performance, scriptPubKeys are cached to avoid looking them up again with getrawtransaction. The cacheSize limits the number of scriptPubKeys cached during indexing (e.g. "10M" for 10 million entries, ~4GB RAM). This value can be increased or decreased depending on available RAM.

The DuckDB database is stored in a db subfolder in the same directory, in a file called frigate.duckdb. DuckDB databases can be transferred between different operating systems, and should survive unclean shutdowns.

Scan

The computeBackend setting controls whether historical scanning uses GPU or CPU. Valid values are AUTO (default), GPU, and CPU. In AUTO mode, the GPU is used if one is detected, otherwise the CPU is used. Set to CPU to force CPU-only scanning. With CPU-only scanning, dbThreads can be used to limit the number of DuckDB threads and reduce CPU load. The memoryLimit setting caps DuckDB's memory usage (e.g. "8GB", "1024MB"). DuckDB's default is 80% of system RAM. Mempool and incremental block scans always run on the CPU backend, since they are short, latency-sensitive and benefit from being decoupled from the longer-running historical scans.

The batchSize setting controls how many transactions are processed per GPU dispatch (default: 300,000). If scanning hangs or becomes unstable on certain GPUs (particularly older OpenCL-only GPUs), try reducing this value (e.g. 10,000 to 50,000).

The maxLabels setting caps the number of labels accepted per silent payments subscription (default: 10). The maxSubscriptions setting caps the number of silent payments subscriptions per connection (default: 100). Requests exceeding either limit are rejected with a JSON-RPC -32602 Invalid params error.

When metricsEnabled is true (default), Frigate emits one Aggregate SP scan stats log line per hour summarising historical scan throughput across all scans in the window. The output is bucketed by result count and duration, rounded, and suppresses any bucket with fewer than ten samples, so no per-client scan information is exposed. Set to false to disable the line entirely. The same setting controls the hourly Aggregate server stats line described in Stats.

Server

Listeners are configured as bind URLs in the scheme tcp:// or ssl:// followed by a host and port. The tcp setting is the plaintext listener (default: tcp://0.0.0.0:50001), and ssl enables a TLS listener (e.g. ssl://0.0.0.0:50002). Each URL specifies the bind interface — use 127.0.0.1 to restrict a listener to localhost, or 0.0.0.0 to bind all interfaces. Both listeners can run simultaneously, or either can be omitted (e.g. set tcp = "" to run TLS only). If neither tcp nor ssl is set, Frigate defaults to tcp on 0.0.0.0:50001.

To enable TLS, set ssl and supply a certificate and private key via sslCert and sslKey. Bare filenames are resolved relative to Frigate's home directory (the per-network directory that holds config.toml). Absolute paths are used as-is. The certificate file may contain a single certificate or a full chain (fullchain.pem), and the key file must be an unencrypted PKCS#8 (-----BEGIN PRIVATE KEY-----) PEM. TLS 1.0, 1.1 and SSLv3 are unconditionally disabled as insecure, and only TLS 1.2 and 1.3 are negotiated.

To generate a self-signed certificate for local or testing use, valid for ten years:

openssl req -x509 -newkey rsa:2048 -keyout ~/.frigate/key.pem -out ~/.frigate/cert.pem -days 3650 -nodes -subj "/CN=localhost"

Frigate implements the Silent Payments RPCs natively and proxies all other Electrum requests (including address-related lookups) to a co-located Electrum backend. The backend is configured with backendElectrumServer, and is intended to point to a server running on the same host. Because Frigate occupies the canonical 50001/50002 Electrum ports, a co-located backend (Fulcrum, ElectrumX, electrs, etc.) must be configured to listen on a different port. The Electrum protocol from 1.3 to 1.6 is supported — for 1.6, ensure Bitcoin Core 28 or higher.

When backendElectrumServer is set, also configure zmqSequenceEndpoint under [core] — see the Core section above for why this pairing matters.

Each client session has its own connection to the backend, opened when the client connects and closed when it disconnects, so the backend frees the session's subscriptions exactly as it would for a directly connected client. If a session's backend connection is lost, Frigate reconnects with exponential backoff (up to backendReconnectMaxBackoffSeconds), negotiates the client's protocol version again and resubscribes the session's scripthashes. Any status that changed while the connection was down is then sent to the client, which stays connected throughout. While the backend is unreachable, requests that need it fail at once with a -32000 error. A request the backend has not answered within backendRequestTimeoutSeconds (default 120) fails with the same error and closes that connection, which is then reconnected. An idle backend connection is kept alive with a server.ping every backendPingIntervalSeconds (default 60).

For an ssl:// backend, Frigate accepts any certificate by default, which suits a localhost backend with a self-signed certificate, and logs a warning at startup. Set backendSslCertFile to pin the backend to exactly that PEM certificate, the usual approach for a remote backend with a self-signed certificate. Alternatively, set backendSslVerify = true to require a certificate issued by a certificate authority the JVM trusts, and matching the backend's hostname. Both settings are ignored, with a warning, when the backend is not ssl://.

server.donation_address returns donationAddress, or an empty string if it is not set, rather than the backend's address. server.banner returns the contents of bannerFile if one is set (up to 16 KB, re-read when it changes), and otherwise a generated banner.

Limits

The settings under [limits] protect Frigate when it is open to the public, and their defaults are chosen for a public server, following Fulcrum and ElectrumX where they have equivalents.

  • Connections: at most maxConnections sessions in total, and maxConnectionsPerIp per client IP address (per /64 prefix for IPv6). A connection over either limit is closed without a response.
  • Requests: a request longer than maxRequestBytes is answered with a -32600 request too large error and the connection is closed. A batch of more than maxBatchSize requests is rejected whole with a -32600 batch too large error, and the session continues. A request containing control characters is answered with a -32700 parse error.
  • Pacing: each blockchain.silentpayments.subscribe starts a scan of the index, so a session may make silentPaymentsSubscribeBurst subscribes at once, and then one every silentPaymentsSubscribeIntervalSeconds. With the defaults, that is 10 at once and then one every 10 seconds. Subscribes beyond this are delayed rather than rejected. Other requests are not paced.
  • Subscriptions: scripthash subscriptions are limited per IP (maxSubscriptionsPerIp) and in total (maxSubscriptions). A subscription over either limit is refused with a -32005 subscription limit exceeded error; subscribing again to a scripthash the session already has is not counted.
  • Idle sessions: a session from which nothing is received for sessionTimeoutSeconds is closed. Electrum clients ping about every minute, so only dead or abandoned sessions are affected.
  • Slow clients: a client that falls notificationQueueSize notifications behind is disconnected. Scripthash status notifications are coalesced, so this is reached through header and silent payments notifications; raise it for clients on slow links receiving very large silent payments scans.

Clients connecting from the subnets in excludedSubnets are exempt from the per-IP limits, the batch cap and pacing, though not from the overall limits (maxConnections and maxSubscriptions) or the protections above against oversized requests and slow clients. The default is the loopback address, so local tools such as the Frigate CLI are not limited, and clients of a server offered over Tor, which all arrive from the loopback address, are treated in the same way. For a home server, add your local network, for example excludedSubnets = ["127.0.0.1/32", "::1/128", "192.168.0.0/16"]. Set excludedSubnets = [] to apply every limit to all clients, including those behind a reverse proxy on the same host, which also arrive from the loopback address.

Usage

The Frigate server may be started as follows:

./bin/frigate

or on macOS:

./Frigate.app/Contents/MacOS/Frigate

To start with a different network, use the -n parameter:

./bin/frigate -n signet

To change the home directory for config and data storage (default: ~/.frigate/ on Linux/macOS, %APPDATA%\Frigate on Windows), use the -d parameter:

./bin/frigate -d /var/lib/frigate

The full range of options can be queried with:

./bin/frigate -h

Frigate CLI

Frigate also ships a CLI tool called frigate-cli to allow easy access to the Electrum RPC.

./bin/frigate-cli

or on macOS:

./Frigate.app/Contents/MacOS/frigate-cli

It uses similar arguments, for example:

./bin/frigate-cli -n signet

The scan private key and spend public key, along with the start block height or timestamp, can be specified as arguments or are prompted for:

./bin/frigate-cli -s SCAN_PRIVATE_KEY -S SPEND_PUBLIC_KEY -b 890000
./bin/frigate-cli
Enter scan private key: SCAN_PRIVATE_KEY
Enter spend public key: SPEND_PUBLIC_KEY
Enter start block height or timestamp (optional, press Enter to skip): 890000

By default the CLI client closes once the initial scan is complete, but it can be configured to follow or stay open for incoming updates. When in follow mode, results are only printed if transactions are found.

./bin/frigate-cli -f

The full range of options can be queried with:

./bin/frigate-cli -h

Deployment

Recommended Topology

The expected production deployment is a single host running Bitcoin Core, Frigate, and a co-located Electrum backend (Fulcrum, electrs, or ElectrumX):

                 ┌─────────────────────────────────────────────┐
client ─TLS/TCP─►│  Frigate :50001 / :50002                    │
                 │    │                                        │
                 │    ├─ Silent Payments RPCs (native)         │
                 │    └─ all other RPCs ─► Electrum backend    │
                 │                          :60001 (localhost) │
                 │                            │                │
                 │                            ▼                │
                 │                         Bitcoin Core        │
                 │                          :8332 RPC          │
                 │                          :28336 ZMQ         │
                 └─────────────────────────────────────────────┘

Frigate occupies the canonical Electrum ports (50001/50002). The backend listens on a non-conflicting port (e.g. 60001) bound to localhost, and all non-silent-payments queries are proxied to it transparently.

Backend Limits

All Frigate client connections to backendElectrumServer appear from a single source IP, so backend per-IP caps (e.g. Fulcrum's max_clients_per_ip) can refuse new Frigate clients well below the backend's overall capacity. The co-located localhost topology above is handled by default: Fulcrum exempts 127.0.0.1/32 and ::1/128, and ElectrumX exempts all private addresses (RFC1918, loopback, link-local). If Frigate runs on a different host from the backend, on Fulcrum add Frigate's IP to subnets_to_exclude_from_per_ip_limits. On ElectrumX the exemption set is hardcoded to private addresses, so keep Frigate on the same private network. electrs imposes no per-IP limits and requires no tuning.

Running as a Service

Frigate runs as a long-lived foreground process and integrates straightforwardly with systemd. A minimal unit (/etc/systemd/system/frigate.service):

[Unit]
Description=Frigate Electrum Server
After=network-online.target bitcoind.service
Wants=network-online.target

[Service]
Type=simple
User=frigate
StateDirectory=frigate
ExecStart=/opt/frigate/bin/frigate -d /var/lib/frigate
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

StateDirectory=frigate ensures /var/lib/frigate exists and is owned by the service user, so the dedicated frigate account does not need a real home directory (useradd --system --no-create-home frigate). Adjust User= and the binary path to match your install.

Public-facing TLS

For public deployments, supply a real certificate via sslCert and sslKey rather than the self-signed example in Configuration > Server. The host field under [server] is advertised in server.features and should match the hostname clients use when validating the certificate. TLS termination at a reverse proxy is also supported – in that case, run Frigate with tcp = "tcp://127.0.0.1:50001" and terminate TLS upstream. Note that clients then all appear to come from the loopback address, which is in the default excludedSubnets (see Limits).

Frigate checks the certificate and key files every sslReloadSeconds (default 300) and reloads them when they change, so a renewed certificate (from certbot, for example) is served without a restart. New connections use the new certificate, while existing ones are unaffected. If the files are incomplete when checked, or the key does not match the certificate, Frigate keeps serving the current certificate, logs a warning, and tries again when the files change.

Resource Requirements

  • RAM: indexing uses ~4 GB for the default scriptPubKey cache (cacheSize = "10M"). Serving steady-state queries is dominated by DuckDB's memory pool, which defaults to 80% of system RAM and can be capped via memoryLimit. 16 GB is a comfortable baseline; 8 GB works with a reduced cache and memory limit.
  • Disk: ~18 GB for the DuckDB index, growing slowly over time.
  • CPU: any modern x86-64 or ARM64 server. Indexing is I/O- and RPC-bound rather than CPU-bound.
  • GPU: optional but strongly recommended for multi-user instances. A single discrete GPU (NVIDIA Ampere or newer, Apple Silicon, or any OpenCL 1.2+ device including integrated GPUs) is sufficient for several simultaneous scans. See GPU Requirements.

Operations

Initial Sync

On first start Frigate indexes from Taproot activation (block 709,632 on mainnet) up to the chain tip. It does not begin serving Silent Payments queries until the index reaches the tip — at that point it logs Electrum server listening on tcp://..., which is the readiness signal. Indexing time depends on RPC throughput from Bitcoin Core.

Logs

Logs are written to frigate.log in the per-network data directory (~/.frigate/ for mainnet, ~/.frigate/<network>/ otherwise) and to stdout. The default appender does not rotate; in production, configure logrotate or run under a service manager that captures stdout (e.g. systemd-journald).

Stats

Frigate logs two stats lines, kept apart so that what is logged frequently cannot expose individual clients:

  • Every 5 minutes, a Server health line reports whether the backend is connected for all, some or no sessions, any backend reconnects or request timeouts since the previous line, the index height (or whether it is at the chain tip) and the mempool size. It contains no counts of sessions or subscriptions. Set healthStatsEnabled = false under [server] to disable it.
  • Every hour, when metricsEnabled is true under [scan], an Aggregate server stats line reports the sessions, IP addresses and subscriptions connected at that moment, and over the hour the notifications delivered, the largest notification backlog, and the silent payments subscribes delayed by pacing. As with the scan stats, each figure is rounded to the nearest ten and omitted below ten.

For health checks from monitoring tools, set adminPort under [server] to enable a JSON-RPC admin endpoint, which is always bound to the loopback address. Its single getinfo method returns the version, network, uptime, effective limits and the same health figures, with usage figures rounded as in the hourly line. It deliberately offers nothing about individual sessions.

echo '{"jsonrpc":"2.0","id":1,"method":"getinfo","params":[]}' | nc -w 2 127.0.0.1 50003

Backup

The tweak index can be rebuilt from Bitcoin Core at any time, but a rebuild from Taproot activation is slow. To minimise downtime, snapshot db/frigate.duckdb under the data directory while Frigate is stopped (DuckDB files are portable across operating systems and survive unclean shutdowns). The config.toml and any TLS material in the data directory are worth including in the same backup.

Upgrades

Stop Frigate, install the new release, start. The DuckDB file format and the on-disk index schema are stable across Frigate releases.

On shutdown, Frigate stops accepting connections and closes idle sessions at once, while sessions handling a request finish it, for up to shutdownDrainSeconds (default 10), before closing. Each session's backend connection is closed with it, and the indexes are closed only after every session has ended.

Reorgs

Frigate detects chain reorgs via Bitcoin Core and rescans the affected block range automatically. Connected silent-payments subscribers are renotified for the rescanned range; clients reconnecting after a disconnect should pass start = lastSeenHeight - 100 on resubscribe to cover any reorg they missed.

Performance

CPU Performance

Without GPU acceleration the scanning query is CPU bound, mostly around EC point multiplication. DuckDB parallelizes the workload based on row groups, with each row group containing 122,880 rows. It will by default configure itself to use all the available cores on the server it is running. The behaviour can be configured in the Frigate configuration file (see dbThreads).

The following results were produced by the included benchmark.py script scanning mainnet to block height 914,000. Note that a mainnet database indexing from height 800,000 is required to run the benchmark.

MacBook M5 Max (18 CPUs):

Blocks Start End Transactions Time Transactions/sec
2 hours 12 913988 914000 8,207 119ms 69,132
1 day 144 913856 914000 127,804 1s 322ms 96,647
1 week 1008 912992 914000 751,769 1s 936ms 388,304
2 weeks 2016 911984 914000 1,709,358 2s 178ms 784,765
1 month 4320 909680 914000 4,240,572 4s 648ms 912,286
3 months 12960 901040 914000 13,558,435 17s 895ms 757,669
6 months 25920 888080 914000 26,103,759 29s 824ms 875,262
1 year 52560 861440 914000 59,578,156 1m 2s 959,622
2 years 105120 808880 914000 132,994,804 2m 19s 954,929

Intel Core Ultra 9 285K (24 CPUs):

Blocks Start End Transactions Time Transactions/sec
2 hours 12 913988 914000 8,207 215ms 38,141
1 day 144 913856 914000 127,804 1s 443ms 88,558
1 week 1008 912992 914000 751,769 2s 748ms 273,605
2 weeks 2016 911984 914000 1,709,358 4s 502ms 379,717
1 month 4320 909680 914000 4,240,572 10s 23ms 423,096
3 months 12960 901040 914000 13,558,435 26s 516ms 511,330
6 months 25920 888080 914000 26,103,759 45s 39ms 579,582
1 year 52560 861440 914000 59,578,156 1m 37s 608,267
2 years 105120 808880 914000 132,994,804 3m 29s 635,405

Higher performance on the longer periods is possible by increasing the number of CPUs. Multiple clients conducting simultaneous scans slows each scan linearly, since a single scan already saturates all available CPU cores.

GPU Performance

GPU performance is significantly higher, and as a result is the default compute backend for historical scans.

MacBook M5 Max (Metal GPU backend):

Blocks Start End Transactions Time Transactions/sec
2 hours 12 913988 914000 8,207 15ms 549,752
1 day 144 913856 914000 127,804 54ms 2,359,838
1 week 1008 912992 914000 751,769 232ms 3,238,371
2 weeks 2016 911984 914000 1,709,358 506ms 3,376,651
1 month 4320 909680 914000 4,240,572 1s 212ms 3,497,959
3 months 12960 901040 914000 13,558,435 3s 836ms 3,534,313
6 months 25920 888080 914000 26,103,759 7s 150ms 3,651,085
1 year 52560 861440 914000 59,578,156 16s 332ms 3,647,902
2 years 105120 808880 914000 132,994,804 38s 2ms 3,499,656

NVIDIA RTX 5080 (CUDA backend):

Blocks Start End Transactions Time Transactions/sec
2 hours 12 913988 914000 8,207 18ms 460,614
1 day 144 913856 914000 127,804 26ms 4,880,898
1 week 1008 912992 914000 751,769 69ms 10,906,924
2 weeks 2016 911984 914000 1,709,358 146ms 11,722,414
1 month 4320 909680 914000 4,240,572 341ms 12,447,948
3 months 12960 901040 914000 13,558,435 1s 66ms 12,722,307
6 months 25920 888080 914000 26,103,759 1s 613ms 16,182,843
1 year 52560 861440 914000 59,578,156 3s 466ms 17,188,956
2 years 105120 808880 914000 132,994,804 7s 690ms 17,294,286

2x NVIDIA RTX 5090 (CUDA backend):

Blocks Start End Transactions Time Transactions/sec
2 hours 12 913988 914000 8,207 21ms 393,537
1 day 144 913856 914000 127,804 53ms 2,404,792
1 week 1008 912992 914000 751,769 92ms 8,207,103
2 weeks 2016 911984 914000 1,709,358 120ms 14,213,421
1 month 4320 909680 914000 4,240,572 158ms 26,779,011
3 months 12960 901040 914000 13,558,435 562ms 24,146,078
6 months 25920 888080 914000 26,103,759 1s 186ms 22,012,526
1 year 52560 861440 914000 59,578,156 2s 96ms 28,418,826
2 years 105120 808880 914000 132,994,804 3s 208ms 41,455,203

This approach is performant enough for a multi-user public instance. As EC computation is offloaded to the GPU, CPU overhead is low and normal Electrum server RPC calls can be handled simultaneously without any performance degradation. Multiple clients conducting simultaneous scans slows each scan linearly, since a single scan already saturates the available GPUs. Using multiple GPUs in the same system is also supported and the workload is scaled across them.

A discrete GPU is not required however. Frigate can take advantage of any integrated GPU supported by OpenCL, which in practice includes almost all desktop Intel and AMD chips produced in the last decade. This prevents saturation of the CPU, and in the case of weaker CPUs (for example older Intel NUCs) can actually be faster. See the section below on enabling the iGPU on Linux.

GPU Requirements

Frigate supports three GPU backends, selected automatically at runtime in order of preference:

CUDA (NVIDIA)

  • NVIDIA Ampere or newer GPU (RTX 30xx, A100, RTX 40xx, H100, RTX 50xx)
  • NVIDIA driver 570.86.15+ (Linux) or 571.14+ (Windows)
  • No CUDA toolkit installation required on the host

OpenCL (NVIDIA, Intel, AMD)

  • Any GPU with an OpenCL 1.2+ runtime
  • NVIDIA: OpenCL runtime is included with the driver
  • Intel: requires intel-opencl-icd (see Enabling Intel iGPU on Linux)
  • AMD: requires ROCm or AMDGPU-PRO OpenCL runtime
  • On Linux, the ocl-icd-libopencl1 ICD loader is required

Metal (Apple)

  • Apple Silicon (M1 or newer) or AMD GPU with Metal support
  • macOS 12 (Monterey) or newer

Benchmarking

The benchmark.py script in the project root can be used to generate the above tables against a running Frigate server:

python3 benchmark.py
python3 benchmark.py --markdown

The --clients N option runs N concurrent clients per scan period to test server behaviour under load:

python3 benchmark.py --clients 4

Enabling Intel iGPU on Linux

Frigate supports GPU-accelerated scanning via OpenCL on Intel integrated GPUs. On most Linux distributions, the Intel OpenCL runtime is not installed by default. Note that on newer Intel CPUs, the CPU may be stronger than the GPU - but offloading the computation to the GPU is still beneficial.

Install the Intel OpenCL runtime

On Ubuntu/Debian:

sudo apt install ocl-icd-libopencl1 intel-opencl-icd clinfo

ocl-icd-libopencl1 is the OpenCL ICD loader that dispatches to vendor runtimes — it may already be installed if another GPU driver (e.g. NVIDIA) is present. intel-opencl-icd is the Intel GPU compute runtime. Both can coexist with other GPU drivers without affecting them. clinfo is a diagnostic tool for listing available OpenCL platforms and devices. The OpenCL ICD (Installable Client Driver) system allows multiple GPU vendors to coexist.

Driver compatibility

The Ubuntu-packaged driver may be outdated for newer Intel GPUs. If Frigate crashes during startup with a newer Intel GPU, install the latest driver from Intel's PPA:

wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | sudo gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg
echo "deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu $(lsb_release -cs) unified" | sudo tee /etc/apt/sources.list.d/intel-gpu.list
sudo apt update
sudo apt install intel-opencl-icd

Verify

Check that the Intel GPU is visible:

clinfo | grep "Device Name"

You should see your Intel GPU listed (e.g. Intel(R) UHD Graphics [0x7d67]).

How It Works

Motivation

BIP 352 proposes that light clients use compact block filters to scan for UTXOs received to a Silent Payments address. This is workable for full nodes but problematic for light clients: a reasonable scan period requires gigabytes of tweaks, filters and blocks to be downloaded, parsed and cached on the client, far exceeding the few megabytes a traditional Electrum wallet uses to restore. It also offers no mempool monitoring, which users rely on to answer the "did you get my transaction?" question.

Frigate addresses both problems by moving scanning to the server, in line with the Remote Scanner approach in the BIP352 Index Server Specification. The scan private key and spend public key are provided to the server, but not stored - they are held in RAM only for the duration of the client session, analogous to how public Electrum servers see ephemeral wallet addresses today.

The Tweak Index

On startup Frigate connects to Bitcoin Core, downloads blocks from the configured start height (or Taproot activation on mainnet) and builds a tweak index - a single table containing one row per Silent-Payments-eligible transaction:

Column Type
txid BLOB
height INTEGER
tweak_key BLOB
outputs LIST(BIGINT)

The txid and tweak_key values are 32 and 64 byte BLOBs respectively. The outputs value is a list of 8 byte integers, each representing the first 8 bytes of the x-value of the Taproot output public key.

In-Database EC Computation

Every silent-payments scan is bounded by EC point multiplication over the tweak index, so Frigate pushes that computation as close to the rows as possible. Instead of loading rows into the Frigate server application, scanning is performed by the database itself. Frigate uses DuckDB, a fast OLAP database designed for analytical query workloads, extended with a custom extension wrapping UltrafastSecp256k1 - a high-performance secp256k1 library with CPU and GPU backends.

Conceptually, scanning for Silent Payments requires computing the Taproot output key for k = 0 and comparing it to the list of known output keys for each tweak row:

SELECT txid, tweak_key, height FROM tweak WHERE list_contains(outputs, hash_prefix_to_int(secp256k1_ec_pubkey_combine([SPEND_PUBLIC_KEY, secp256k1_ec_pubkey_create(secp256k1_tagged_sha256('BIP0352/SharedSecret', secp256k1_ec_pubkey_tweak_mul(tweak_key, SCAN_PRIVATE_KEY) || int_to_big_endian(0)))]), 1));

The client can then download each matching transaction and determine if it does indeed contain outputs it is interested in, including for higher values of k.

Frigate performs all of these steps at once using a single scanning function that also includes a further step to scan for change:

SELECT txid, tweak_key, height FROM ufsecp_scan((SELECT txid, height, tweak_key, outputs FROM tweak), SCAN_PRIVATE_KEY, SPEND_PUBLIC_KEY, [CHANGE_TWEAK_KEY]);

The change tweak is added to the computed P0 and checked again against the outputs for a match. All inputs are provided in little endian format, with public keys in uncompressed little endian x,y format to avoid decompression overhead on each tweak. UltrafastSecp256k1 uses batch affine addition and KPlan-optimized scalar multiplication to maximize throughput on CPU, and can leverage CUDA, OpenCL or Metal to offload EC computation to the GPU for significantly higher performance.

Electrum Protocol

The Electrum protocol is by far the most widely used light client protocol for Bitcoin wallets, and support is now almost a requirement for widespread adoption of any wallet technology proposal. It is characterised by resource efficiency for the client in terms of bandwidth, CPU and storage, allowing a good user experience on almost any platform. It has however been designed around BIP32 wallets. Silent Payments presents an alternative model, where instead of an incrementing derivation path index (and associated gap limit) transactions must be found through scanning the blockchain. As such, new methods are necessary. Frigate implements the following Electrum JSON-RPC methods, proposed for inclusion in the Electrum protocol:

server.features

Servers supporting silent payments advertise the capability by including a silent_payments field in the server.features response:

{
  ...,
  "silent_payments": [0]
}

Each integer is a fully-specified silent payments protocol version supported by the server. BIP352 defines version 0.

blockchain.silentpayments.subscribe

Signature

blockchain.silentpayments.subscribe(scan_private_key, spend_public_key, start, labels)
  • scan_private_key: A 64 character string containing the hex of the scan private key.
  • spend_public_key: A 66 character string containing the hex of the spend public key.
  • start: (Optional) An integer block height to start scanning from, or a string of the form "FROM-TO" to specify a closed range of heights. Integer values above 500,000,000 are treated as seconds from the start of the epoch, matching nLockTime convention.
  • labels: (Optional) An array of positive integers specifying additional silent payment labels to scan for. Change (m = 0) is always included regardless. To aid in wallet recovery, this parameter should only be used for specialized applications.

Result

A subscription JSON object literal containing details of the current subscription:

  • address: The silent payment address that has been subscribed to.
  • labels: An array of the labels that are subscribed to (must include 0).
  • start_height: The block height from which the subscription scan was started.

If a subscription for the same scan address already exists on the connection, servers must not narrow its coverage. If the new request's resolved start height is greater than the existing subscription's start_height, the server retains the existing wider start and returns it as start_height in the response. Clients should treat the returned start_height as authoritative. On every subscribe (including re-subscribe to the same address), the server must re-run the historical scan from the returned start_height and emit notifications as usual, so that clients which clear their local cache on re-subscribe are repopulated.

Result Example

{
  "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv",
  "labels": [0],
  "start_height": 882000
}

Notifications

Once subscribed, the client will receive notifications as results are returned from the scan with the following signature. Servers must send the response to blockchain.silentpayments.subscribe before any notification for that subscription. The subscription object in every notification must be byte-identical to the one returned in the original blockchain.silentpayments.subscribe response with the same address, labels and start_height. It identifies which subscription the notification belongs to, not the range that the server happened to scan to produce it. All historical (progress < 1.0) results must be sent before current (up to date) results. A progress of 1.0 indicates the scan is up to date as of this notification. The first such notification marks the end of the historical scan; subsequent 1.0 notifications represent live updates as new blocks and mempool transactions are scanned. Clients should flush any buffered history to local state on every 1.0 notification.

blockchain.silentpayments.subscribe(subscription, progress, history)

Body

A dictionary with the following key/value pairs:

  1. A subscription JSON object literal containing details of the current subscription:
  • address: The silent payment address that has been subscribed to.
  • labels: An array of the labels that are subscribed to (must include 0).
  • start_height: The block height from which the subscription scan was started.
  1. A progress key/value pair indicating the progress of a historical scan:
  • progress: A floating point value between 0.0 and 1.0. Will be 1.0 for all current (up to date) results.
  1. A history array of transactions. Confirmed transactions are listed in order by height, followed by mempool transactions in arbitary order. Each transaction is a dictionary with the following keys:
  • height: The integer height of the block the transaction was confirmed in. For mempool transactions, 0 should be used.
  • tx_hash: The transaction hash in hexadecimal.
  • tweak_key: The tweak key (input_hash*A) for the transaction in compressed format.

Result Example

{
  "subscription": {
    "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv",
    "labels": [0],
    "start_height": 882000
  },
  "progress": 1.0,
  "history": [
    {
      "height": 890004,
      "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412",
      "tweak_key": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8"
    },
    {
      "height": 905008,
      "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403",
      "tweak_key": "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004"
    },
    {
      "height": 0,
      "tx_hash": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
      "tweak_key": "03aeea547819c08413974e2ab2b12212e007166bb2058f88b009e082b9b4914a58"
    }
  ]
}

It is recommended that servers implementing this protocol send history results incrementally as the historical scan progresses. In addition, a maximum page size of 1000 is suggested when sending historical transactions. This will avoid transmission issues with large wallets that have many transactions, while providing the client with regular progress updates. Servers should also emit a notification with empty history at regular intervals (e.g. every 5 seconds) during a historical scan, to keep the client updated on scanning progress. In the case of block reorgs, the server should rescan all existing subscriptions from the reorg-ed block height and send any history (if found) to the client. All found mempool transactions should be sent on the initial subscription, but thereafter previously sent mempool transactions should not be resent. The server must not re-notify a previously sent mempool transaction once it confirms in a block - clients track confirmation state via blockchain.scripthash.subscribe. The silent payments subscription is purely a discovery channel. Once a silent payments subscription has reached progress = 1.0, servers must deliver silent payments notifications before any scripthash notification arising from the same block or mempool event, since the silent payments subscription is how clients learn which scripthashes to subscribe to.

Clients reconnecting with prior history should pass start = lastSeenHeight - reorgLimit to limit the rescan to a recent window. A reorg limit of 100 blocks is sufficient.

For every transaction announced via this subscription, clients must retrieve it with blockchain.transaction.get and subscribe to all owned outputs with blockchain.scripthash.subscribe. The scripthash subscription is the canonical source of confirmation height, reorg recovery, and spend tracking; the silent payments subscription does not duplicate any of these. In other words, the silent payments address subscription is a replacement for the monotonically increasing derivation path index in BIP32 wallets. The subscription seeks only to add to the client's knowledge of incoming silent payments transactions. The client is responsible for checking the transactions do actually send to addresses it has keys for, and using normal Electrum wallet synchronization techniques to monitor for changes to these addresses. The tweak key is provided to allow the client to avoid looking up the scriptPubKeys of spent outputs.

blockchain.silentpayments.unsubscribe

Signature

blockchain.silentpayments.unsubscribe(scan_private_key, spend_public_key)
  • scan_private_key: A 64 character string containing the hex of the scan private key.
  • spend_public_key: A 66 character string containing the hex of the spend public key.

Result

The silent payment address that has been unsubscribed. This should cancel any scans that may be currently running for this address.

Result Example

sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv

Scan Audit Mode

Frigate includes a correctness testing mode to verify the database extension will find all outputs for a given wallet. In normal operation, the tweak index stores the hash prefixes derived from the taproot output public keys of each eligible transaction. In audit mode, the index instead stores the hash prefix of the expected change output key (P0 = Bspend + t0·G) computed from the provided wallet keys. This treats every silent payments eligible transaction as paying to the provided wallet. The audit can then be run with the following query:

SELECT (SELECT COUNT(*) FROM tweak) AS total_rows, COUNT(*) AS matched_rows FROM ufsecp_scan((SELECT txid, height, tweak_key, outputs FROM tweak), from_hex('<scan_private_key_little_endian>'), from_hex('<spend_public_key_little_endian_x_y>'), [from_hex('<change_label_key_little_endian_x_y>')]); 

This mode is activated by setting two environment variables before starting Frigate:

export FRIGATE_AUDIT_SCAN_KEY=<scan_private_key_hex>
export FRIGATE_AUDIT_SPEND_KEY=<spend_public_key_hex>

When both variables are set, a warning is logged on startup confirming audit mode is active. The index must be rebuilt from scratch when switching between normal and audit modes.

Building

To clone this project, use

git clone --recursive git@github.com:sparrowwallet/frigate.git

or for those without SSH credentials:

git clone --recursive https://github.com/sparrowwallet/frigate.git

In order to build, Frigate requires Java 25 or higher to be installed. The release binaries are built with Eclipse Temurin 25.0.2+10. If you are using SDKMAN, you can use sdk env install to ensure you have the correct version.

Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:

sudo apt install -y rpm fakeroot binutils

The Frigate binaries can be built from source using

./gradlew jpackage

Note that to build the Windows installer, you will need to install WiX.

When updating to the latest HEAD

git pull --recurse-submodules

Reporting Issues

Please use the Issues tab above to report an issue. If possible, look in the frigate.log file in the configuration directory for information helpful in debugging.

License

Frigate is licensed under the Apache 2 software licence.

GPG Key

The Frigate release binaries here are signed using craigraw's GPG key:
Fingerprint: D4D0D3202FC06849A257B38DE94618334C674B40
64-bit: E946 1833 4C67 4B40

About

Frigate Electrum Server

Resources

Stars

93 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages