Plutia
Plutia is a deterministic, verifiable PLC mirror for plc.directory with signed checkpoints and a proof-serving API.
Key Capabilities
- Mirror and resolver modes.
- Pebble-backed state/index storage.
- Compressed append-only operation blocks (
zstd) in mirror mode. - Deterministic canonical operation handling and signature-chain verification.
- Signed Merkle checkpoints and DID inclusion proof API.
- Corruption detection and restart-safe ingestion.
- Measured storage in benchmarked runs is about 1.2 KB per operation.
- Benchmarked replay throughput is about 45x higher than naive replay in the same test setup.
Trust Model
- Plutia mirrors data from
https://plc.directory. - Plutia validates operation signature chains and prev-link continuity according to configured verification policy.
- Plutia does not alter PLC authority or introduce consensus.
- Checkpoints are mirror commitments about what this mirror observed and verified, not global consensus.
Modes
mirror: stores full verifiable operation history (data/ops/*.zst) + state + proofs/checkpoints.resolver: stores resolved DID state/index only (no op block archive).thin: on-demand, verifiable DID resolver with persistent TTL/LRU cache; no full replay, no blocklog, no full history archive.
Quick Start
task build
task verify:small
task bench
Dev / Smoke Test (Docker Compose)
VERSION="$(cat VERSION)" \
COMMIT="$(git rev-parse --short HEAD)" \
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
docker compose -f docker-compose.yaml up --build
Compose build args are read from environment variables (VERSION, COMMIT, BUILD_DATE).
Equivalent direct build:
docker build \
--build-arg VERSION="$(cat VERSION)" \
--build-arg COMMIT="$(git rev-parse --short HEAD)" \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t plutia:local .
In another terminal, generate the mirror signing key inside the running container:
docker compose -f docker-compose.yaml exec plutia /app/plutia keygen --out=/var/lib/plutia/mirror.key
Verify checkpoint and proof endpoints:
curl -sS http://127.0.0.1:8080/checkpoints/latest | jq .
DID="$(curl -sS 'http://127.0.0.1:8080/export?count=1' | head -n 1 | jq -r '.did')"
curl -sS "http://127.0.0.1:8080/did/${DID}/proof" | jq .
CLI Commands
plutia serve --config=config.default.yaml [--max-ops=0]
plutia replay --config=config.default.yaml [--max-ops=0]
plutia verify --config=config.default.yaml --did=did:plc:example
plutia snapshot --config=config.default.yaml
plutia bench --config=config.default.yaml --max-ops=200000
plutia compare --config=config.default.yaml --remote=https://mirror.example.com
plutia keygen --out=./mirror.key [--force]
plutia version
Versioning and Reproducible Builds
Plutia follows semantic versioning, starting at v0.1.0.
plutia version prints:
Version(defaults todevif not injected)CommitBuildDate(UTC RFC3339)GoVersion
Build metadata is injected through ldflags:
go build -trimpath \
-ldflags "-X main.version=v0.1.0 -X main.commit=$(git rev-parse --short HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o ./bin/plutia ./cmd/plutia
Task runner equivalent:
task build
HTTP API
GET /healthGET /metrics(Prometheus)GET /status(includes build/version metadata)GET /did/{did}GET /did/{did}/proofGET /checkpoints/latestGET /checkpoints/{sequence}
PLC API Compatibility
Plutia includes read-only compatibility endpoints for plc.directory API consumers:
GET /{did}(returnsapplication/did+ld+json)GET /{did}/logGET /{did}/log/lastGET /{did}/log/auditGET /{did}/dataGET /export(NDJSON,application/jsonlines, supportscountup to1000, andafterRFC3339 filtering based on ingested operation timestamps)
For audit/export compatibility fields, createdAt is sourced from the mirror's recorded ingest timestamp for each operation reference.
Write behavior is intentionally unsupported:
POST /{did}returns405 Method Not AllowedwithAllow: GET
Verification features are additive extensions and remain available under:
GET /did/{did}GET /did/{did}/proofGET /checkpoints/latestGET /checkpoints/{sequence}
Metrics and Observability
Prometheus series exposed at /metrics include:
ingest_ops_totalingest_ops_per_secondingest_lag_opsverify_failures_totalcheckpoint_duration_secondscheckpoint_sequencedisk_bytes_totaldid_countthin_cache_hits_totalthin_cache_misses_totalthin_cache_entriesthin_cache_evictions_total
Operational hardening includes:
- Per-IP token-bucket rate limits (stricter on proof endpoints).
- Per-request timeout (default
10s) with cancellation propagation. - Upstream ingestion retries with exponential backoff and
429handling. - Graceful SIGINT/SIGTERM shutdown with flush-before-exit behavior.
Running Your Own Mirror
System Requirements
- Go 1.25+
- SSD-backed storage recommended
- RAM: 4GB minimum, 8GB+ recommended for larger throughput
- CPU: multi-core recommended for parallel verification workers
Disk Projections
Using benchmarked density (~1.2KB/op total):
- 5,000,000 ops: ~6GB
- 10,000,000 ops: ~12GB
Always keep extra headroom for compaction, checkpoints, and operational buffers.
Example config.default.yaml
See config.default.yaml. All supported config keys:
modedata_dirplc_sourceverifyzstd_levelblock_size_mbcheckpoint_intervalcommit_batch_sizeverify_workersexport_page_sizereplay_tracethin_cache_ttlthin_cache_max_entrieslisten_addrmirror_private_key_pathpoll_intervalrequest_timeouthttp_retry_max_attemptshttp_retry_base_delayhttp_retry_max_delayrate_limit.resolve_rpsrate_limit.resolve_burstrate_limit.proof_rpsrate_limit.proof_burst
Config files are optional. If --config points to a missing file, Plutia falls back to internal defaults and then applies any PLUTIA_* environment overrides.
Example docker-compose.yaml
services:
plutia:
image: ghcr.io/fuwn/plutia:0.1.0
command: ["plutia", "serve", "--config=/etc/plutia/config.yaml"]
ports:
- "8080:8080"
volumes:
- ./config.default.yaml:/etc/plutia/config.yaml:ro
- ./data:/var/lib/plutia
restart: unless-stopped
Upgrade and Backup Guidance
- Stop the process cleanly (
SIGTERM) to flush pending writes. - Back up
data/index,data/ops, anddata/checkpointstogether. - Keep the same
modeper data directory across restarts. - Upgrade binaries first in staging, then production using the same on-disk data.
Which Mode Should I Run?
| Mode | Disk Usage (at current PLC size) | CPU Usage | Ingestion Behavior | Verification Guarantees | Historical Archive? | Checkpoint Proof Support? | Recommended For |
|---|---|---|---|---|---|---|---|
mirror |
~1.17 KB per operation (~100 GB at current PLC head; scales linearly) | High sustained | Continuous full replay + polling | Full chain verification at ingest (verify=full), deterministic state, signed checkpoints |
Yes | Yes | Independent mirrors, auditors, proof-serving infrastructure |
resolver |
~0.8 KB/op (~60–70 GB at current PLC head) | Medium to high sustained | Continuous replay + polling, state-only storage | Full verification at ingest; stores resolved state without retaining historical block archive | No | No | Operators who want full verification with lower storage overhead |
thin |
~475 bytes per cached DID (scales with active usage, not global PLC size) | Low idle, bursty on requests | No global replay; fetches and verifies per DID on demand | Performs full signature and prev-link verification for each requested DID chain before caching. | No | No | Edge deployments, small VPS instances, low-disk resolvers |
Disk usage scales linearly with total PLC operation count.
Mirror Mode
- Maintains a full operation archive and materialized state.
- Generates signed checkpoints and supports proof serving.
- Preserves full DID audit history.
- Operates with the highest autonomy because it does not depend on upstream for resolved DIDs once ingested.
- Has the largest disk footprint.
Resolver Mode
- Replays and verifies the global stream, but does not keep historical op blocks.
- Supports full verification policies while reducing storage versus mirror mode.
- Maintains current resolved state for all ingested DIDs.
- Offers a balanced operational profile for most self-hosted operators.
- Resolver mode does not support checkpoint inclusion proofs because it does not retain historical block references required for Merkle inclusion verification.
Thin Mode
- Fetches DID logs from upstream on demand and verifies them locally.
- Stores only verified latest DID state plus cache metadata.
- Uses minimal disk and scales with active DID usage, not total PLC history.
- Depends on upstream availability at request time.
- Does not support checkpoint inclusion proofs.
Quick Recommendations
< 10 GBavailable disk: runthin.20–80 GBavailable disk: runresolver.100 GB+available disk: runmirror.
Security Tradeoffs
thinmode still verifies signature chains and prev linkage, but resolution depends on upstream availability for cache misses or refreshes.mirrormode is the most self-contained because it stores full verified history locally and can serve proofs from local checkpoints.resolvermode sits between the two: it verifies globally like mirror mode but does not retain a full historical archive for proof reconstruction.
Mirror Comparison
Use:
plutia compare --config=config.default.yaml --remote=https://mirror.example.com
The command fetches remote /checkpoints/latest and compares:
- checkpoint sequence
- DID Merkle root
- signature presence
Behavior:
- same sequence + different root => divergence warning and non-zero exit
- different sequences => reports which mirror is ahead and exits non-zero
- matching sequence/root/signature presence => success
License
MIT OR Apache-2.0