📺 Watch the Voiceover Demo Video
A Logos Basecamp app that lets anyone upload a document and make it permanently discoverable — without a central server, without requiring the uploader to hold tokens, and without a single point of censorship.
A whistleblower selects a file, adds metadata, and submits. The app uploads the bytes to Logos Storage, broadcasts the resulting CID and metadata envelope over Logos Delivery so the document is immediately discoverable, and optionally anchors the CID on-chain via a SPEL program on the Logos Execution Zone.
The uploader never needs tokens, a wallet, or to coordinate with anyone. Publication (upload + broadcast) and anchoring (on-chain commitment) are separate steps run by separate parties:
user (file + meta)
│
▼
Basecamp app ── whistleblower-core / @whistleblower/indexing-module ──┐
│ Publisher │
│ 1. storage.put(bytes) ──► Logos Storage │
│ 2. canonicalise + sha256 │
│ 3. delivery.publish(topic, env) ──► Logos Delivery topic │
▼ │
/logos/whistleblower/v1/documents ◄───────────────────────────────────┘
│
▼
ANY third party — NGO, journalist collective, automated guardian
$ batch-anchor --backend lez …
subscribes ─ accumulates CIDs ─ submits anchor_batch (up to 50/tx) ──► LEZ registry
resumes from .batch-anchor-state.json after a restart
So a source with no on-chain footprint can publish, and anyone else can durably anchor the CID later — in bulk, permissionlessly, idempotently.
Three independent layers of idempotence keep re-runs safe: Delivery
dedup on (topic, cid) ▸ the CLI's atomically-persisted seen-set ▸ the
registry's init_if_empty PDA write. Re-anchoring an existing CID is
always a no-op.
| Deliverable | Where |
|---|---|
| Basecamp app — QML manifest + Vite/TypeScript web flavour | packages/basecamp-app/ |
| Indexing module (Rust) — canonical, FFI-friendly, reusable | packages/whistleblower-core/ |
| Indexing module (TypeScript) — byte-identical port | packages/indexing-module/ |
| On-chain CID registry — SPEL program for LEZ, with IDL | packages/registry-program/ |
Permissionless batch anchor CLI — batch-anchor binary |
packages/batch-anchor-cli/ |
| End-to-end demo + CU benchmarks | scripts/demo.sh · scripts/benchmark.sh |
| Architecture + demo write-ups | docs/ARCHITECTURE.md · docs/DEMO.md |
The Rust crate and TypeScript port produce byte-for-byte identical canonical envelope bytes — pinned by a cross-language fixture test that fails CI if they ever diverge.
Everything is dual-licensed MIT OR Apache-2.0.
The LEZ live-stack workflow is not a
mock. On every push it boots a real logos-scaffold sequencer, builds
the RISC0 guest with RISC0_DEV_MODE=0 (real proofs), and deploys
the registry program to the running chain:
{ "program": "whistleblower_registry",
"program_id": "80aefab4d46df398f94e525fc78a4968a96512faa26041196cb5df27f75b83ec",
"status": "submitted" }That program_id is the RISC0 image ID computed from the deployed guest
ELF — deterministic from the binary, verifiable by anyone who rebuilds
it. The same job then runs the end-to-end demo and the compute-unit
benchmark against the live sequencer.
The standard CI workflow runs rustfmt,
clippy -D warnings, 9 Rust tests, 11 TypeScript tests, and the
demo on every push. Both workflows are green on main.
# from a clean clone — no external services required
cargo build --workspace --release
pnpm install
bash scripts/demo.shExpected tail:
PUBLISH ok cid=bafy… size=109 topic=/logos/whistleblower/v1/documents
ANCHOR ok tx=tx-1 newly_anchored=1 cu=Some(7000)
LOOKUP ok cid=bafy… present_in_registry=true
════════════════════════════════════════════════
DEMO COMPLETE
• upload → CID issued by Logos Storage
• broadcast → envelope on /logos/whistleblower/v1/documents
• batch-anchor → CID present in registry
════════════════════════════════════════════════
For the live LEZ path (make build && make deploy the registry, then
RISC0_DEV_MODE=0 MODE=LEZ bash scripts/demo.sh), see
docs/DEMO.md.
Every box is exercised by CI; each links to the source that satisfies it.
Functionality
- Upload to Logos Storage → CID, with retry —
publisher.rs - Broadcast metadata envelope on a Delivery topic, idempotent on
(topic, cid)—envelope.rs - Optional on-chain anchor, distinct from upload —
publisher.anchor()is a separate call - Standalone, permissionless batch-anchor CLI, ≤50 CIDs/tx —
batch-anchor-cli - Registry stores
(cid, metadata_hash, anchor_timestamp), queryable vialookup—whistleblower_registry.rs - Self-contained, reusable indexing module — zero coupling to the app
Usability
- Basecamp GUI: QML manifest + entry point and a buildable web preview —
basecamp-app/ - Indexing module shipped as a library/SDK with API README — Rust + TypeScript
- Checked-in IDL, regenerable via
make idl—idl/
Reliability
- Jittered exponential back-off on upload, surfaces final error —
retry.rs - Delivery dedup + CLI resumes from last batch after interruption — regression tests in
pipeline.rs
Performance
- Measured CU for single + 50-CID batch —
docs/BENCHMARKS.md. Batching 50 CIDs is far cheaper per-CID than 50 single anchors.
Supportability
- Registry deployed + tested on a live LEZ sequencer — see the live-stack workflow
- End-to-end demo with
RISC0_DEV_MODE=0against a real sequencer —docs/DEMO.md - CI green on
main; README covers build, deploy, running the app + batch tool, querying the registry
// Cargo.toml: whistleblower-core = { git = "https://github.com/chinesepowered/hack-lp0017" }
use std::sync::Arc;
use whistleblower_core::*;
let publisher = Publisher::new(
Arc::new(my_storage), // your StorageAdapter
Arc::new(my_delivery), // your DeliveryAdapter
Some(Arc::new(my_anchor)), // your AnchorAdapter (optional)
PublisherConfig::default(),
);
let result = publisher.publish(bytes, PublishMeta {
title: "…".into(), description: "…".into(),
content_type: "application/pdf".into(), tags: vec!["leak".into()],
}).await?;
// result.envelope.cid is on Storage + broadcast on Delivery.
// result.metadata_hash is what anyone can later commit on-chain.Every arrow in the pipeline is an adapter trait. CI exercises the
in-memory adapters end-to-end; the live adapters in
batch-anchor-cli/src/adapters/
and the window.logos.bridge web shim call the real services. The SPEL
program follows the logos-co/whisper-wall
patterns and pins LEZ v0.2.0-rc3. For TypeScript or native C++/QML
callers, see packages/indexing-module/README.md
and packages/whistleblower-core/src/ffi.rs.
.
├── README.md, docs/{ARCHITECTURE,DEMO,BENCHMARKS}.md
├── Cargo.toml # Rust workspace · package.json (pnpm workspace)
├── scripts/{demo,benchmark}.sh # reproducible demo + CU benchmark
├── .github/workflows/
│ ├── ci.yml # rustfmt + clippy + tests + demo
│ └── lez-live.yml # real deploy to a live LEZ sequencer
└── packages/
├── whistleblower-core/ # canonical Rust indexing module + FFI
├── indexing-module/ # TypeScript port + in-memory adapters
├── registry-program/ # SPEL program for LEZ (+ IDL, Makefile)
├── batch-anchor-cli/ # Rust binary; mock + LEZ-live backends
└── basecamp-app/ # Basecamp web + QML manifest
Dual-licensed under MIT OR Apache-2.0 at your option. See
LICENSE-MIT and LICENSE-APACHE.