diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b08c05 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# NOTE: Dockerfile and docker-entrypoint.sh MUST stay in the context - +# the Dockerfile COPYs the entrypoint into the runtime image. + +# ---- Rust build output (re-built inside the image) ----------------------- +target/ +**/target/ + +# ---- Local runtime state - never bake into the image -------------------- +data/ +**/*.db +**/*.db-journal +**/*.sqlite +**/*.sqlite-journal + +# ---- VCS / CI / editor / OS junk --------------------------------------- +.git/ +.github/ +.gitignore +.gitattributes +.idea/ +.vscode/ +.DS_Store +**/.DS_Store +.envrc + +# ---- Non-Rust fabric language bindings (not needed to build certrelay) -- +# The Rust workspace only needs fabric/rust + fabric/Cargo.toml. +# Excluding these typically saves >1GB on a checked-out workspace. +fabric/js/ +fabric/go/ +fabric/python/ +fabric/kotlin/ +fabric/swift/ +fabric/examples/ + +# ---- Generic language-binding build artefacts (defensive) -------------- +**/node_modules/ +**/dist/ +**/.next/ +**/*.tsbuildinfo +**/.build/ +**/.swiftpm/ +**/Package.resolved +**/venv/ +**/.venv/ +**/__pycache__/ +**/*.egg-info/ +**/*.pyc +**/build/ +**/.gradle/ + +# ---- Repo docs / scratch (not needed at build time) -------------------- +NOTES.md +CHANGELOG.md +LICENSE +README.md +release-plz.toml +setup-spaced-prod-env.sh +.commitlintrc.yml diff --git a/.gitignore b/.gitignore index 8d93e4d..a927964 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ __pycache__/ # Kotlin build/ .gradle/ + +.DS_Store +data/ + +setup-spaced-prod-env.sh +NOTES.md diff --git a/CERTRELAY_API.md b/CERTRELAY_API.md new file mode 100644 index 0000000..2a2b8bd --- /dev/null +++ b/CERTRELAY_API.md @@ -0,0 +1,481 @@ +# Certrelay HTTP API + +Certrelay exposes a plain HTTP API for the Spaces certificate relay network. +All routes are defined in `relay/src/http.rs` and served on the listen address +configured by `CERTRELAY_BIND` / `CERTRELAY_PORT` (default `127.0.0.1:7778` on +mainnet). + +There are **9 endpoints**: two write paths (`POST /message`, `POST /announce`) +and seven read paths. Every route is public, CORS-open, and per-IP +rate-limited. There is no authentication. + +For client-side usage, see [`RESOLVE.md`](RESOLVE.md) (querying) and the +`Publishing to a local relay` section in [`README.md`](README.md). + +## Common behavior + +### CORS + +All routes are wrapped in a permissive CORS layer, so browser clients can call +the API from any origin. + +### Client IP and rate limiting + +Each request is keyed to a client IP for rate limiting. By default the IP comes +from the TCP peer address. When `CERTRELAY_REMOTE_IP_HEADER` is set (e.g. +`x-forwarded-for`), the relay reads that header instead and uses the leftmost +IP in comma-separated lists. + +### Default rate limits + +| Bucket | Endpoints | Limit | +|----------|------------------------------------------------|--------------| +| `message` | `POST /message` | 10 / min / IP | +| `announce` | `POST /announce` | 5 / min / IP | +| `peers` | `GET /peers`, `GET /anchors` | 10 / min / IP | +| `query` | `GET /query`, `GET /hints`, `POST /chain-proof`, `GET /reverse`, `GET /addrs` | 15 / min / IP | + +Exceeded limits return `429 Too Many Requests`. + +### Message size + +`POST /message` bodies are capped at `DEFAULT_MAX_MESSAGE_SIZE` (512 KB). + +--- + +## Endpoint summary + +| Method | Path | Purpose | Request | Response | +|--------|----------------|----------------------------------------------|---------------------------------|-----------------------------------| +| `POST` | `/message` | Submit a signed certificate message | Binary borsh `Message` | `200` + `"ok"` or error | +| `POST` | `/announce` | Announce a peer relay URL | JSON `Announcement` | `200` + `"ok"` or error | +| `GET` | `/peers` | List verified peers | — | JSON `[PeerInfo]` | +| `GET` | `/query` | Resolve handles | `?q=...` (+ optional `?hints=`) | Binary borsh `Message` | +| `GET` | `/anchors` | Get anchor set | — or `?root=` | JSON `AnchorSet` + headers | +| `GET` | `/hints` | Lightweight freshness check | `?q=...` | JSON `HintsResponse` | +| `POST` | `/chain-proof` | Build a chain proof | JSON `ChainProofRequest` | Binary borsh `ChainProof` | +| `GET` | `/reverse` | Numeric id → handle name | `?ids=...` | JSON `[{id, name}]` | +| `GET` | `/addrs` | Address → handles | `?name=...&addr=...` | JSON `{address, handles}` | + +--- + +## `POST /message` + +Submit a signed certificate message. This is the **write path** for the relay +network. + +**Request** + +- Content-Type: typically `application/octet-stream` +- Body: borsh-encoded `Message` containing one or more handle updates (cert + + signed `RecordSet`) + +**Behavior** + +1. Deserialize and verify the message against the relay's anchor set and + on-chain state. +2. On success, store zones and handle records in SQLite. +3. Asynchronously gossip the raw message bytes to up to 4 random verified + peers. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | Accepted, stored, gossip initiated | +| `400` | Invalid borsh or verification rejected (`rejected: `) | +| `413` | Body exceeds max message size | +| `429` | Rate limited | + +**Example** + +Used by `fabric publish` and by peer relays forwarding gossip. Not practical to +construct by hand; use the Fabric client. + +--- + +## `POST /announce` + +Register a peer relay URL with this node. Other relays call this during +bootstrap and periodic re-announce. + +**Request** + +```json +{ + "url": "https://relay.example.com", + "capabilities": 0 +} +``` + +| Field | Type | Required | Notes | +|----------------|--------|----------|-------| +| `url` | string | yes | Public HTTPS URL; max 256 bytes | +| `capabilities` | u32 | yes | Capability flags (currently unused; send `0`) | + +The receiver fills in `source_ip` from the TCP connection (or proxy header); the +client does not send it. + +**Behavior** + +- URL is stored in the **unverified** peer table. +- A background task health-checks unverified peers (`HEAD /peers`) every 10s + and promotes successful ones to **verified**. +- Only verified peers appear in `GET /peers` and receive gossip. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | `"ok"` | +| `400` | Invalid JSON, empty URL, or URL too long | +| `429` | Rate limited | + +**Example** + +```bash +curl -sS -X POST https://relay.example.com/announce \ + -H 'Content-Type: application/json' \ + -d '{"url":"https://my-relay.example.com","capabilities":0}' +``` + +--- + +## `GET /peers` + +Return the list of **verified, non-stale** peer relays known to this node. + +**Response** + +JSON array of `PeerInfo` objects: + +```json +[ + { + "source_ip": "203.0.113.42", + "url": "https://relay-cosmos.spacesprotocol.org", + "capabilities": 0 + } +] +``` + +| Field | Type | Description | +|----------------|--------|-------------| +| `source_ip` | string | IP observed when the peer announced (receiver-attested) | +| `url` | string | Reachable relay URL | +| `capabilities` | u32 | Capability flags | + +Peers whose `last_seen` is older than `verified_ttl` (default 600s) are +omitted. The relay's own `--self-url` is never included. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | JSON array (may be empty) | +| `429` | Rate limited | + +**Example** + +```bash +curl -s https://relay.example.com/peers | jq . +curl -sI https://relay.example.com/peers # HEAD also works (health checks) +``` + +--- + +## `GET /query` + +Resolve one or more handles and return the certificates and chain proof needed +to verify them locally. + +**Query parameters** + +| Param | Required | Description | +|---------|----------|-------------| +| `q` | yes | Comma-separated handles (max 6). Examples: `alice@bitcoin`, `bob@bitcoin`, `@bitcoin` (root zone of a space) | +| `hints` | no | Comma-separated epoch hints: `space:root_hex:height,...`. Lets the relay omit a ZK receipt when the client already has a verifiable epoch cached | + +**Response** + +- `200 OK` with body = **binary borsh-encoded `Message`** +- Header: `cache-control: public, max-age=300` + +The message contains certificates and proofs for each requested handle. Decode +and verify with a Fabric client (`fabric resolve`, or library bindings). + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | Binary `Message` (may be small/empty if handles not found) | +| `400` | Missing `q`, or more than 6 handles | +| `429` | Rate limited | +| `500` | Internal resolve error | + +**Example** + +```bash +# Confirm transport only (body is binary) +curl -sS -o /tmp/zone.msg \ + -w 'http=%{http_code} size=%{size_download}B\n' \ + 'http://127.0.0.1:7778/query?q=user@rad' + +# Full resolve via Fabric CLI +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +--- + +## `GET /anchors` + +Return a trust anchor set used for certificate verification. + +**Query parameters** + +| Param | Required | Description | +|--------|----------|-------------| +| `root` | no | 64-char hex trust id (32 bytes). Return the anchor set matching this root, or `404` if not stored | + +Without `root`, returns the latest anchor set held by this relay. + +**Response headers** (always present when a latest set exists) + +| Header | Description | +|--------------------|-------------| +| `x-anchor-root` | Hex-encoded trust id of the latest anchor set | +| `x-anchor-height` | Block height of the latest receipted anchor | + +**Response body** + +JSON `AnchorSet` with `entries` (array of root anchors). Header +`cache-control: public, max-age=300` on success. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | JSON anchor set | +| `400` | Invalid `root` hex or wrong length | +| `404` | Anchor set not found | +| `429` | Rate limited | + +**Example** + +```bash +# Cheap comparison across relays (no body download) +curl -sI https://relay.example.com/anchors | grep -i '^x-anchor-' + +# Full anchor set +curl -s https://relay.example.com/anchors | jq . + +# Specific trust id +curl -s 'https://relay.example.com/anchors?root=54f37f3308acdb0b2887fef6fde0247a184e33ee14aa6fb34d5f968d81b09205' +``` + +Fabric clients use `HEAD /anchors` to vote on the freshest trust id across +seeds, then `GET /anchors?root=` to download the winning set. + +--- + +## `GET /hints` + +Lightweight freshness check without returning certificates or proofs. + +**Query parameters** + +| Param | Required | Description | +|-------|----------|-------------| +| `q` | yes | Comma-separated handles (max 6), same format as `/query` | + +**Response** + +JSON `HintsResponse`: + +```json +{ + "anchor_tip": 951192, + "hints": [ + { + "epoch_tip": 950988, + "name": "@swifty", + "seq": 0, + "delegate_seq": 0, + "epochs": [ + { + "epoch": 950988, + "res": [{ "seq": 2, "name": "taylor@swifty" }] + } + ] + } + ] +} +``` + +| Field | Description | +|--------------|-------------| +| `anchor_tip` | Current Bitcoin tip height (per relay's spaced) | +| `hints` | Per-space epoch heights and per-handle seq numbers | + +Header: `cache-control: public, max-age=300`. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | JSON hints | +| `400` | Missing `q`, duplicate handles, or more than 6 handles | +| `429` | Rate limited | + +**Example** + +```bash +curl -s 'https://relay.example.com/hints?q=taylor@swifty,@swifty' | jq . +``` + +Fabric uses `/hints` to rank relays by freshness before sending a full +`/query`. + +--- + +## `POST /chain-proof` + +Build a chain proof for a set of spaces and numeric identities. Forwards the +request to the local `spaced` node. + +**Request** + +JSON `ChainProofRequest`: + +| Field | Type | Limit | +|----------|----------|-------| +| `spaces` | string[] | max 6 | +| `nums` | string[] | max 20 | + +**Response** + +- `200 OK` with body = **binary borsh-encoded `ChainProof`** + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | Binary proof | +| `400` | Invalid JSON or too many spaces/nums | +| `429` | Rate limited | +| `500` | Spaced failed to build proof | + +**Example** + +Used internally by `fabric publish` when signing a message. Typically not called +directly by operators. + +--- + +## `GET /reverse` + +Look up human-readable handle names from numeric identities (`num_id`). + +**Query parameters** + +| Param | Required | Description | +|-------|----------|-------------| +| `ids` | yes | Comma-separated numeric ids (max 20) | + +**Response** + +JSON array: + +```json +[ + { "id": "num1qx8dtlzq...", "name": "taylor@swifty" } +] +``` + +Reverse mappings exist only for handles that published with the +`SIG_PRIMARY_ZONE` flag (the default for `fabric publish`). + +Header: `cache-control: public, max-age=300`. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | JSON array (may be partial if some ids unknown) | +| `400` | Missing `ids` or more than 20 ids | +| `429` | Rate limited | +| `500` | Lookup failed | + +**Example** + +```bash +curl -s 'https://relay.example.com/reverse?ids=num1qx8dtlzq...' | jq . +``` + +--- + +## `GET /addrs` + +Find handles that published a given address record. + +**Query parameters** + +| Param | Required | Description | +|--------|----------|-------------| +| `name` | yes | Addr-record protocol key (e.g. `btc`, `eth`, `nostr`) | +| `addr` | yes | Address value to search for | + +**Response** + +```json +{ + "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + "handles": [ + { "handle": "alice@bitcoin", "rev": "alice@bitcoin" } + ] +} +``` + +Header: `cache-control: public, max-age=300`. + +**Responses** + +| Status | Meaning | +|--------|---------| +| `200` | JSON match (handles may be empty) | +| `400` | Missing `name` or `addr` | +| `429` | Rate limited | +| `500` | Lookup failed | + +**Example** + +```bash +curl -s 'https://relay.example.com/addrs?name=btc&addr=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' | jq . +``` + +--- + +## What is not exposed + +| Missing endpoint | Notes | +|------------------|-------| +| `/health` | Docker HEALTHCHECK uses `GET /peers` (+ optional `fabric resolve`); see `docker-healthcheck.sh` | +| Admin / config | No shutdown, reload, or stats routes | +| Authentication | All endpoints are public | +| Metrics | No Prometheus scrape endpoint; use `RUST_LOG` and log grep | + +--- + +## Quick smoke test + +```bash +RELAY=http://127.0.0.1:7778 + +curl -s "$RELAY/peers" | head -c 200; echo +curl -s "$RELAY/anchors" | head -c 200; echo +curl -s "$RELAY/hints?q=@rad" +curl -sS -o /dev/null -w 'query http=%{http_code} size=%{size_download}B\n' \ + "$RELAY/query?q=user@rad" +``` + +For decoded handle resolution, use the Fabric client — see [`RESOLVE.md`](RESOLVE.md). diff --git a/Cargo.lock b/Cargo.lock index f966c3e..ff5f8f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2764,6 +2764,7 @@ dependencies = [ "hex", "libveritas", "libveritas_testutil", + "log", "rand 0.9.4", "reqwest", "rusqlite", diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0bad4ee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,153 @@ +# syntax=docker/dockerfile:1.7 +# +# Multi-stage build for the `certrelay` binary. +# +# Stage 1 ("builder") compiles the Rust workspace against musl on Alpine, +# producing a statically-linked binary. Stage 2 ("runtime") copies only +# the binary, a tiny entrypoint, and the minimum runtime packages +# (ca-certificates + tini) into a clean Alpine image, so the resulting +# image contains no Rust toolchain, no source code and no C build chain. +# +# Build: docker build -t certrelay:latest . +# Run: docker run --rm -p 7778:7778 -v certrelay-data:/data \ +# --env-file <(grep ^export setup-spaced-prod-env.sh | sed 's/^export //') \ +# certrelay:latest + +# NOTE on RUST_VERSION: +# Cargo.toml declares `rust-version = "1.85"` as the workspace MSRV, but +# the locked transitive deps (spaces_*, icu_*, borsh_utils, sip7, ...) +# already require rustc >= 1.88. We pin to a comfortably newer stable +# here so `cargo build --locked` succeeds without touching Cargo.lock. +# Override at build time with: --build-arg RUST_VERSION=1.91 (etc). +ARG RUST_VERSION=1.90 +ARG RUST_ALPINE_VERSION=3.21 +ARG RUNTIME_ALPINE_VERSION=3.21 + +# --------------------------------------------------------------------------- +# Stage 1: build +# --------------------------------------------------------------------------- +FROM rust:${RUST_VERSION}-alpine${RUST_ALPINE_VERSION} AS builder + +# Build-time toolchain needed for: +# build-base, musl-dev -> C toolchain for rusqlite (bundled sqlite), +# yuki and other native deps +# clang, llvm -> some -sys crates prefer clang +# cmake -> aws-lc / ring style native builds +# perl -> ring's build.rs +# pkgconf, linux-headers -> assorted -sys crates +# git -> resolving any git deps in Cargo.lock +# +# NONE of these end up in the final image. +RUN apk add --no-cache \ + build-base \ + musl-dev \ + clang \ + llvm \ + cmake \ + perl \ + pkgconf \ + linux-headers \ + git + +# NOTE: do NOT set `RUSTFLAGS="-C target-feature=-crt-static"`. On the +# rust:*-alpine images the default Rust target is *-unknown-linux-musl +# with crt-static enabled, which produces a fully static binary that +# needs nothing from the runtime image. Disabling crt-static would force +# a dynamic dependency on libgcc_s.so.1 / libunwind, which aren't present +# in the clean `alpine:*` runtime stage and would fail with +# "Error loading shared library libgcc_s.so.1" at container start. +ENV CARGO_TERM_COLOR=always \ + CARGO_NET_RETRY=10 + +WORKDIR /src + +# Copy the whole workspace. The .dockerignore keeps `target/`, `data/`, +# language-binding build artefacts and other noise out of the context. +COPY . . + +# Build three binaries from the workspace: +# * certrelay (the relay server itself) +# * fabric (the Fabric client CLI - used by HEALTHCHECK to verify the +# relay can actually resolve a subname end-to-end, including +# cert-chain decoding and verification) +# * monitor (bootstrap relay health monitor - polls BOOTSTRAP_RELAYS) +# BuildKit caches the cargo registry and target dir across rebuilds, but +# nothing from those caches is copied into the runtime image. +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=/src/target,sharing=locked \ + cargo build --release --locked \ + -p relay --bin certrelay \ + -p relay --bin monitor \ + -p fabric-resolver --bin fabric \ + && cp /src/target/release/certrelay /usr/local/bin/certrelay \ + && cp /src/target/release/monitor /usr/local/bin/monitor \ + && cp /src/target/release/fabric /usr/local/bin/fabric \ + && strip /usr/local/bin/certrelay /usr/local/bin/monitor /usr/local/bin/fabric + +# --------------------------------------------------------------------------- +# Stage 2: runtime +# --------------------------------------------------------------------------- +FROM alpine:${RUNTIME_ALPINE_VERSION} AS runtime + +# ca-certificates -> outbound HTTPS to checkpoint/peer relays +# tini -> proper PID 1 / signal forwarding +# tzdata -> stable timestamp formatting in logs +RUN apk add --no-cache \ + ca-certificates \ + tini \ + tzdata \ + && addgroup -S certrelay \ + && adduser -S -G certrelay -h /data -s /sbin/nologin certrelay \ + && mkdir -p /data \ + && chown -R certrelay:certrelay /data + +# Binaries + entrypoint + healthcheck +COPY --from=builder /usr/local/bin/certrelay /usr/local/bin/certrelay +COPY --from=builder /usr/local/bin/monitor /usr/local/bin/monitor +COPY --from=builder /usr/local/bin/fabric /usr/local/bin/fabric +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY docker-healthcheck.sh /usr/local/bin/docker-healthcheck.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh \ + /usr/local/bin/docker-healthcheck.sh \ + /usr/local/bin/certrelay \ + /usr/local/bin/monitor \ + /usr/local/bin/fabric + +# Sensible container-friendly defaults. Every one of these can be overridden +# at `docker run` time via `-e VAR=value` or `--env-file`. The names match +# the env vars exported by ./setup-spaced-prod-env.sh and the `env = ...` +# attributes declared on the `Args` struct in relay/src/app.rs. +ENV CERTRELAY_CHAIN=mainnet \ + CERTRELAY_DATA_DIR=/data \ + CERTRELAY_BIND=0.0.0.0 \ + CERTRELAY_PORT=7778 \ + CERTRELAY_BOOTSTRAP=false \ + CERTRELAY_ANCHOR_REFRESH=300 \ + RUST_LOG=info \ + CHECK_INTERVAL=30 \ + REQUEST_TIMEOUT=15 + +VOLUME ["/data"] +EXPOSE 7778/tcp + +# HEALTHCHECK contract: +# * --start-period=120s first 2 minutes after start are graced so the +# anchor-refresh / checkpoint work doesn't flap +# the status from "starting" to "unhealthy" +# * --interval=30s normal cadence between probes +# * --timeout=15s covers wget(/peers, 5s) + fabric resolve(~5s) +# * --retries=3 three consecutive failures => unhealthy +# +# Liveness (always): GET /peers must return 200. +# Resolve (opt-in): if CERTRELAY_HEALTHCHECK_HANDLE is set, the bundled +# `fabric` CLI must resolve that handle against this +# relay and return a zone JSON containing the handle. +HEALTHCHECK --interval=30s --timeout=15s --start-period=120s --retries=3 \ + CMD ["/usr/local/bin/docker-healthcheck.sh"] + +USER certrelay +WORKDIR /data + +ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] +CMD ["certrelay"] diff --git a/MINTING.md b/MINTING.md new file mode 100644 index 0000000..ef5ec39 --- /dev/null +++ b/MINTING.md @@ -0,0 +1,258 @@ +# How a Handle Gets Minted + +Minting (creating a new subname like `user@rad`) happens **outside `certrelay`** +entirely. `certrelay` only stores and serves the artifacts of a mint that has +already occurred upstream in the Spaces tooling +([`spacesprotocol/spaces`](https://github.com/spacesprotocol/spaces) — the +`space-cli` + `subs` binaries). This is why the README keeps saying *"the +handle must already exist on a reachable relay before you can publish records +under it."* + +This document traces where each piece in a `.spacecert` file (e.g. +`data/certs/user@rad.cert.json`) comes from. + +## The two-party model + +Minting `user@rad` involves two distinct actors with distinct keys: + +| Party | Owns | What they do | +|---|---|---| +| **Parent-space operator** for `@rad` | The on-chain `@rad` space (a Bitcoin UTXO) | Issues batches of subnames by signing a Merkle root and anchoring it to Bitcoin | +| **End user** wanting `user@rad` | A fresh BIP-340 keypair they generate themselves | Submits a request to the operator, then later uses the secret to sign records via `fabric publish` | + +The protocol's "trustless" property comes from the fact that the operator can +never forge a handle to a key the end user didn't generate — the user's pubkey +is part of what gets hashed into the Merkle leaf, and the user keeps the +secret. + +## The minting flow, step by step + +This is the `space-cli` / `subs` pipeline documented in +[`SUBSPACES.md`](https://github.com/spacesprotocol/spaces/blob/main/SUBSPACES.md) +in the upstream `spaces` repo. `certrelay` participates only in the very last +step. + +### 1. Operator brings `@rad` online + +The operator first registers and "operates" the parent space on Bitcoin: + +```bash +space-cli operate @rad +``` + +This sets up `@rad`'s on-chain state so the operator can anchor subspace tree +roots into it. Done once, lives on Bitcoin forever. + +### 2. End user generates a keypair and a request + +The end user runs `subs` locally: + +```bash +subs request user@rad +``` + +This produces a `user@rad.req.json` containing the user's freshly-generated +x-only public key plus the desired handle. **The user's 32-byte secret never +leaves their machine.** That secret is exactly the same hex blob you'll +eventually feed to `fabric publish --secret-key-file`. + +### 3. Operator accepts the request + +The operator collects requests (typically many at once — that's the "batch +issuance" advantage Spaces emphasizes) and folds them into their subspace +tree: + +```bash +subs add user@rad.req.json +subs add bob@rad.req.json +subs add carol@rad.req.json +... +``` + +Each `add` inserts a leaf in the operator's local Merkle tree. The leaf hashes +`(handle_label, user_pubkey, ...)` together. Millions of subnames collapse +into a single 32-byte root. + +### 4. Operator commits the tree root on Bitcoin + +Periodically the operator pushes the new tree root to chain. Mechanically this +is a Bitcoin transaction signed by the `@rad` space UTXO that publishes the +new 32-byte root as the "current state" of the `@rad` subspace tree. After +confirmation, that root is anchored to a specific Bitcoin block height — this +is the "anchor" that `certrelay` (via `spaced`) refreshes every +`CERTRELAY_ANCHOR_REFRESH` seconds and exposes on `GET /anchors`. + +After this confirms, `user@rad` is officially minted: a Merkle inclusion proof +exists that links `(user, user_pubkey)` to a root that's permanently anchored +to Bitcoin proof-of-work. + +### 5. Operator hands the end user a `.spacecert` + +The operator exports the two-piece certificate proving that minting happened +and gives it to the user. The file is a small JSON envelope with two base64 +fields: + +```json +{ + "root_cert": "AANyYWQA…", + "handle_cert": "AAVvdGhlcgNyYWQA…" +} +``` + +That's literally the format of `data/certs/user@rad.cert.json` and +`data/certs/other@rad.cert.json` in this repo. Decoded: + +| Field | What it contains | +|---|---| +| `root_cert` | Proof that `@rad` exists as a registered space (root-level certificate, label = `rad`) | +| `handle_cert` | Proof that `user@rad` is bound to a specific x-only pubkey, plus a Merkle path from that leaf up to the tree root that was anchored in step 4 | + +You can see this is exactly what `fabric.export()` reconstructs by walking the +resolution upward from leaf to root: + +```rust +// fabric/rust/src/client.rs +pub async fn export(&self, handle: &str) -> Result> { + let sname = SName::try_from(handle)?; + let lookup = libveritas::names::Lookup::new(vec![sname.clone()]); + let mut all_verified: Vec = Vec::new(); + + let mut prev_batch: Vec = Vec::new(); + let mut batch: Vec = lookup.start(); + while !batch.is_empty() { + if batch == prev_batch { break; } + let strs: Vec = batch.iter().map(|s| s.to_string()).collect(); + let refs: Vec<&str> = strs.iter().map(|s| s.as_str()).collect(); + let (verified, _) = self.resolve_flat(&refs, false).await?; + prev_batch = batch; + batch = lookup.advance(&verified.zones); + all_verified.push(verified); + } + + let mut certs = Vec::new(); + for msg in &all_verified { + certs.extend(msg.certificates()); + } + let chain = CertificateChain::new(sname, certs); + Ok(chain.to_bytes()) +} +``` + +So the cert file is just a portable snapshot of "the certificates a relay +would hand you if you resolved `user@rad` right now." + +### 6. End user publishes records via `certrelay` + +Now — and only now — `certrelay` enters the picture. The user uses their +stashed secret + the `.spacecert` to sign a `RecordSet` and POST it to a +relay's `/message`, exactly as documented for `fabric publish`. The relay +verifies that: + +- the handle cert chains back to the root cert, +- the root cert chains back to a Bitcoin-anchored state root the relay knows + about (via its `/anchors` set, refreshed from `spaced`), +- the new records' Schnorr signature validates against the pubkey embedded in + the handle cert, +- the `seq` is strictly greater than what's currently stored. + +If all four pass, the relay stores and gossips. If any fail, `/message` +returns 4xx and nothing propagates. See the `Handler::handle_message` / +`gossip_message` path in `relay/src/handler.rs` and `relay/src/http.rs`. + +## Optional step 7 — bind the handle to a Bitcoin UTXO (`space pointer`) + +For handles that need on-chain interactivity (e.g. being sold, transferred via +Bitcoin transactions, or used as an L1 identity), the user can additionally +mint a **space pointer (sptr)** linking their off-chain handle to an on-chain +UTXO with the same script pubkey, as shown in [`SUBSPACES.md`](https://github.com/spacesprotocol/spaces/blob/main/SUBSPACES.md): + +```bash +space-cli createptr 5120d3c3196cb3ed7fa79c882ed62f8e5942e546130d5ae5983da67dbb6c9bdd2e79 +# → sptr13thcluavwywaktvv466wr6hykf7x5avg49hgdh7w8hh8chsqvwcskmtxpd +``` + +This is *purely optional*. Plain off-chain handles (the common case) skip it +and live entirely as Merkle leaves under the operator's tree root, with no +per-handle on-chain footprint. Every `user@rad`-style mint by default takes +**zero extra block space** beyond the operator's periodic batched root +commitment — that's the scalability story. + +## Why `certrelay` doesn't mint + +Minting requires: + +- Signing transactions with the `@rad` space's UTXO key (operator-only). +- Submitting those transactions to Bitcoin and waiting for confirmations. +- Maintaining the local subspace Merkle tree across batches. + +None of that is in `certrelay`'s scope. `certrelay` is intentionally a +**post-mint relay**: its only chain interaction is *reading* anchor roots from +`spaced` (via `CERTRELAY_SPACED_RPC_URL`) so it can verify that the certs +people publish chain back to real Bitcoin-anchored roots. The relay does +space-level rate limits (100 handle updates/min per space — see +`relay/src/handler.rs`) but never *creates* a handle, only validates incoming +messages against existing chain anchors. + +This separation is also why the cert files in `data/certs/` had to be given +to you (or pulled out of an operator's tooling) ahead of time — neither +`certrelay` nor `fabric` can produce them. They are the operator's signed +attestation of the mint, exported once and reused for every subsequent +`fabric publish`. + +## End-to-end: who runs what + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PARENT SPACE OPERATOR (owns @rad UTXO) │ +│ │ +│ space-cli operate @rad ← one-time, on Bitcoin │ +│ subs add user@rad.req.json ← inserts user into Merkle tree │ +│ space-cli commit ... ← anchors tree root to Bitcoin │ +│ exports user@rad.cert.json ← gives the .spacecert to user │ +└──────────────────────────────────────┬──────────────────────────────────┘ + │ (out-of-band: email, QR, etc.) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ END USER │ +│ │ +│ subs request user@rad ← generated keypair earlier │ +│ ./user-rad-secret.hex ← the 32-byte secret │ +│ ./user@rad.cert.json ← received from operator │ +│ │ +│ fabric publish --secret-key-file ./user-rad-secret.hex \ │ +│ --txt website=https://example.com \ │ +│ --seeds http://127.0.0.1:7778 user@rad │ +└──────────────────────────────────────┬──────────────────────────────────┘ + │ POST /message (binary) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ CERTRELAY (this repo) │ +│ │ +│ - Verifies handle_cert → root_cert → anchored Bitcoin state root │ +│ - Verifies Schnorr sig on records against handle's pubkey │ +│ - Stores in SQLite │ +│ - Gossips to up to 4 random verified peers │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## TL;DR + +- **Minting = the parent-space operator (`@rad`) adding your `user@rad` + request to their off-chain Merkle tree and committing the new root on + Bitcoin via `space-cli` / `subs`.** Done outside this repo, in the + [`spacesprotocol/spaces`](https://github.com/spacesprotocol/spaces) tooling. +- The operator hands you a `.spacecert` file (`root_cert` + `handle_cert`). + The files in `data/certs/` are exactly that artifact, base64-wrapped. +- You generated the 32-byte secret yourself before submitting the request; + the operator never sees or signs anything with it. +- `certrelay` enters only after minting: it verifies the cert chain back to + the on-chain anchor, then serves and gossips whatever records you sign with + your secret via `fabric publish`. +- Optional `space-cli createptr` gives the handle an on-chain UTXO if you + need transferability; most off-chain handles skip it. + +For the canonical step-by-step commands and the exact wire format of the +request and the tree commitment, the upstream reference is +[`SUBSPACES.md`](https://github.com/spacesprotocol/spaces/blob/main/SUBSPACES.md) +in the `spaces` repo — that's the source of truth for the minting half, just +as this repo is the source of truth for the relay half. diff --git a/README.md b/README.md index acb09d0..dde64b6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ The protocol is plain HTTP — relays are queryable from browsers, mobile apps, ## Fabric Client -For documentation on using Fabric to resolve handles, publish records, and verify identities, see **[spacesprotocol.org/docs](https://spacesprotocol.org/docs)**. +For full reference docs (resolve, publish, sign, verify, badge / trust-pinning), see **[spacesprotocol.org/docs](https://spacesprotocol.org/docs)**. + +The [Querying with the Fabric client](#querying-with-the-fabric-client) section below shows how to point every binding at a relay you're running yourself. ## Running a Relay @@ -44,6 +46,235 @@ No external Bitcoin node required. Data is stored in `~/.certrelay` by default. | `--is-bootstrap` | `CERTRELAY_BOOTSTRAP` | `false` | Run as a bootstrap node | | `--skip-checkpoint-sync` | - | `false` | Skip checkpoint download, sync from scratch | +### Querying with the Fabric client + +Once `certrelay` has reached the chain tip it serves the Fabric HTTP API on `http://127.0.0.1:7778`. Every Fabric binding can be pointed at that URL by passing it as a *seed*. Two notes that apply to all the examples below: + +- `devMode` / `dev_mode` / `--dev-mode` skips finality (work-depth) checks during verification. **You usually want this when querying your own relay**, because a freshly-synced local node may briefly produce zones whose anchors haven't yet accumulated enough confirmations for the strict default policy. +- Without `--trust-id` (or `Fabric::trust(...)`), zones come back with a `badge` of `unverified`. That's expected — pin a trust id from a QR / Veritas desktop scan when you want the orange-checkmark badge. + +#### Verify the relay is responding (no client library) + +```bash +# Liveness + peer / anchor snapshots are JSON and curl-friendly. +curl -s http://127.0.0.1:7778/peers | head -c 200; echo +curl -s http://127.0.0.1:7778/anchors | head -c 200; echo +curl -s 'http://127.0.0.1:7778/hints?q=@rad' + +# /query is GET with ?q=[,...] (max 6). +# The response body is a borsh-encoded `Message` (binary), so curl can +# only confirm HTTP status + size; use one of the Fabric clients below +# to actually decode and verify the zone. +curl -sS -o /tmp/zone.msg \ + -w 'http=%{http_code} size=%{size_download}B\n' \ + 'http://127.0.0.1:7778/query?q=user@rad' +# expected: http=200 size= (subname found, with proof) +# vs. http=200 size= (subname not present) +``` + +#### JavaScript / TypeScript + +```bash +npm install @spacesprotocol/fabric-web +``` + +```javascript +import { Fabric } from "@spacesprotocol/fabric-web"; + +const fabric = new Fabric({ + seeds: ["http://127.0.0.1:7778"], + devMode: true, +}); + +const { zone } = await fabric.resolve("user@rad"); +console.log(JSON.stringify(zone.toJson(), null, 2)); +``` + +A runnable copy lives at [`fabric/js/examples/query-local.mjs`](fabric/js/examples/query-local.mjs): + +```bash +cd fabric/js +node examples/query-local.mjs +``` + +CLI form (handy for shell scripts): + +```bash +node fabric-web/dist/cli.js --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +#### Rust + +```bash +cargo add fabric-resolver +``` + +```rust +use fabric::client::Fabric; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let fabric = Fabric::with_seeds(&["http://127.0.0.1:7778"]) + .with_dev_mode(); + + if let Some(zone) = fabric.resolve("user@rad").await? { + println!("{}: {:?}", zone.handle, zone.sovereignty); + } else { + println!("handle not found"); + } + Ok(()) +} +``` + +CLI form (installs a `fabric` binary alongside `certrelay`): + +```bash +cargo install --git https://github.com/spacesprotocol/certrelay.git --bin fabric +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +#### Python + +```bash +pip install fabric-resolver +``` + +```python +import asyncio +from fabric import Fabric + +async def main(): + fabric = Fabric(seeds=["http://127.0.0.1:7778"], dev_mode=True) + zone = await fabric.resolve("user@rad") + if zone is None: + print("handle not found") + else: + print(f"{zone.handle}: {zone.sovereignty}") + +asyncio.run(main()) +``` + +CLI form: + +```bash +python -m fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +#### Go + +```bash +go get github.com/spacesprotocol/fabric-go +``` + +```go +package main + +import ( + "fmt" + "log" + + fabric "github.com/spacesprotocol/fabric-go" +) + +func main() { + f := fabric.New() + f.SetSeeds([]string{"http://127.0.0.1:7778"}) + f.SetDevMode(true) + + zone, err := f.Resolve("user@rad") + if err != nil { + log.Fatal(err) + } + if zone == nil { + fmt.Println("handle not found") + return + } + fmt.Printf("%s: %s\n", zone.Handle, zone.Sovereignty) +} +``` + +#### Publishing to a local relay + +The handle must already exist on a reachable relay (i.e. its certificate +chain was previously minted by the parent space owner via `spaces-cli` or +the Veritas desktop). Publishing then signs a new `RecordSet` under that +existing handle and broadcasts it. + +The fastest path is the `fabric publish` CLI shipped alongside `certrelay` +in this repo (and in the Docker image): + +```bash +SECRET=$(cat ./user-rad-secret.hex) # 64-char hex BIP-340 secret key + +fabric publish \ + --seeds http://127.0.0.1:7778 \ + --dev-mode \ + --secret-key-env SECRET \ + --txt website=https://example.com \ + --addr btc=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 \ + --addr nostr=npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 \ + user@rad +# stdout: {"handle":"user@rad","seq":2,"primary":true,"txts":1,"addrs":2} +``` + +`fabric publish` automatically resolves the handle first to pick the next +`seq` number, exports the existing certificate chain, signs each block +with the supplied key, sets `SIG_PRIMARY_ZONE` (override with +`--no-primary`) and broadcasts to the relay's `/message` endpoint. Use +`--seq ` to override the auto-bump and `--dry-run` to print the +would-be payload as JSON without broadcasting. See +`fabric publish --help` for the full list of flags. + +The same flow is available programmatically in every binding (the CLI +is a thin wrapper over `fabric.publish(cert, records, secretKey, primary)`): + +1. Construct `Fabric` pointing at your local relay. +2. `cert = await fabric.export("user@rad")` — pull the existing certificate chain. +3. Build a `RecordSet` of the records you want to publish (include `Record::seq(N)` strictly greater than the currently-stored seq). +4. `await fabric.publish({ cert, records, secretKey, primary: true })`. + +End-to-end library examples that pack records, sign, and broadcast in +every binding live under [`fabric/examples/`](fabric/examples/) — they +default to public seeds, so swap in `seeds: ["http://127.0.0.1:7778"]` +(and enable `devMode` if your local relay isn't yet caught up to a +finalized epoch) to target a relay you're running yourself. + +### Docker + +A multi-stage Alpine `Dockerfile` is included in the repo. It produces a +~30 MB image whose runtime layer contains only `ca-certificates`, `tini`, +`tzdata`, the statically-linked `certrelay` binary and the `fabric` +client binary used by the built-in healthcheck. + +```bash +docker build -t certrelay:latest . + +# Run with a named volume for persistence + a healthcheck-driven +# subname probe (any handle that should resolve via this relay): +docker run -d --name certrelay \ + -p 7778:7778 \ + -v certrelay-data:/data \ + -e CERTRELAY_CHAIN=mainnet \ + -e CERTRELAY_BIND=0.0.0.0 \ + -e CERTRELAY_SELF_URL=https://relay.example.com \ + -e CERTRELAY_HEALTHCHECK_HANDLE=user@rad \ + certrelay:latest + +docker ps # STATUS column should flip to "(healthy)" after start-up +``` + +`CERTRELAY_HEALTHCHECK_HANDLE` is optional. When set, the container +HEALTHCHECK runs `fabric --seeds http://127.0.0.1:$PORT --dev-mode ` +every 30s (after a 2-minute startup grace) and marks the container +`unhealthy` after 3 consecutive failures — i.e. you get *automatic* +"is the relay still resolving subnames?" monitoring straight from +`docker ps` / your orchestrator. + +When the variable is unset, the healthcheck still verifies that +`GET /peers` answers, so a crashed or wedged relay is still surfaced. + +See `NOTES.md` for additional bind-mount / `--env-file` recipes. + ### Public relay behind a reverse proxy ```bash @@ -61,4 +292,4 @@ certrelay --spaced-rpc-url http://user:password@127.0.0.1:12888 ## License -MIT \ No newline at end of file +MIT diff --git a/RESOLVE.md b/RESOLVE.md new file mode 100644 index 0000000..2f2cd9c --- /dev/null +++ b/RESOLVE.md @@ -0,0 +1,290 @@ +# Resolving Handles with `fabric` + +`fabric` is the client side of the certrelay protocol. It turns a handle like +`user@rad` into the owner-signed `RecordSet` published under it, plus the +cryptographic proofs (chain commitment, Merkle path) needed to verify the +result against Bitcoin's chain state. + +This document covers querying only. For publishing, see the `Publishing to a +local relay` section in [`README.md`](README.md). For minting (how a handle +gets created in the first place), see [`MINTING.md`](MINTING.md). + +## 1. The CLI (the quick path) + +The Docker image ships a `fabric` binary used by the built-in healthcheck. +From inside the container: + +```bash +docker exec -i certrelay fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +From the host, after `cargo install --git https://github.com/spacesprotocol/certrelay.git --bin fabric`: + +```bash +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +Output is one JSON object per resolved handle on stdout (one line per handle). +Anything that didn't resolve goes to stderr as `: not found`. + +```json +{ + "handle": "user@rad", + "sovereignty": "Sovereign", + "records": { + "seq": 2, + "txt": { "website": ["https://example.com"] }, + "addr": { "btc": ["bc1q..."] } + }, + "badge": "unverified", + "commitment": { ... } +} +``` + +### Flags + +``` +Usage: fabric resolve [OPTIONS] [ ...] + +Options: + --seeds Seed relay URLs (comma-separated) + --trust-id Trust ID for verification + --dev-mode Enable dev mode (skip finality checks) + -h, --help Show this help +``` + +| Flag | When you need it | +|---|---| +| `--seeds` | Always, unless you want the built-in public seeds (`relay-cosmos`, `relay-atlas`). Use `http://127.0.0.1:7778` for a local relay. | +| `--dev-mode` | **Almost always set against your local relay.** A freshly-synced node may produce zones whose anchors haven't yet accumulated enough work-depth for the strict default policy. Without `--dev-mode` you'll see verification failures even though the data is correct. | +| `--trust-id` | Set when you want the verified-badge state. Without it, every zone comes back with `"badge": "unverified"`. | +| (positional) | One or more handles. Up to 6 per request — the relay's `/query` enforces `MAX_HANDLES = 6`. | + +### Backwards-compat shortcut + +The dispatcher in `fabric/rust/src/main.rs` treats the no-subcommand form as +an implicit `resolve`, so these two are equivalent: + +```bash +fabric resolve --seeds http://127.0.0.1:7778 --dev-mode user@rad +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad +``` + +### Multiple handles in one call + +Just list them. They batch into a single `GET /query` call per relay: + +```bash +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad other@rad @rad +``` + +`@rad` (no label) is valid and resolves the **root zone** of the space — +useful for inspecting the space's chain commitment without resolving a +specific subname. + +### Filter for what you actually want + +`fabric` always emits the full zone JSON; pair it with `jq` for surgical +extraction: + +```bash +# Just the records the handle is publishing +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad | jq '.records' + +# Just one address (e.g. btc) +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad \ + | jq -r '.records.addr.btc[0]' + +# Current seq (useful before a manual republish) +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad | jq '.records.seq' + +# Sovereignty state +fabric --seeds http://127.0.0.1:7778 --dev-mode user@rad | jq '.sovereignty' +``` + +### Multiple seeds for resilience + +```bash +fabric --seeds http://127.0.0.1:7778,https://relay-cosmos.spacesprotocol.org \ + --dev-mode user@rad +``` + +Fabric picks among them based on freshness (it calls `/hints` first to see +who has the newest data). Useful when you don't fully trust your local +relay's view yet but want it tried first. + +## 2. The library form + +The CLI is a thin wrapper around `fabric.resolve()`. All bindings share the +same shape: + +1. Construct `Fabric` with seeds + (optional) dev-mode. +2. `await fabric.resolve(handle)` — returns `null` / `None` / `nil` if not + found, or a `Zone`-shaped object. +3. Inspect `.records`, `.sovereignty`, `.commitment`, etc. + +### Rust + +```rust +use fabric::client::Fabric; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let fabric = Fabric::with_seeds(&["http://127.0.0.1:7778"]) + .with_dev_mode(); + + if let Some(zone) = fabric.resolve("user@rad").await? { + println!("{}: {:?}", zone.handle, zone.sovereignty); + } else { + println!("handle not found"); + } + Ok(()) +} +``` + +### JavaScript / TypeScript + +A runnable copy lives at `fabric/js/examples/query-local.mjs`: + +```javascript +import { Fabric } from "@spacesprotocol/fabric-web"; + +const RELAY = "http://127.0.0.1:7778"; +const HANDLE = "user@rad"; + +const fabric = new Fabric({ + seeds: [RELAY], + devMode: true, +}); + +try { + const { zone } = await fabric.resolve(HANDLE); + console.log(JSON.stringify(zone.toJson(), null, 2)); +} catch (e) { + console.error(`Failed to resolve ${HANDLE}:`, e.message ?? e); + process.exit(1); +} +``` + +Run it directly: + +```bash +cd fabric/js +node examples/query-local.mjs +``` + +### Python + +```python +import asyncio +from fabric import Fabric + +async def main(): + fabric = Fabric(seeds=["http://127.0.0.1:7778"], dev_mode=True) + zone = await fabric.resolve("user@rad") + if zone is None: + print("handle not found") + else: + print(f"{zone.handle}: {zone.sovereignty}") + +asyncio.run(main()) +``` + +### Go + +```go +package main + +import ( + "fmt" + "log" + + fabric "github.com/spacesprotocol/fabric-go" +) + +func main() { + f := fabric.New() + f.SetSeeds([]string{"http://127.0.0.1:7778"}) + f.SetDevMode(true) + + zone, err := f.Resolve("user@rad") + if err != nil { + log.Fatal(err) + } + if zone == nil { + fmt.Println("handle not found") + return + } + fmt.Printf("%s: %s\n", zone.Handle, zone.Sovereignty) +} +``` + +Kotlin and Swift bindings follow the same pattern — see their READMEs under +[`fabric/`](fabric/). + +## 3. What you get back + +A `Zone` object (or null/None/nil if not found). The key fields: + +| Field | What it tells you | +|---|---| +| `handle` | The canonical name, e.g. `user@rad` | +| `sovereignty` | `Sovereign` if the handle's certificate is final / irrevocable; `Subspace` if it's still in a mutable Merkle tree under its parent space; sometimes `Unknown` | +| `records` | The owner-signed `RecordSet` — what `fabric publish` writes. `records.seq`, `records.txt`, `records.addr`, etc. | +| `commitment` | The on-chain commitment that anchors this zone (block height, state root). Useful for proving freshness. | +| `num_id` | The numeric identity, used for `GET /reverse` lookups (only present if `SIG_PRIMARY_ZONE` was set when publishing) | +| `delegate` | Optional delegation target — present if the handle delegated its records to another zone | +| `badge` | `verified` if you pinned a `--trust-id` that matches; `unverified` otherwise | + +## 4. Verifying without any client library + +To bypass the libraries entirely while debugging, the HTTP API is +curl-friendly: + +```bash +# Liveness — JSON +curl -s http://127.0.0.1:7778/peers | head -c 200; echo +curl -s http://127.0.0.1:7778/anchors | head -c 200; echo + +# Lightweight existence check — JSON, no proof +curl -s 'http://127.0.0.1:7778/hints?q=user@rad' + +# Full resolve — binary borsh-encoded Message +curl -sS -o /tmp/zone.msg \ + -w 'http=%{http_code} size=%{size_download}B\n' \ + 'http://127.0.0.1:7778/query?q=user@rad' +# expected: http=200 size= → resolved, with proof +# vs. http=200 size= → handle not present +``` + +`/query` returns a binary `borsh`-encoded `Message`. curl can only confirm +transport (status code + body size). To actually decode and verify the +payload you need one of the libraries — that's the point of having clients in +five languages. + +## 5. Common gotchas + +| Symptom | Cause | Fix | +|---|---|---| +| `: not found` but you just published | Handle was never minted, OR the relay you're querying isn't peered with one that has the cert | Verify with `curl http://127.0.0.1:7778/hints?q=user@rad`; if empty, give peers time to gossip or query a seed directly | +| Returns but `badge: "unverified"` | Expected default — no trust ID pinned | Pass `--trust-id ` if you want verified-badge state | +| "verification failed" with no `--dev-mode` | Local relay isn't yet at chain tip; anchors lack work-depth | Add `--dev-mode` against local relays | +| Stale `records.seq` after a publish | New `seq` wasn't strictly greater, or gossip hasn't propagated yet | Re-publish with explicit `--seq `; or wait ~30s and re-query | +| Multiple handles partially fail | Hit `MAX_HANDLES = 6` per request, or some weren't found | Split into smaller batches; check stderr for which ones failed | + +## TL;DR — single-command recipe + +For a local Docker relay with a minted handle: + +```bash +docker exec -i certrelay fabric \ + --seeds http://127.0.0.1:7778 \ + --dev-mode \ + user@rad +``` + +Output is one JSON line per resolved handle. Pipe through `jq '.records'` to +see exactly what records are published, or `jq '.sovereignty'` to check +whether the cert is final. This is the same call the container's +`HEALTHCHECK` makes every 30 seconds against `CERTRELAY_HEALTHCHECK_HANDLE`, +so if `docker ps` shows `(healthy)`, this command is already working. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..d6e8273 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# certrelay container entrypoint. +# +# Responsibilities: +# 1. Apply container-friendly defaults for the CERTRELAY_* env vars when +# the caller has not set them explicitly. The defaults mirror the +# shape of setup-spaced-prod-env.sh so that "source that file, then +# `docker run --env-file ...`" Just Works. +# 2. Make sure CERTRELAY_DATA_DIR exists and is writable by the current +# uid before we exec the relay (the relay also tries to create it, +# but failing here gives a clearer error message than a panic later). +# 3. Print a one-shot summary of the env certrelay will actually see, so +# operators can verify the configuration was wired through the +# container correctly. +# 4. exec the binary so signals from tini (PID 1) reach it directly. + +set -eu + +: "${CERTRELAY_CHAIN:=mainnet}" +: "${CERTRELAY_DATA_DIR:=/data}" +: "${CERTRELAY_BIND:=0.0.0.0}" +: "${CERTRELAY_PORT:=7778}" +: "${CERTRELAY_BOOTSTRAP:=false}" +: "${CERTRELAY_ANCHOR_REFRESH:=300}" + +export CERTRELAY_CHAIN CERTRELAY_DATA_DIR CERTRELAY_BIND CERTRELAY_PORT \ + CERTRELAY_BOOTSTRAP CERTRELAY_ANCHOR_REFRESH + +# Ensure the data dir is usable. The relay will create it too, but doing it +# here gives a precise error if a volume was mounted with the wrong owner. +if ! mkdir -p "${CERTRELAY_DATA_DIR}" 2>/dev/null; then + echo "certrelay: cannot create data dir ${CERTRELAY_DATA_DIR}" >&2 + exit 1 +fi +if [ ! -w "${CERTRELAY_DATA_DIR}" ]; then + echo "certrelay: data dir ${CERTRELAY_DATA_DIR} is not writable by uid $(id -u)" >&2 + echo " (chown the host volume to the 'certrelay' user inside the image)" >&2 + exit 1 +fi + +# Redact secrets that may appear in URLs (basic auth) before logging. +redact() { + # turns scheme://user:pass@host -> scheme://***:***@host + printf '%s' "${1:-}" | sed -E 's#(://)[^:@/]+:[^@/]+@#\1***:***@#g' +} + +cat <} + CERTRELAY_SPACED_RPC_URL = $(redact "${CERTRELAY_SPACED_RPC_URL:-}") + CERTRELAY_REMOTE_IP_HEADER = ${CERTRELAY_REMOTE_IP_HEADER:-} + CERTRELAY_BOOTSTRAP = ${CERTRELAY_BOOTSTRAP} + CERTRELAY_ANCHOR_REFRESH = ${CERTRELAY_ANCHOR_REFRESH} + RUST_LOG = ${RUST_LOG:-info} +EOF + +# If the user passed extra args (or only flags), forward them to certrelay. +# If they passed nothing or just "certrelay", run the binary with no extra +# args - all configuration comes from the env vars above. +case "${1:-}" in + ""|certrelay) + shift 2>/dev/null || true + exec certrelay "$@" + ;; + -*) + exec certrelay "$@" + ;; + *) + exec "$@" + ;; +esac diff --git a/docker-healthcheck.sh b/docker-healthcheck.sh new file mode 100644 index 0000000..6ab6d9a --- /dev/null +++ b/docker-healthcheck.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# certrelay container HEALTHCHECK. +# +# Two probes: +# +# 1. Liveness - GET /peers must answer 200 within 5s. Verifies the +# HTTP listener is up and the rate-limited request path +# reaches the router. Always runs. +# +# 2. Resolve - If the operator sets CERTRELAY_HEALTHCHECK_HANDLE +# (e.g. "user@rad"), run the bundled `fabric` client +# against the local relay and assert it returns a zone +# whose subject matches the queried handle. Exercises +# the full client->relay->cert-chain->verify pipeline. +# +# Exit codes follow the docker HEALTHCHECK contract: +# 0 = healthy, 1 = unhealthy (any other value is treated as unhealthy). + +set -eu + +BIND="${CERTRELAY_BIND:-127.0.0.1}" +PORT="${CERTRELAY_PORT:-7778}" + +# When the relay binds to a wildcard address (0.0.0.0 / ::), the +# healthcheck still has to connect to a concrete one from inside the +# container's own netns. Loopback is always correct in that case. +case "$BIND" in + 0.0.0.0|::|"") HEALTH_HOST="127.0.0.1" ;; + *) HEALTH_HOST="$BIND" ;; +esac + +URL="http://${HEALTH_HOST}:${PORT}" + +# -------- Probe 1: liveness -------------------------------------------- +if ! wget -q --timeout=5 -O /dev/null "${URL}/peers"; then + echo "healthcheck: ${URL}/peers did not respond within 5s" >&2 + exit 1 +fi + +# -------- Probe 2: subname resolution (opt-in) ------------------------- +HANDLE="${CERTRELAY_HEALTHCHECK_HANDLE:-}" +if [ -n "$HANDLE" ]; then + # The bundled fabric binary writes a single line of zone JSON to + # stdout on success and ": not found" to stderr on a clean + # miss (still exit 0). Network / decode errors exit non-zero. + if ! out=$(fabric --seeds "$URL" --dev-mode "$HANDLE" 2>/dev/null); then + echo "healthcheck: fabric resolve crashed for ${HANDLE} via ${URL}" >&2 + exit 1 + fi + if [ -z "$out" ]; then + echo "healthcheck: ${HANDLE} did not resolve via ${URL}" >&2 + exit 1 + fi + # No jq in the runtime image; the zone JSON always contains both + # the literal "handle" key and the requested handle string. + case "$out" in + *'"handle"'*"${HANDLE}"*) : ;; + *) + echo "healthcheck: unexpected resolve output for ${HANDLE}" >&2 + printf '%s' "$out" | head -c 200 >&2 + echo >&2 + exit 1 + ;; + esac +fi + +exit 0 diff --git a/fabric/Cargo.toml b/fabric/Cargo.toml index 10075af..7733396 100644 --- a/fabric/Cargo.toml +++ b/fabric/Cargo.toml @@ -19,10 +19,17 @@ path = "rust/src/lib.rs" [[bin]] name = "fabric" path = "rust/src/main.rs" -required-features = ["client"] +# The `publish` subcommand needs `signing` (secp256k1 schnorr) in addition +# to the HTTP `client` stack used by `resolve`. +required-features = ["client", "signing"] [features] -default = ["client"] +# `signing` is in the default set so `cargo install --bin fabric` and the +# `cargo build -p fabric-resolver --bin fabric` in the Dockerfile both +# produce a CLI that can publish out of the box. Library consumers that +# don't want secp256k1 link-in (e.g. the `relay` crate) opt out with +# `default-features = false`. +default = ["client", "signing"] client = ["reqwest", "rand", "tokio"] signing = ["secp256k1"] diff --git a/fabric/js/examples/query-local.mjs b/fabric/js/examples/query-local.mjs new file mode 100644 index 0000000..aa6a8a5 --- /dev/null +++ b/fabric/js/examples/query-local.mjs @@ -0,0 +1,17 @@ +import { Fabric } from "@spacesprotocol/fabric-web"; + +const RELAY = "http://127.0.0.1:7778"; +const HANDLE = "user@rad"; + +const fabric = new Fabric({ + seeds: [RELAY], + devMode: true, +}); + +try { + const { zone } = await fabric.resolve(HANDLE); + console.log(JSON.stringify(zone.toJson(), null, 2)); +} catch (e) { + console.error(`Failed to resolve ${HANDLE}:`, e.message ?? e); + process.exit(1); +} diff --git a/fabric/rust/examples/query-local/Cargo.lock b/fabric/rust/examples/query-local/Cargo.lock new file mode 100644 index 0000000..67231f0 --- /dev/null +++ b/fabric/rust/examples/query-local/Cargo.lock @@ -0,0 +1,2827 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-groth16" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" +dependencies = [ + "base58ck", + "base64 0.21.7", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-units" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "borsh_utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4dfa09c15f642d281e02a3837ddd26f6133508d43fe7d80fed8c6a6e0f5caa2" +dependencies = [ + "bitcoin", + "borsh", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fabric-resolver" +version = "0.2.3" +dependencies = [ + "dashmap", + "hex", + "libveritas", + "rand 0.9.4", + "reqwest", + "serde", + "serde_json", + "sha2", + "spaces_nums", + "tokio", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "include_bytes_aligned" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee796ad498c8d9a1d68e477df8f754ed784ef875de1414ebdaf169f70a6a784" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libveritas" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b646e5e793b4a806c5f17646e7b6a1eb9f1ab89b5539f3249e99f02c175e95" +dependencies = [ + "base64 0.22.1", + "borsh", + "hex", + "libveritas_zk", + "risc0-zkvm", + "serde", + "serde_json", + "sip7", + "spacedb", + "spaces_nums", + "spaces_protocol", +] + +[[package]] +name = "libveritas_zk" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c67914770647dd88766cd7da471776bebd28751dd561d7782e978b12d9524cf" +dependencies = [ + "borsh", + "serde", + "spacedb", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "no_std_strings" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.11.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "query-local" +version = "0.1.0" +dependencies = [ + "fabric-resolver", + "serde_json", + "tokio", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "risc0-binfmt" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1883f0c5d19b865f395209a137dcb29e56dc49951424967b8d0114c129f46e77" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "elf", + "lazy_static", + "postcard", + "rand 0.9.4", + "risc0-zkp", + "risc0-zkvm-platform", + "ruint", + "semver", + "serde", + "tracing", +] + +[[package]] +name = "risc0-circuit-keccak" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f543c60287fece797a5da4209384ab1bfebd9644fcfe591e11b1aa85f1a02f8" +dependencies = [ + "anyhow", + "bytemuck", + "paste", + "risc0-binfmt", + "risc0-circuit-recursion", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-recursion" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2347e909c6b2a65584b5898f3802eec5b8c1b4b45329edfdd8587b6a04dd3357" +dependencies = [ + "anyhow", + "bytemuck", + "hex", + "metal", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-rv32im" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61676419814a818fdb5e10066b13c5488b3f54aa9668794bd06c99bc91bff1f2" +dependencies = [ + "anyhow", + "bit-vec", + "bytemuck", + "derive_more", + "paste", + "risc0-binfmt", + "risc0-core", + "risc0-zkp", + "serde", + "tracing", +] + +[[package]] +name = "risc0-core" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b956a976b8ce4713694dcc6c370b522a42ccef4ba45da5b6e57dbf26cdb7b1" +dependencies = [ + "bytemuck", + "rand_core 0.9.5", +] + +[[package]] +name = "risc0-groth16" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc57e76bb87193d154ac5ee6ee352fbd7edabddab36f02a81f40a048e5ca14f9" +dependencies = [ + "anyhow", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-serialize", + "bytemuck", + "hex", + "num-bigint", + "num-traits", + "risc0-binfmt", + "risc0-zkp", + "serde", +] + +[[package]] +name = "risc0-zkos-v1compat" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd70cb45b5d37d025f25663b87c6b9dc9df7f413ee2068531a57f50b0eb95db" +dependencies = [ + "include_bytes_aligned", + "no_std_strings", + "risc0-zkvm-platform", +] + +[[package]] +name = "risc0-zkp" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f40d362a6c146ec6dc69208f539b92fd86e47b0dbc2083801423034a38155a2" +dependencies = [ + "anyhow", + "blake2", + "borsh", + "bytemuck", + "cfg-if", + "digest", + "hex", + "hex-literal", + "metal", + "paste", + "rand_core 0.9.5", + "risc0-core", + "risc0-zkvm-platform", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b7eafb5d85be59cbd9da83f662cf47d834f1b836e14f675d1530b12c666867" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "hex", + "risc0-binfmt", + "risc0-circuit-keccak", + "risc0-circuit-recursion", + "risc0-circuit-rv32im", + "risc0-core", + "risc0-groth16", + "risc0-zkos-v1compat", + "risc0-zkp", + "risc0-zkvm-platform", + "rrs-lib", + "semver", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm-platform" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db893788c416287e2e1a87e6b8f5302511a04a45329e699d6a32a16874fd24f" +dependencies = [ + "bytemuck", + "cfg-if", + "getrandom 0.2.17", + "getrandom 0.3.4", + "libm", + "num_enum", + "paste", + "stability", +] + +[[package]] +name = "rrs-lib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4382d3af3a4ebdae7f64ba6edd9114fff92c89808004c4943b393377a25d001" +dependencies = [ + "downcast-rs", + "paste", +] + +[[package]] +name = "ruint" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" +dependencies = [ + "borsh", + "proptest", + "rand 0.8.6", + "rand 0.9.4", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sip7" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36b86b0da0b64d08a6b0685891b6ba542fe52b01d542a8b6439ff89612518a1" +dependencies = [ + "base64 0.22.1", + "hex", + "serde", + "spaces_protocol", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spacedb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48cda82e951391df9d0a54c96f8b04e117ff39abad5359b181cb71c80798d77" +dependencies = [ + "borsh", + "sha2", +] + +[[package]] +name = "spaces_nums" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7848223efb973297723383234aff01c772fe383f6849bd6d3c0d257c83766c76" +dependencies = [ + "bech32", + "bitcoin", + "borsh", + "borsh_utils", + "hex", + "log", + "serde", + "spaces_protocol", +] + +[[package]] +name = "spaces_protocol" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea63b4a6544d3df35aceb1d1ad4ad2c9f59cc3aa7df5f381241df8dd399ec45" +dependencies = [ + "bitcoin", + "borsh", + "borsh_utils", + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/fabric/rust/examples/query-local/Cargo.toml b/fabric/rust/examples/query-local/Cargo.toml new file mode 100644 index 0000000..2d7d22c --- /dev/null +++ b/fabric/rust/examples/query-local/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "query-local" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "query-local" +path = "src/main.rs" + +# Self-contained: do not pull in the parent workspace. +[workspace] + +[dependencies] +# Path-deps the in-tree `fabric-resolver` crate so this example always +# matches the library code it sits next to. +fabric = { path = "../../..", package = "fabric-resolver", default-features = false, features = ["client"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +serde_json = "1" diff --git a/fabric/rust/examples/query-local/src/main.rs b/fabric/rust/examples/query-local/src/main.rs new file mode 100644 index 0000000..97e4b38 --- /dev/null +++ b/fabric/rust/examples/query-local/src/main.rs @@ -0,0 +1,148 @@ +//! Minimal resolver example: pass a handle on the command line and +//! optionally one or more relay seeds; print the resulting zone as +//! pretty-printed JSON to stdout. +//! +//! Run from this directory: +//! +//! cargo run -- --seeds http://127.0.0.1:7778 --dev-mode user@rad +//! +//! Or against the public network with defaults: +//! +//! cargo run -- user@rad + +use fabric::TrustId; +use fabric::client::Fabric; +use std::str::FromStr; + +#[tokio::main] +async fn main() { + let argv: Vec = std::env::args().skip(1).collect(); + std::process::exit(run(argv).await); +} + +async fn run(args: Vec) -> i32 { + let mut handle: Option = None; + let mut seeds: Vec = Vec::new(); + let mut trust_id: Option = None; + let mut dev_mode = false; + + let mut it = args.into_iter(); + while let Some(arg) = it.next() { + match arg.as_str() { + "--seeds" => { + let Some(val) = it.next() else { + return usage("--seeds requires a value"); + }; + seeds = val + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + if seeds.is_empty() { + return usage("--seeds requires at least one non-empty URL"); + } + } + "--trust-id" => { + let Some(val) = it.next() else { + return usage("--trust-id requires a value"); + }; + match TrustId::from_str(&val) { + Ok(t) => trust_id = Some(t), + Err(e) => { + eprintln!("error: invalid trust id: {e}"); + return 1; + } + } + } + "--dev-mode" => dev_mode = true, + "-h" | "--help" => { + print_usage(); + return 0; + } + other if other.starts_with('-') => { + return usage(&format!("unknown option: {other}")); + } + other => { + if handle.is_some() { + return usage("only one handle is supported"); + } + handle = Some(other.to_string()); + } + } + } + + let Some(handle) = handle else { + return usage("missing handle"); + }; + + let mut fabric = if seeds.is_empty() { + Fabric::new() + } else { + let refs: Vec<&str> = seeds.iter().map(|s| s.as_str()).collect(); + Fabric::with_seeds(&refs) + }; + if dev_mode { + fabric = fabric.with_dev_mode(); + } + if let Some(id) = trust_id + && let Err(e) = fabric.trust(id).await + { + eprintln!("error: failed to pin trust id: {e}"); + return 1; + } + + match fabric.resolve(&handle).await { + Ok(Some(zone)) => match serde_json::to_string_pretty(&zone) { + Ok(s) => { + println!("{s}"); + 0 + } + Err(e) => { + eprintln!("error: serializing zone to JSON: {e}"); + 1 + } + }, + Ok(None) => { + eprintln!("{handle}: not found"); + 1 + } + Err(e) => { + eprintln!("error: {e}"); + 1 + } + } +} + +fn usage(msg: &str) -> i32 { + eprintln!("error: {msg}"); + print_usage(); + 2 +} + +fn print_usage() { + eprintln!( + "Usage: query-local [OPTIONS] \n\ + \n\ + Resolve a Spaces handle (e.g. user@rad) via the certrelay network\n\ + and print the resulting zone as pretty-printed JSON on stdout.\n\ + \n\ + Options:\n\ + \x20 --seeds Comma-separated relay URLs\n\ + \x20 (default: built-in public seeds)\n\ + \x20 --trust-id Pin a trust ID for verified-badge state\n\ + \x20 --dev-mode Skip finality checks (use against\n\ + \x20 freshly-synced local relays)\n\ + \x20 -h, --help Show this help\n\ + \n\ + Examples:\n\ + \x20 # Resolve via your local relay\n\ + \x20 query-local --seeds http://127.0.0.1:7778 --dev-mode user@rad\n\ + \n\ + \x20 # Resolve against multiple seeds; fabric picks the freshest\n\ + \x20 query-local --seeds http://127.0.0.1:7778,https://relay-cosmos.spacesprotocol.org \\\n\ + \x20 user@rad\n\ + \n\ + \x20 # Resolve via the built-in public seeds\n\ + \x20 query-local user@rad" + ); +} diff --git a/fabric/rust/src/main.rs b/fabric/rust/src/main.rs index 8f76526..def38f6 100644 --- a/fabric/rust/src/main.rs +++ b/fabric/rust/src/main.rs @@ -1,55 +1,347 @@ use fabric::TrustId; use fabric::client::Fabric; +use fabric::libveritas::sip7::{Record, RecordSet}; use std::str::FromStr; #[tokio::main] async fn main() { - let args: Vec = std::env::args().skip(1).collect(); - let mut handles = Vec::new(); - let mut seeds = Vec::new(); - let mut trust_id = None; + let argv: Vec = std::env::args().skip(1).collect(); + std::process::exit(dispatch(argv).await); +} + +/// Subcommand router. +/// +/// Backwards-compatible: if the first positional token is `resolve` or +/// `publish`, that's the subcommand. Otherwise (e.g. `fabric --seeds X +/// alice@bitcoin`) the args are forwarded to `resolve`, preserving the +/// previous CLI shape. +async fn dispatch(argv: Vec) -> i32 { + let mut iter = argv.into_iter(); + match iter.next().as_deref() { + None => { + print_top_usage(); + 1 + } + Some("-h") | Some("--help") => { + print_top_usage(); + 0 + } + Some("resolve") => run_resolve(iter.collect()).await, + Some("publish") => run_publish(iter.collect()).await, + Some(first) => { + let mut args = Vec::with_capacity(1); + args.push(first.to_string()); + args.extend(iter); + run_resolve(args).await + } + } +} + +// --------------------------------------------------------------------------- +// Resolve subcommand (unchanged behaviour from the pre-publish CLI) +// --------------------------------------------------------------------------- + +async fn run_resolve(args: Vec) -> i32 { + let mut handles: Vec = Vec::new(); + let mut seeds: Vec = Vec::new(); + let mut trust_id: Option = None; + let mut dev_mode = false; + + let mut it = args.into_iter(); + while let Some(arg) = it.next() { + match arg.as_str() { + "--seeds" => { + let Some(val) = it.next() else { + return usage_err("--seeds requires a value", Cmd::Resolve); + }; + seeds = val.split(',').map(|s| s.to_string()).collect(); + } + "--trust-id" => { + let Some(val) = it.next() else { + return usage_err("--trust-id requires a value", Cmd::Resolve); + }; + match TrustId::from_str(&val) { + Ok(t) => trust_id = Some(t), + Err(e) => { + eprintln!("error: invalid trust id: {e}"); + return 1; + } + } + } + "--dev-mode" => dev_mode = true, + "-h" | "--help" => { + print_resolve_usage(); + return 0; + } + other if other.starts_with('-') => { + return usage_err(&format!("unknown option: {other}"), Cmd::Resolve); + } + other => handles.push(other.to_string()), + } + } + + if handles.is_empty() { + return usage_err("no handles specified", Cmd::Resolve); + } + + let fabric = match build_fabric(&seeds, dev_mode, trust_id).await { + Ok(f) => f, + Err(code) => return code, + }; + + let handle_refs: Vec<&str> = handles.iter().map(|s| s.as_ref()).collect(); + match fabric.resolve_all(&handle_refs).await { + Ok(zones) => { + for handle in &handles { + match zones.iter().find(|z| z.handle.to_string() == *handle) { + Some(zone) => println!("{}", serde_json::to_string(zone).unwrap()), + None => eprintln!("{handle}: not found"), + } + } + 0 + } + Err(e) => { + eprintln!("error: {e}"); + 1 + } + } +} + +// --------------------------------------------------------------------------- +// Publish subcommand (new) +// --------------------------------------------------------------------------- + +async fn run_publish(args: Vec) -> i32 { + let mut handle: Option = None; + let mut seeds: Vec = Vec::new(); + let mut trust_id: Option = None; let mut dev_mode = false; - let mut it = args.iter(); + let mut secret_inline: Option = None; + let mut secret_env: Option = None; + let mut secret_file: Option = None; + + let mut explicit_seq: Option = None; + let mut primary = true; + let mut dry_run = false; + + let mut txts: Vec<(String, Vec)> = Vec::new(); + let mut addrs: Vec<(String, Vec)> = Vec::new(); + + let mut it = args.into_iter(); while let Some(arg) = it.next() { match arg.as_str() { "--seeds" => { - let val = it - .next() - .unwrap_or_else(|| exit_usage("--seeds requires a value")); + let Some(val) = it.next() else { + return usage_err("--seeds requires a value", Cmd::Publish); + }; seeds = val.split(',').map(|s| s.to_string()).collect(); } "--trust-id" => { - let val = it - .next() - .unwrap_or_else(|| exit_usage("--trust-id requires a value")); - trust_id = Some(TrustId::from_str(val).unwrap_or_else(|e| { - eprintln!("error: invalid trust id: {e}"); - std::process::exit(1); - })); + let Some(val) = it.next() else { + return usage_err("--trust-id requires a value", Cmd::Publish); + }; + match TrustId::from_str(&val) { + Ok(t) => trust_id = Some(t), + Err(e) => { + eprintln!("error: invalid trust id: {e}"); + return 1; + } + } } "--dev-mode" => dev_mode = true, - "--help" | "-h" => { - print_usage(); - std::process::exit(0); + "--secret-key" => { + let Some(val) = it.next() else { + return usage_err("--secret-key requires a value", Cmd::Publish); + }; + secret_inline = Some(val); + } + "--secret-key-env" => { + let Some(val) = it.next() else { + return usage_err("--secret-key-env requires a value", Cmd::Publish); + }; + secret_env = Some(val); + } + "--secret-key-file" => { + let Some(val) = it.next() else { + return usage_err("--secret-key-file requires a value", Cmd::Publish); + }; + secret_file = Some(val); + } + "--seq" => { + let Some(val) = it.next() else { + return usage_err("--seq requires a value", Cmd::Publish); + }; + match val.parse::() { + Ok(n) => explicit_seq = Some(n), + Err(_) => { + return usage_err( + "--seq must be a non-negative integer", + Cmd::Publish, + ); + } + } + } + "--txt" => { + let Some(val) = it.next() else { + return usage_err("--txt requires key=value[,value2,...]", Cmd::Publish); + }; + match parse_record_kv(&val) { + Some(kv) => txts.push(kv), + None => { + return usage_err( + "--txt requires key=value[,value2,...]", + Cmd::Publish, + ); + } + } + } + "--addr" => { + let Some(val) = it.next() else { + return usage_err("--addr requires key=value[,value2,...]", Cmd::Publish); + }; + match parse_record_kv(&val) { + Some(kv) => addrs.push(kv), + None => { + return usage_err( + "--addr requires key=value[,value2,...]", + Cmd::Publish, + ); + } + } + } + "--no-primary" => primary = false, + "--dry-run" => dry_run = true, + "-h" | "--help" => { + print_publish_usage(); + return 0; + } + other if other.starts_with('-') => { + return usage_err(&format!("unknown option: {other}"), Cmd::Publish); } other => { - if other.starts_with('-') { - exit_usage(&format!("unknown option: {other}")); + if handle.is_some() { + return usage_err( + "publish takes exactly one handle", + Cmd::Publish, + ); } - handles.push(other); + handle = Some(other.to_string()); } } } - if handles.is_empty() { - exit_usage("no handles specified"); + let Some(handle) = handle else { + return usage_err("publish requires a handle", Cmd::Publish); + }; + + if txts.is_empty() && addrs.is_empty() { + return usage_err("at least one --txt or --addr is required", Cmd::Publish); } + let secret_bytes = match resolve_secret_key(secret_inline, secret_env, secret_file) { + Ok(b) => b, + Err(code) => return code, + }; + + let fabric = match build_fabric(&seeds, dev_mode, trust_id).await { + Ok(f) => f, + Err(code) => return code, + }; + + // Pick the next seq. Explicit --seq always wins; otherwise resolve the + // handle once to read the currently-stored seq and bump it. + let seq = match explicit_seq { + Some(n) => n, + None => match fabric.resolve(&handle).await { + Ok(Some(zone)) => zone.records.seq().unwrap_or(0).saturating_add(1), + Ok(None) => 1, + Err(e) => { + eprintln!("error: looking up current seq for {handle}: {e}"); + eprintln!("(pass --seq to skip the pre-publish resolve)"); + return 1; + } + }, + }; + + let mut record_list: Vec = Vec::with_capacity(1 + txts.len() + addrs.len()); + record_list.push(Record::seq(seq)); + for (k, vals) in &txts { + let refs: Vec<&str> = vals.iter().map(|s| s.as_str()).collect(); + record_list.push(Record::txt(k, &refs)); + } + for (k, vals) in &addrs { + let refs: Vec<&str> = vals.iter().map(|s| s.as_str()).collect(); + record_list.push(Record::addr(k, &refs)); + } + let records = match RecordSet::pack(record_list) { + Ok(r) => r, + Err(e) => { + eprintln!("error: packing records: {e}"); + return 1; + } + }; + + if dry_run { + let summary = serde_json::json!({ + "handle": handle, + "seq": seq, + "primary": primary, + "txts": txts.iter().map(|(k, v)| serde_json::json!({ "key": k, "values": v })).collect::>(), + "addrs": addrs.iter().map(|(k, v)| serde_json::json!({ "key": k, "values": v })).collect::>(), + "dry_run": true, + }); + println!("{}", summary); + return 0; + } + + // Pull the existing cert chain so the relay's `prove` call has the + // right anchor context. Requires that the handle has been minted and + // is already known to at least one reachable relay. + let cert = match fabric.export(&handle).await { + Ok(c) => c, + Err(e) => { + eprintln!("error: exporting cert chain for {handle}: {e}"); + eprintln!("(the handle must already exist on a reachable relay before you can publish records under it)"); + return 1; + } + }; + + if let Err(e) = fabric.publish(&cert, records, &secret_bytes, primary).await { + eprintln!("error: publish failed: {e}"); + return 1; + } + + let summary = serde_json::json!({ + "handle": handle, + "seq": seq, + "primary": primary, + "txts": txts.len(), + "addrs": addrs.len(), + }); + println!("{}", summary); + 0 +} + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +#[derive(Copy, Clone)] +enum Cmd { + Resolve, + Publish, +} + +async fn build_fabric( + seeds: &[String], + dev_mode: bool, + trust_id: Option, +) -> Result { let mut fabric = if seeds.is_empty() { Fabric::new() } else { - let refs: Vec<&str> = seeds.iter().map(|s| s.as_ref()).collect(); + let refs: Vec<&str> = seeds.iter().map(|s| s.as_str()).collect(); Fabric::with_seeds(&refs) }; if dev_mode { @@ -58,43 +350,163 @@ async fn main() { if let Some(id) = trust_id { if let Err(e) = fabric.trust(id).await { eprintln!("error: failed to pin trust id: {e}"); - std::process::exit(1); + return Err(1); } } + Ok(fabric) +} - let handle_refs: Vec<&str> = handles.iter().map(|s| s.as_ref()).collect(); - match fabric.resolve_all(&handle_refs).await { - Ok(zones) => { - for handle in &handles { - match zones.iter().find(|z| z.handle.to_string() == *handle) { - Some(zone) => println!("{}", serde_json::to_string(zone).unwrap()), - None => eprintln!("{handle}: not found"), - } +fn resolve_secret_key( + inline: Option, + env: Option, + file: Option, +) -> Result<[u8; 32], i32> { + let provided = [inline.is_some(), env.is_some(), file.is_some()] + .iter() + .filter(|b| **b) + .count(); + if provided == 0 { + eprintln!( + "error: supply exactly one of --secret-key / --secret-key-env / --secret-key-file" + ); + return Err(2); + } + if provided > 1 { + eprintln!( + "error: --secret-key, --secret-key-env and --secret-key-file are mutually exclusive" + ); + return Err(2); + } + + let raw = if let Some(s) = inline { + s + } else if let Some(var) = env { + match std::env::var(&var) { + Ok(v) => v, + Err(_) => { + eprintln!("error: env var {var} is not set"); + return Err(1); } } + } else if let Some(path) = file { + match std::fs::read_to_string(&path) { + Ok(v) => v, + Err(e) => { + eprintln!("error: reading {path}: {e}"); + return Err(1); + } + } + } else { + unreachable!("provided count was validated above") + }; + + let trimmed = raw.trim(); + let bytes = match hex::decode(trimmed) { + Ok(b) => b, Err(e) => { - eprintln!("error: {e}"); - std::process::exit(1); + eprintln!("error: secret key is not valid hex: {e}"); + return Err(1); } + }; + if bytes.len() != 32 { + eprintln!( + "error: secret key must decode to exactly 32 bytes (got {})", + bytes.len() + ); + return Err(1); } + let mut out = [0u8; 32]; + out.copy_from_slice(&bytes); + Ok(out) } -fn print_usage() { +fn parse_record_kv(s: &str) -> Option<(String, Vec)> { + let (k, v) = s.split_once('=')?; + let key = k.trim(); + if key.is_empty() { + return None; + } + let values: Vec = v.split(',').map(|s| s.to_string()).collect(); + if values.iter().all(|s| s.is_empty()) { + return None; + } + Some((key.to_string(), values)) +} + +fn usage_err(msg: &str, cmd: Cmd) -> i32 { + eprintln!("error: {msg}"); + match cmd { + Cmd::Resolve => print_resolve_usage(), + Cmd::Publish => print_publish_usage(), + } + 2 +} + +fn print_top_usage() { println!( - "Usage: fabric [options] [ ...]\n\ + "Usage: fabric [OPTIONS] [ARGS]\n\ + \n\ + Commands:\n\ + \x20 resolve [ ...] Resolve handles via the relay network\n\ + \x20 publish Sign + broadcast records under a handle\n\ + \n\ + Backwards compatibility: `fabric [options] ...` (no command)\n\ + is equivalent to `fabric resolve [options] ...`.\n\ + \n\ + Run `fabric --help` for command-specific options." + ); +} + +fn print_resolve_usage() { + println!( + "Usage: fabric resolve [OPTIONS] [ ...]\n\ \n\ Resolve handles via the certrelay network.\n\ \n\ Options:\n\ \x20 --seeds Seed relay URLs (comma-separated)\n\ - \x20 --trust-id Trust ID for verification\n\ + \x20 --trust-id Trust ID for verification\n\ \x20 --dev-mode Enable dev mode (skip finality checks)\n\ \x20 -h, --help Show this help" ); } -fn exit_usage(msg: &str) -> ! { - eprintln!("error: {msg}"); - print_usage(); - std::process::exit(1); +fn print_publish_usage() { + println!( + "Usage: fabric publish [OPTIONS] \n\ + \n\ + Sign and broadcast records under to the relay network.\n\ + The handle must already have a certificate chain available on the\n\ + relay (i.e. it was previously minted by the parent space owner).\n\ + \n\ + Connection options:\n\ + \x20 --seeds Seed relay URLs (comma-separated)\n\ + \x20 --trust-id Trust ID for verification\n\ + \x20 --dev-mode Enable dev mode (skip finality checks)\n\ + \n\ + Signing material (exactly one required):\n\ + \x20 --secret-key 32-byte BIP-340 secret as 64-char hex\n\ + \x20 (UNSAFE: visible in `ps`/shell history)\n\ + \x20 --secret-key-env Read secret hex from environment variable VAR\n\ + \x20 --secret-key-file Read secret hex from file (whitespace-trimmed)\n\ + \n\ + Records (at least one --txt or --addr required, all repeatable):\n\ + \x20 --txt =[,,...] Add a TXT record\n\ + \x20 --addr =[,,...] Add an addr record\n\ + \n\ + Other:\n\ + \x20 --seq Explicit seq number (default: +1)\n\ + \x20 --no-primary Skip SIG_PRIMARY_ZONE flag\n\ + \x20 (the relay won't write num_id -> handle reverse map)\n\ + \x20 --dry-run Print the would-be payload as JSON, don't broadcast\n\ + \x20 -h, --help Show this help\n\ + \n\ + Example:\n\ + \x20 SECRET=$(cat ./user-rad-secret.hex) \\\n\ + \x20 fabric publish --seeds http://127.0.0.1:7778 --dev-mode \\\n\ + \x20 --secret-key-env SECRET \\\n\ + \x20 --txt website=https://example.com \\\n\ + \x20 --addr btc=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 \\\n\ + \x20 user@rad" + ); } diff --git a/relay/Cargo.toml b/relay/Cargo.toml index eff19dd..5c3e1e8 100644 --- a/relay/Cargo.toml +++ b/relay/Cargo.toml @@ -8,6 +8,10 @@ publish = false name = "certrelay" path = "src/main.rs" +[[bin]] +name = "monitor" +path = "src/bin/monitor.rs" + [dependencies] clap = { version = "4", features = ["derive", "env"] } libveritas = { workspace = true } @@ -30,6 +34,7 @@ sha2 = "0.10" tracing = "0.1" yuki = { version = "0.0.1", package = "bitcoin_yuki" } env_logger = "0.11.9" +log = "0.4" hex = "0.4.3" tower-http = { version = "0.6", features = ["cors"] } base64 = "0.22" diff --git a/relay/src/app.rs b/relay/src/app.rs index 4e2708c..e7848ba 100644 --- a/relay/src/app.rs +++ b/relay/src/app.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::anchor::AnchorSets; use crate::{ @@ -67,15 +67,80 @@ fn default_data_dir() -> PathBuf { .unwrap_or_else(|_| PathBuf::from(".certrelay")) } +/// Mask `user:pass` in URLs of the form `scheme://user:pass@host[...]` so +/// secrets in env-var-sourced configuration are never echoed to the console. +fn redact_url(url: &str) -> String { + if let Some(scheme_end) = url.find("://") { + let after = &url[scheme_end + 3..]; + if let Some(at_rel) = after.find('@') { + let userinfo = &after[..at_rel]; + if userinfo.contains(':') { + return format!("{}://***:***{}", &url[..scheme_end], &after[at_rel..]); + } + } + } + url.to_string() +} + +/// Print every `CERTRELAY_*`-backed setting that this process will use, +/// alongside the effective values after defaults have been applied. Written +/// to stderr with `eprintln!` so it's visible regardless of `RUST_LOG` / +/// tracing-subscriber configuration. +fn log_startup_env(args: &Args, data_dir: &Path) { + let effective_port = args.port.unwrap_or(match args.chain { + ExtendedNetwork::Mainnet => 7778, + _ => 7779, + }); + let spaced_url_display = match args.spaced_rpc_url.as_deref() { + Some(u) => redact_url(u), + None => "".to_string(), + }; + let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "".to_string()); + + eprintln!("certrelay: effective configuration"); + eprintln!(" CERTRELAY_CHAIN = {}", args.chain); + eprintln!(" CERTRELAY_DATA_DIR = {}", data_dir.display()); + eprintln!(" CERTRELAY_BIND = {}", args.bind); + eprintln!(" CERTRELAY_PORT = {}", effective_port); + eprintln!( + " CERTRELAY_SELF_URL = {}", + args.self_url.as_deref().unwrap_or("") + ); + eprintln!(" CERTRELAY_SPACED_RPC_URL = {}", spaced_url_display); + eprintln!( + " CERTRELAY_REMOTE_IP_HEADER = {}", + args.remote_ip_header.as_deref().unwrap_or("") + ); + eprintln!(" CERTRELAY_BOOTSTRAP = {}", args.is_bootstrap); + eprintln!( + " CERTRELAY_ANCHOR_REFRESH = {}s", + args.anchor_refresh + ); + eprintln!(" --skip-checkpoint-sync = {}", args.skip_checkpoint_sync); + eprintln!(" RUST_LOG = {}", rust_log); +} + pub async fn run( args: Vec, shutdown: tokio::sync::broadcast::Sender<()>, ) -> anyhow::Result<()> { let args = Args::try_parse_from(args)?; - let data_dir = args.data_dir.unwrap_or_else(default_data_dir); + // Cloning here (rather than `unwrap_or_else` which would move out of + // `args`) lets log_startup_env borrow the full Args struct below. + let data_dir = args + .data_dir + .as_ref() + .cloned() + .unwrap_or_else(default_data_dir); std::fs::create_dir_all(&data_dir)?; + log_startup_env(&args, &data_dir); + + // Captured before fields of `args` get consumed below, so the final + // "ready" banner can still surface the public URL to operators. + let self_url_for_banner: Option = args.self_url.clone(); + // Start embedded yuki + spaced if no external spaced URL was provided let mut spaced_url = args.spaced_rpc_url; if spaced_url.is_none() { @@ -273,7 +338,23 @@ pub async fn run( }); let bind_addr = format!("{}:{}", args.bind, port); let listener = tokio::net::TcpListener::bind(&bind_addr).await?; - tracing::info!("relay listening on {}", listener.local_addr()?); + let local = listener.local_addr()?; + tracing::info!("relay listening on {}", local); + + // Guaranteed-visible "ready" banner so operators always see *where* + // the relay is serving, even if no tracing-subscriber is configured. + eprintln!(); + eprintln!("certrelay: ready"); + eprintln!(" listening on: http://{}", local); + if local.ip().is_unspecified() { + eprintln!( + " (also reachable as http://127.0.0.1:{} on this host)", + local.port() + ); + } + if let Some(self_url) = self_url_for_banner.as_deref() { + eprintln!(" public URL: {}", self_url.trim_end_matches('/')); + } let mut shutdown_rx = shutdown.subscribe(); tokio::select! { diff --git a/relay/src/bin/monitor.rs b/relay/src/bin/monitor.rs new file mode 100644 index 0000000..c80fae7 --- /dev/null +++ b/relay/src/bin/monitor.rs @@ -0,0 +1,291 @@ +//! Bootstrap relay health monitor. +//! +//! Polls each URL in [`BOOTSTRAP_RELAYS`] with `GET /peers` on a fixed interval. +//! Logs error/recovery transitions and peer counts for healthy relays. + +use clap::Parser; +use relay::BOOTSTRAP_RELAYS; +use relay::PeerInfo; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; + +const DEFAULT_CHECK_INTERVAL_MINS: u64 = 30; +const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 15; + +#[derive(Parser)] +#[command( + name = "monitor", + about = "Poll bootstrap certrelay seeds and track /peers health", + long_about = "Polls each URL listed in BOOTSTRAP_RELAYS with GET /peers on a \ + fixed interval. Logs when a seed enters or leaves an error state \ + (unreachable, timeout, empty peer list). Logs peer counts for \ + seeds that return a non-empty list. With --watch-all, also probes \ + every peer URL reported by the bootstrap relays (deduplicated)." +)] +struct Args { + /// Minutes to wait between poll loops. + #[arg( + long, + env = "CHECK_INTERVAL", + default_value_t = DEFAULT_CHECK_INTERVAL_MINS + )] + check_interval: u64, + + /// Per-request HTTP timeout in seconds. + #[arg( + long, + env = "REQUEST_TIMEOUT", + default_value_t = DEFAULT_REQUEST_TIMEOUT_SECS + )] + timeout: u64, + + /// Log filter passed to env_logger (e.g. info, debug, monitor=debug). + #[arg(long, env = "RUST_LOG", default_value = "info")] + rust_log: String, + + /// Also probe every peer URL returned by bootstrap relays (deduplicated). + #[arg(long, env = "WATCH_ALL", default_value_t = false)] + watch_all: bool, +} + +struct RelayTracker { + in_error: bool, + error_since: Option, +} + +enum ProbeOutcome { + Ok { count: usize, peers: Vec }, + Err(String), +} + +impl ProbeOutcome { + fn is_ok(&self) -> bool { + matches!(self, Self::Ok { .. }) + } +} + +#[derive(Clone, Copy)] +enum NodeKind { + Bootstrap, + Discovered, +} + +impl NodeKind { + fn as_str(self) -> &'static str { + match self { + Self::Bootstrap => "bootstrap", + Self::Discovered => "discovered", + } + } +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + env_logger::Builder::new() + .parse_filters(&args.rust_log) + .init(); + + let check_interval = Duration::from_secs(args.check_interval * 60); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(args.timeout)) + .build() + .expect("failed to build HTTP client"); + + let mut trackers: HashMap = HashMap::new(); + + log::info!( + "monitor: watching {} bootstrap relay(s), interval={}min, timeout={}s, watch_all={}", + BOOTSTRAP_RELAYS.len(), + args.check_interval, + args.timeout, + args.watch_all + ); + + let mut cycle_summary: Option = None; + + loop { + if let Some(ref summary) = cycle_summary { + log::info!("{summary}"); + } + + let mut discovered_peers: HashSet = HashSet::new(); + let mut bootstrap_active = 0; + let bootstrap_total = BOOTSTRAP_RELAYS.len(); + + for &base in BOOTSTRAP_RELAYS { + let url = peers_url(base); + let outcome = probe(&client, &url, args.timeout).await; + if outcome.is_ok() { + bootstrap_active += 1; + } + if args.watch_all { + collect_discovered_peers(&outcome, &mut discovered_peers); + } + record_outcome(&mut trackers, NodeKind::Bootstrap, base, outcome); + } + + let mut discovered_active = 0; + let discovered_total = if args.watch_all { + discovered_peers.len() + } else { + 0 + }; + + if args.watch_all { + let mut peer_urls: Vec = discovered_peers.into_iter().collect(); + peer_urls.sort(); + log::debug!("watch-all: probing {} discovered peer(s)", peer_urls.len()); + for base in peer_urls { + let url = peers_url(&base); + let outcome = probe(&client, &url, args.timeout).await; + if outcome.is_ok() { + discovered_active += 1; + } + record_outcome(&mut trackers, NodeKind::Discovered, &base, outcome); + } + } + + cycle_summary = Some(format_loop_summary( + bootstrap_active, + bootstrap_total, + discovered_active, + discovered_total, + args.watch_all, + )); + + tokio::time::sleep(check_interval).await; + } +} + +fn pct(active: usize, total: usize) -> f64 { + if total == 0 { + 0.0 + } else { + (active as f64 / total as f64) * 100.0 + } +} + +fn format_loop_summary( + bootstrap_active: usize, + bootstrap_total: usize, + discovered_active: usize, + discovered_total: usize, + watch_all: bool, +) -> String { + let bootstrap_pct = pct(bootstrap_active, bootstrap_total); + let mut line = format!( + "====== {bootstrap_active} of {bootstrap_total} bootstrap nodes active {bootstrap_pct:.1}% ======" + ); + if watch_all { + let discovered_pct = pct(discovered_active, discovered_total); + line.push_str(&format!( + " {discovered_active} of {discovered_total} discovered nodes active {discovered_pct:.1}% ======" + )); + } + line +} + +fn normalize_url(url: &str) -> String { + url.trim_end_matches('/').to_string() +} + +fn is_bootstrap(url: &str) -> bool { + let normalized = normalize_url(url); + BOOTSTRAP_RELAYS + .iter() + .any(|seed| normalize_url(seed) == normalized) +} + +fn collect_discovered_peers(outcome: &ProbeOutcome, discovered: &mut HashSet) { + let ProbeOutcome::Ok { peers, .. } = outcome else { + return; + }; + for peer in peers { + if !is_bootstrap(&peer.url) { + discovered.insert(normalize_url(&peer.url)); + } + } +} + +fn record_outcome( + trackers: &mut HashMap, + kind: NodeKind, + base: &str, + outcome: ProbeOutcome, +) { + let label = kind.as_str(); + let tracker = trackers.entry(base.to_string()).or_insert(RelayTracker { + in_error: false, + error_since: None, + }); + + match outcome { + ProbeOutcome::Ok { count, .. } => { + if tracker.in_error { + let down_for = tracker + .error_since + .map(|t| t.elapsed()) + .unwrap_or_default(); + log::info!( + "[{label}] {base}: resumed from error state (was down for {:.1}s)", + down_for.as_secs_f64() + ); + tracker.in_error = false; + tracker.error_since = None; + } + if count > 0 { + log::info!("[{label}] {base}/peers : {count} peer(s)"); + } + } + ProbeOutcome::Err(reason) => { + if !tracker.in_error { + log::error!("[{label}] {base}: entered error state — {reason}"); + tracker.in_error = true; + tracker.error_since = Some(Instant::now()); + } else { + log::warn!("[{label}] {base}: still in error state — {reason}"); + } + } + } +} + +fn peers_url(base: &str) -> String { + format!("{}/peers", base.trim_end_matches('/')) +} + +async fn probe(client: &reqwest::Client, url: &str, timeout_secs: u64) -> ProbeOutcome { + let resp = match client.get(url).send().await { + Ok(r) => r, + Err(e) => { + let reason = if e.is_timeout() { + format!("connection timed out after {timeout_secs}s") + } else if e.is_connect() { + format!("unreachable: {e}") + } else { + format!("request failed: {e}") + }; + return ProbeOutcome::Err(reason); + } + }; + + if !resp.status().is_success() { + return ProbeOutcome::Err(format!("HTTP {}", resp.status())); + } + + let peers: Vec = match resp.json().await { + Ok(p) => p, + Err(e) => return ProbeOutcome::Err(format!("invalid JSON: {e}")), + }; + + if peers.is_empty() { + return ProbeOutcome::Err("empty peer list []".into()); + } + + ProbeOutcome::Ok { + count: peers.len(), + peers, + } +} diff --git a/relay/src/http.rs b/relay/src/http.rs index 15ab24c..098c524 100644 --- a/relay/src/http.rs +++ b/relay/src/http.rs @@ -80,6 +80,7 @@ pub const DEFAULT_MAX_MESSAGE_SIZE: usize = 512 * 1024; pub const BOOTSTRAP_RELAYS: &[&str] = &[ "https://relay-cosmos.spacesprotocol.org", "https://relay-atlas.spacesprotocol.org", + "http://70.251.209.207:47778", ]; /// Shared application state.