diff --git a/Cargo.toml b/Cargo.toml index 0b18bee..8e78e53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" rust-version = "1.75" description = "Grin light-wallet-server: register a view credential, scan the chain in the background, store recognized outputs in a DB, and serve balance/outputs/broadcast/height." license = "MIT" +repository = "https://github.com/Such-Software/grin-lws" +readme = "README.md" +keywords = ["grin", "light-wallet-server", "lws", "mimblewimble", "wallet"] +categories = ["cryptography::cryptocurrencies"] +# crates.io publish is gated until the rangeproof-rewind crypto lands (scaffold). publish = false [[bin]] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d5b93e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Such Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 34c85d1..77bfc04 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ view credential, run a **background scanner** over the Grin chain, **store** recognized outputs (with recovered derivation paths) in a **database**, and serve balance / unspent-outputs / broadcast / height fast from that store. -It is the Grin analogue of monero-lws, and lets any backend proxy to it exactly -the way `smirk-backend-core`'s `xmr_wow.rs` proxies to the Monero LWS via -`infra/lws/client.rs`. +It is the Grin analogue of monero-lws: a wallet backend proxies to it exactly the +way a backend proxies to the Monero LWS — forward a view credential, serve the +stored results back to the wallet. **Status: SCAFFOLD.** The HTTP surface, DTOs, config, database layer, and the scanner loop *structure* are real and build. The two chain-crypto seams — block @@ -20,12 +20,12 @@ milestone (see [Build order](#build-order)). ## Why this exists (and why it needs a DB) -`smirk-backend-core`'s current `POST /wallet/grin/scan` is an **on-demand, -stateless proxy**: each request forwards a `rewind_hash` to grin-wallet's -`scan_rewind_hash`, which rescans the chain and returns matches, **storing -nothing**. That works for Grin's small UTXO set, but it is *not* a -light-wallet-server — every read re-pays the scan cost, and it has no output -store to recover derivation paths from. +A common wallet-backend approach is an **on-demand, stateless** grin scan: each +request forwards a `rewind_hash` to grin-wallet's `scan_rewind_hash`, which +rescans the chain and returns matches, **storing nothing**. That works for +Grin's small UTXO set, but it is *not* a light-wallet-server — every read +re-pays the scan cost, and it has no output store to recover derivation paths +from. A real LWS — like monero-lws — keeps a **per-account output store** maintained by a **background scanner**, so client reads are O(rows) not O(chain), and it can @@ -145,33 +145,32 @@ chain_cursor(id=1 PK, height, block_hash) -- reorg-safe resume --- -## How `smirk-backend-core` proxies to it +## How a wallet backend proxies to it -Identical wiring to the XMR/WOW LWS (`infra/lws/client.rs`): +Identical wiring to a Monero LWS client — forward the view credential, serve the +stored results: ``` -wallet ──JWT──▶ smirk-backend-core ──private──▶ grin-lws ──▶ grin node - /api/v1/wallet/grin/scan /get_unspent_outs (Foreign API v2) +wallet ──JWT──▶ wallet backend ──private──▶ grin-lws ──▶ grin node + /wallet/grin/scan /get_unspent_outs (Foreign API v2) ``` -1. Add `GrinLwsConfig { lws_url, admin_url?, admin_key? }` in `config.rs`, next - to `LwsConfig`. -2. Add a `GrinLwsClient` in `infra/` mirroring `infra/lws/client.rs` (shared - `reqwest::Client`, timeouts, size-capped hostile-response reads, `Secret`- - wrapped admin key). -3. Behind a `features.chains.grin_lws` flag, `POST /wallet/grin/scan` forwards - the `rewind_hash` to grin-lws `/get_unspent_outs` (returning outputs **with - paths**) instead of driving grin-wallet `scan_rewind_hash` on-demand. +1. Add a grin-lws client config (`lws_url`, optional `admin_url` + `admin_key`) + alongside your existing Monero-LWS config. +2. Add an HTTP client mirroring your Monero-LWS client (shared `reqwest::Client`, + timeouts, size-capped hostile-response reads, a `Secret`-wrapped admin key). +3. Behind a feature flag, have your grin scan endpoint forward the `rewind_hash` + to grin-lws `/get_unspent_outs` (returning outputs **with paths**) instead of + driving grin-wallet `scan_rewind_hash` on-demand. 4. Register at balance-fetch time by forwarding `rewind_hash` to grin-lws - `/register` (idempotent), exactly as `fetchLwsBalance` awaits `registerLws` - for XMR/WOW. + `/register` (idempotent), exactly as you await LWS registration for Monero. **Non-custodial guarantee preserved end to end:** the backend forwards only the `rewind_hash` and stores nothing; grin-lws holds the `rewind_hash` for scanning just as monero-lws holds the view key. No spend key ever leaves the wallet. -When grin-lws ships, the client can **drop** its `grin_identify_output` helper — -`/get_unspent_outs` returns paths directly. +Once grin-lws is wired in, the wallet client can **drop** any client-side +output-identify helper — `/get_unspent_outs` returns paths directly. --- @@ -228,3 +227,12 @@ Run as a private service alongside a grin node and its database. Bind to loopback / a private network; expose it only to the proxying backend. Add a readiness probe on `/ready`. Everything is env-configured — no code changes to deploy. + +--- + +## License & provenance + +MIT — see [`LICENSE`](LICENSE). + +grin-lws was extracted from the [Smirk](https://smirk.cash) wallet's backend and +generalized into a standalone service that any wallet backend can run. diff --git a/src/api.rs b/src/api.rs index 6b6ebf2..01fd318 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,11 @@ -//! HTTP surface — mirrors the monero-lws user API so smirk-backend-core can -//! proxy to it exactly as it proxies the XMR/WOW LWS (see `infra/lws/client.rs`). +//! HTTP surface — mirrors the monero-lws user API so a wallet backend can proxy +//! to it exactly as it proxies a Monero LWS. //! //! AUTH: the user API is unauthenticated at THIS layer — grin-lws binds to -//! loopback / a private network and trusts smirk-backend-core (which does the -//! JWT auth) as its only caller. The admin API (`/list_accounts`, `/rescan`) is -//! gated by a shared bearer (`GRINLWS_ADMIN_KEY`) and disabled when unset. +//! loopback / a private network and trusts the proxying wallet backend (which +//! does the JWT auth) as its only caller. The admin API (`/list_accounts`, +//! `/rescan`) is gated by a shared bearer (`GRINLWS_ADMIN_KEY`) and disabled +//! when unset. //! //! The `rewind_hash` in every user request is a VIEW credential (public-key //! derived — it cannot spend). Its request DTOs omit `Debug` so it is never diff --git a/src/error.rs b/src/error.rs index 39dd5e6..5a795bf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,7 @@ //! Error type mapped to HTTP responses. //! -//! Hostile-upstream hygiene (carried over from smirk-backend-core's LWS/grin -//! clients): a node/DB error NEVER interpolates an untrusted upstream response -//! body into the client-facing message. +//! Hostile-upstream hygiene: a node/DB error NEVER interpolates an untrusted +//! upstream response body into the client-facing message. use axum::{http::StatusCode, response::IntoResponse, Json}; diff --git a/src/grin.rs b/src/grin.rs index 8163f53..f5f9d31 100644 --- a/src/grin.rs +++ b/src/grin.rs @@ -4,9 +4,8 @@ //! block outputs (commit + rangeproof), and relay a finalized transaction. This //! is the ONLY component that reaches the chain; everything else reads the DB. //! -//! Hostile-upstream hygiene (carried from smirk-backend-core's node client): -//! per-request + connect timeouts, a streaming body-size cap, and errors that -//! never interpolate an untrusted node body. +//! Hostile-upstream hygiene: per-request + connect timeouts, a streaming +//! body-size cap, and errors that never interpolate an untrusted node body. //! //! REWIND (the hard, deferred part): recognizing which of a block's outputs //! belong to a registered account means rewinding each output's Bulletproof diff --git a/src/main.rs b/src/main.rs index c7a8afb..c78a825 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,25 +10,24 @@ //! //! WHY A DB //! -------- -//! smirk-backend-core's current `POST /wallet/grin/scan` is an ON-DEMAND, -//! STATELESS proxy: each request forwards a `rewind_hash` to grin-wallet's -//! `scan_rewind_hash`, which rescans and returns matches, storing NOTHING. That -//! is fine for grin's small UTXO set but it is NOT a light-wallet-server. A real -//! LWS — like monero-lws — keeps a per-account output store maintained by a -//! background scanner, so reads are O(rows) not O(chain). Hence this service -//! needs a database (the 3 tables in `migrations/`). +//! A typical wallet backend exposes an ON-DEMAND, STATELESS grin scan: each +//! request forwards a `rewind_hash` to grin-wallet's `scan_rewind_hash`, which +//! rescans and returns matches, storing NOTHING. That is fine for grin's small +//! UTXO set but it is NOT a light-wallet-server. A real LWS — like monero-lws — +//! keeps a per-account output store maintained by a background scanner, so reads +//! are O(rows) not O(chain). Hence this service needs a database (the 3 tables in +//! `migrations/`). //! -//! HOW smirk-backend-core PROXIES TO IT -//! ------------------------------------ -//! Identical wiring to the XMR/WOW LWS (`infra/lws/client.rs`): the backend adds -//! a `GrinLwsConfig { lws_url, admin_url?, admin_key? }` and a `GrinLwsClient`; -//! when the `grin_lws` feature is on, `POST /wallet/grin/scan` forwards the -//! `rewind_hash` to this service's `/get_unspent_outs` (and registers via -//! `/register` at balance-fetch time, idempotently, exactly as `fetchLwsBalance` -//! awaits `registerLws` for XMR/WOW). Non-custodial guarantee preserved: the -//! backend forwards only the `rewind_hash` and stores nothing; grin-lws holds -//! the `rewind_hash` for scanning just as monero-lws holds the view key. NEVER a -//! spend key. +//! HOW A WALLET BACKEND PROXIES TO IT +//! ---------------------------------- +//! Identical wiring to a Monero LWS client: the backend adds a grin-lws client +//! (`lws_url`, optional `admin_url` + `admin_key`); behind a feature flag, its +//! grin scan endpoint forwards the `rewind_hash` to this service's +//! `/get_unspent_outs` (and registers via `/register` at balance-fetch time, +//! idempotently, exactly as it awaits LWS registration for Monero). Non-custodial +//! guarantee preserved: the backend forwards only the `rewind_hash` and stores +//! nothing; grin-lws holds the `rewind_hash` for scanning just as monero-lws +//! holds the view key. NEVER a spend key. //! //! STATUS: SCAFFOLD. Routes, DTOs, config, DB layer, and the scanner loop //! structure are real; the two chain-crypto seams — block parsing diff --git a/src/secret.rs b/src/secret.rs index c367701..5ea2b85 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,5 +1,5 @@ //! A tiny redacting wrapper so secrets (DB URL, node auth, admin key) never leak -//! into `Debug` output or tracing spans. Mirrors smirk-backend-core's `Secret`. +//! into `Debug` output or tracing spans. #[derive(Clone)] pub struct Secret(String);