Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 85 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ authors = ["Buffrr <contact@buffrr.dev>"]

[workspace.dependencies]
fabric-resolver = { path = "fabric", version = "0.2.3" }
libveritas = { version = "0.2" }
libveritas_testutil = { version = "0.2" }
libveritas = { version = "0.3.1" }
libveritas_testutil = { version = "0.3.1" }

spaces_client = { version = "0.1" }
spaces_protocol = { version = "0.1" }
spaces_nums = { version = "0.1" }
spaces_checkpoint = { version = "0.1" }
spaces_client = { version = "0.2.1" }
spaces_protocol = { version = "0.2.1" }
spaces_nums = { version = "0.2.1" }
spaces_checkpoint = { version = "0.2.1" }
spacedb = { version = "0.1", features = ["hash-idx"] }
77 changes: 74 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Certificate relay network for the [Spaces protocol](https://spacesprotocol.org).

Certrelay consists of two components:

- **relay** — HTTP server that verifies certificates, stores them in SQLite, and gossips with peers
- **relay** — HTTP server that verifies certificates, stores them in SQLite, and syncs with peers (pull-based replication with poke notifications)
- **fabric** — Client library available in Rust, JavaScript, Go, Python, Kotlin, and Swift

The protocol is plain HTTP — relays are queryable from browsers, mobile apps, and any language with an HTTP client. All verification is done client-side against Bitcoin's chain state.
Expand Down Expand Up @@ -38,12 +38,83 @@ No external Bitcoin node required. Data is stored in `~/.certrelay` by default.
| `--data-dir` | `CERTRELAY_DATA_DIR` | `~/.certrelay` | Data directory |
| `--bind` | `CERTRELAY_BIND` | `127.0.0.1` | Bind address |
| `--port` | `CERTRELAY_PORT` | `7778` (mainnet) / `7779` (other) | Listen port |
| `--self-url` | `CERTRELAY_SELF_URL` | - | Public URL for peer announcements |
| `--self-url` | `CERTRELAY_SELF_URL` | - | Public URL for peer announcements (also enables poke sending) |
| `--config` | `CERTRELAY_CONFIG` | - | Path to a TOML config file (see below) |
| `--spaced-rpc-url` | `CERTRELAY_SPACED_RPC_URL` | - | External spaced RPC (skips embedded node) |
| `--remote-ip-header` | `CERTRELAY_REMOTE_IP_HEADER` | - | Header for client IP behind reverse proxy |
| `--remote-ip-header` | `CERTRELAY_REMOTE_IP_HEADER` | - | Header for client IP behind reverse proxy (rightmost entry is used) |
| `--is-bootstrap` | `CERTRELAY_BOOTSTRAP` | `false` | Run as a bootstrap node |
| `--anchor-refresh` | `CERTRELAY_ANCHOR_REFRESH` | `300` | Anchor refresh interval in seconds |
| `--allow-private-peers` | `CERTRELAY_ALLOW_PRIVATE_PEERS` | `false` | Accept peers on private/loopback addresses (local development only) |
| `--skip-checkpoint-sync` | - | `false` | Skip checkpoint download, sync from scratch |

### Configuration file

Rate limits, sync tuning, peer table sizes, concurrency caps, and storage
retention live in an optional TOML file passed via `--config` (or
`CERTRELAY_CONFIG`). Every field is optional and defaults to the values shown —
a config file only needs the settings being changed. Unknown keys are rejected
to catch typos.

```toml
[rate_limits]
message_per_min = 60 # /message (client publishes)
proof_per_min = 30 # /query, /chain-proof (trigger proof generation)
read_per_min = 120 # /hints, /reverse, /addrs, /anchors, /peers, /stats
announce_per_min = 5 # /announce
sync_per_min = 60 # /sync, /sync/summary (pages are the unit)
poke_per_min = 30 # /poke
space_per_min = 100 # per-space content updates (replacements only)
handle_period_secs = 300 # per-handle content cap period (replacements only)
handle_burst = 3 # per-handle burst within the period

[sync]
interval_secs = 45 # pull round cadence (plus jitter)
jitter_secs = 15
page_limit = 1000 # rows requested per /sync page (max 1000)
peers_per_round = 2 # peers pulled from per round
max_pages_per_peer = 200 # page budget per peer per round
poke_debounce_ms = 2000 # coalescing window for outgoing pokes
poke_cooldown_ms = 5000 # min gap between poke-triggered pulls per peer

[peers]
max_unverified = 1000 # announced-but-unverified peer slots
max_verified = 100 # verified peer slots
verified_ttl_secs = 600 # verified peers expire without a liveness refresh

[limits]
max_message_size = 524288 # /message body cap in bytes (512 KB)
proof_concurrency = 6 # concurrent chain-proof generations (503 beyond)
verify_concurrency = 4 # concurrent message verifications (503 beyond)

[retention]
max_storage_bytes = 10737418240 # handle payload budget (10 GB); 0 = unlimited
entitlement_per_epoch = 10000 # handles per (space, epoch) counted as paid-for
evict_low_water_pct = 90 # evict down to this % of the budget
sweep_interval_secs = 30 # pressure check cadence
eviction_batch = 1000 # rows deleted per transaction
```

Notes on the less obvious knobs:

- **Content limits are churn-only.** `space_per_min` and the `handle_*` caps
charge only when an existing record is *replaced*; the first insert of a
handle is always free so relays can bootstrap-sync a whole network's data.
- **Retention never rejects data under budget.** A space's *entitlement* is
`entitlement_per_epoch × epochs it committed on-chain` — commitments cost
Bitcoin transactions, so storage beyond entitlement is storage nobody paid
for. Only when `max_storage_bytes` is exceeded does the relay evict, most
over-entitled space first (oldest and least-queried rows first), and stop
admitting *new* handles for over-entitled spaces until pressure clears.
Size the budget to your disk; a small relay stays functional by shedding
the heaviest spaces, a big relay can hold everything.

### Monitoring

- `GET /health` — unmetered liveness check for load balancers and peers.
- `GET /stats` — JSON counters: message intake, sync progress (including last
successful sync per peer — the key signal that replication is healthy),
pokes, rate-limit rejections, storage totals versus budget, and evictions.

### Public relay behind a reverse proxy

```bash
Expand Down
1 change: 1 addition & 0 deletions fabric/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ client = ["reqwest", "rand", "tokio"]
signing = ["secp256k1"]

[dependencies]
borsh = { version = "1.6", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
libveritas = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion fabric/go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22

require (
github.com/btcsuite/btcd/btcec/v2 v2.3.6
github.com/spacesprotocol/libveritas-go v0.2.0
github.com/spacesprotocol/libveritas-go v0.3.1
)

require (
Expand Down
4 changes: 2 additions & 2 deletions fabric/go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/spacesprotocol/libveritas-go v0.2.0 h1:QJ/QYb3ixZu+PGQB1MgXaqVwY8NvcqjDE1dtqF2HFVQ=
github.com/spacesprotocol/libveritas-go v0.2.0/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8=
github.com/spacesprotocol/libveritas-go v0.3.1 h1:MN7DkFvuySiv006iE5xBZc0SUAwi7QB3aO1iuWjKeQ8=
github.com/spacesprotocol/libveritas-go v0.3.1/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8=
Loading
Loading