Moonshine is a private, lightweight CLI light wallet for the DarkFi blockchain. It connects to darkfi-lightwalletd over gRPC. The default sync is padded trial decryption. Experimental UnifOMR is off unless you pass --unifomr or set experimental_unifomr = true.
3.01.07 (crate semver 3.1.7): Speaks lightwalletd proto 2.1.0. A normal moonshine sync pulls compact Money notes, DAO calls, and auth-token events from darkfi-lightwalletd, trial-decrypts them into the local SQLCipher wallet (same scan semantics as drk, without full-chain validation like darkfid), and deletes processed compact blocks from the encrypted UniFFI-format cache. Beyond sync and DRK send, Moonshine can mint/freeze Money tokens, create/mint DAOs, and deploy/lock Deployooor WASM authorities through the same lightwalletd SendTransaction path (native fee attached locally). moonshine sync --unifomr remains experimental and is not maintained until DarkFi mainnet contracts are live. Tor is Arti 0.48, with a fresh circuit per dial. A send spends one coin, the largest that covers the amount plus fee. DarkFi checkout 44ec78b6e (nighthawk-testnet, path dependency). Testnet explorer: https://explorer.testnet.dark.fi.
Trial decryption is the default. Payments from
drk, Nighthawk, and Moonshine are found from the note itself.--force-trialstill forces a full pass.--unifomrregisters clue keys and can miss funds when the other wallet is not registered on this same lightwalletd.
Unlike full nodes (darkfid) or the heavy CLI wallet (drk), Moonshine keeps a pruned SQLite wallet (SQLCipher + wrapped secrets) and a small on-disk footprint.
- Prerequisites
- Build
- Configuration
- Quick start
- CLI command tree
- Features
- Related projects
- License
- Create a token and a DAO
| Requirement | Notes |
|---|---|
| Rust stable | rustup |
protoc |
On PATH |
Sibling darkfi-nighthawk-testnet |
Directory name is literal. Pin 44ec78b6e (full 44ec78b6efbf1d3e797f99bffe95195994b2488d, nighthawk24 nighthawk-testnet). A darkfi checkout on master is not enough, and git.dark.fi returns HTTP 504 and does not have this pin. You may ln -s nighthawk-android-wallet/third_party/darkfi darkfi-nighthawk-testnet — that tree is the same pin. |
Sibling darkfi-lightwalletd |
Proto from ../darkfi-lightwalletd/proto/lightwallet.proto |
| Running lightwalletd | Local or remote before moonshine sync |
Clone the path dependencies as sibling directories (names matter — see Cargo.toml / build.rs):
parent/
darkfi-nighthawk-testnet/ # MUST be this name — pin 44ec78b6e, not git.dark.fi master
darkfi-lightwalletd/ # gRPC server + proto + UnifOMR crate
moonshine/ # this repo
# optional; same pin if you already built Android:
nighthawk-android-wallet/third_party/darkfi
Reuse the Android vendored tree (same pin) instead of a second clone:
# from the parent/ directory that contains moonshine/
ln -sfn nighthawk-android-wallet/third_party/darkfi darkfi-nighthawk-testnet
# Cargo git deps inside that tree (git.dark.fi returns HTTP 504):
# kvdb-overlay https://gitlab.com/nighthawk24/kvdb-overlay accd2689
# RandomX https://codeberg.org/darkrenaissance/RandomX 0e31d5a
# rust-url https://github.com/darkrenaissance/rust-url branch v258Pin DarkFi to the same revision lightwalletd uses (reproducible builds):
# from darkfi-lightwalletd/
FORCE_DARKFI_PIN=1 ./scripts/fetch-darkfi.sh
# See docs/darkfi-pin.md — current SHA 44ec78b6efbf1d3e797f99bffe95195994b2488dMoonshine’s own pruned wallet remains SQLCipher (PRAGMA key) by design — it does
not use upstream bin/drk / turso. Mobile/desktop UniFFI clients track tip drk.
brew install protobuf # or: sudo apt install protobuf-compilerCargo.lock is committed so release builds stay reproducible.
From the moonshine/ directory (with ../darkfi-nighthawk-testnet and ../darkfi-lightwalletd present):
cargo build --release
# Binary: ./target/release/moonshine
cargo test~/.config/moonshine/config.toml (created on first run):
server_url = "http://127.0.0.1:9067"
network = "testnet"
use_tor = false# Local LWD (Tor auto-skipped for loopback):
moonshine --server http://127.0.0.1:9067 syncOptional wallet passphrase for SQLCipher (otherwise a random {wallet}.pass file is created):
export MOONSHINE_WALLET_PASS='your-strong-passphrase'network must match lightwalletd (mainnet / testnet / localnet). Localnet payment addresses use the Mainnet D… prefix (same as drk).
Remote cleartext (http:// to non-loopback) is refused. For remote servers use https:// and set tls_pin_sha256 in config:
server_url = "https://lwd.example.com:9067"
tls_pin_sha256 = "<64 hex chars>" # SHA-256 of the *leaf* certificate DERMoonshine’s PinnedVerifier checks that hash against the presented leaf cert (same policy as mobile). Localhost / 127.0.0.1 / [::1] may use cleartext without a pin. See ../darkfi-lightwalletd/docs/TLS_PINNING.md.
# Terminal A — darkfid (testnet example)
# Terminal B — lightwalletd (see darkfi-lightwalletd README; bind 127.0.0.1 or TLS)
moonshine wallet create --name main
moonshine address default
moonshine sync
moonshine balance
moonshine tx list
moonshine tx send --to <address> --amount 1.5 --token DRK
# or send custom tokens or fund a DAO treasury with spend-hook & user-data:
# moonshine tx send --to <DAO_TREASURY_ADDR> --amount 10 --token ANON --spend-hook $(moonshine dao spend-hook) --user-data <DAO_BULLA>
# Money token / DAO / Deployooor (fee paid from a DRK coin; sync after mining/confirming)
moonshine token generate-mint --alias ANON
moonshine token mint ANON 100 "$(moonshine address list | awk '/★/{print $1; exit}')"
moonshine dao create GuideDAO 20 10 10 0.67 ANON
moonshine dao mint GuideDAO
moonshine dao spend-hook
moonshine dao balance GuideDAO
moonshine contract generate-deploy
moonshine contract deploy <CONTRACT_ID> ./my_contract.wasmFull token/DAO walkthrough (including DAO treasury deposits, proposals, and votes): docs/deploy-a-dao.md.
moonshine
├── wallet create | import | export | delete | list | info
├── address list | new | default
├── balance
├── coins list
├── tx send [--to <ADDR>] [--amount <VAL>] [--token <TOK>] [--spend-hook <HOOK>] [--user-data <DATA>] | list | show | broadcast | inspect
├── sync [--force-trial] [--unifomr] [--rebuild-merkle]
├── rescan
├── status
├── config [--server-url <URL>] [--network <NET>] [--use-tor <BOOL>] [--tls-pin <HEX>] [--experimental-unifomr <BOOL>]
├── doctor
├── prune
├── token generate-mint | import | list | mint | freeze
├── dao create | import | list | spend-hook | balance | calls | view | proposals | mint
├── contract generate-deploy | list | events | deploy | lock
└── version
| Feature | Status | Notes |
|---|---|---|
| Trial decrypt (default) | ✅ | Padded compact outputs, nullifiers, token mints, compact DAO calls |
| Token mint / freeze / send | ✅ | token generate-mint / mint / freeze via Money + native fee; custom token transfers with spend-hooks |
| DAO create / mint / treasury | ✅ | dao create / mint / spend-hook / balance / proposals; automatic DAO treasury trial decryption on sync |
| Contract deploy / lock | ✅ | Deployooor authority + WASM deploy / lock through lightwalletd |
| Clue PK register on sync | ✅ | Fail-closed if registration fails for all addresses. Session cache is FIFO-capped at 10,000 ids |
| UnifOMR send clue | ✅ | GetCluePublicKey → build_omr_clue_from_pk; abort if unavailable. tx send --no-omr omits the clue for trial-decrypt receive. Change outputs get extra_omr_clues, but strict sync still does not make that change spendable. Run sync --force-trial after the change is in a block |
| One input per send | ✅ | Largest coin that covers amount + fee. A two-input proof is not built; darkfid rejects it |
| Localnet UnifOMR (2026-10-21) | ✅ | 100/100 each way with Android and with desktop. --no-omr payments were received by those wallets under trial decrypt |
| Payment memo on wire | ✅ | --memo → OMR-aware bytes in recipient MoneyNote::memo (not local-only) |
| Multi detection_keys | ✅ | Up to 16 wallet secrets in GetUnifOmrDigest |
| Local BlockCache | ✅ | Sparse/PIR compact blocks cached beside wallet DB |
| Instant Sync Checkpoints | ✅ | Fast Merkle tree restore from authenticated CheckpointSnapshot (see docs/instant-sync-strategy.md) |
| Proto Version Lockstep | ✅ | Client proto 2.1.0. A 1.x or 3.x server is a hard mismatch; an empty server version is a mismatch |
| OMR Err → no silent trial (S15) | ✅ | Tip not advanced on OMR error |
| Experimental UnifOMR | optional | sync --unifomr or experimental_unifomr = true. Off by default |
| Tip regression rewind | ✅ | Rewinds sync height when tip < last synced. Deleted note/tx counts come from SQLite execute rowcounts, not log-line length |
chain_name network guard |
✅ | Must match config network |
| TLS pin remote HTTPS | ✅ | tls_pin_sha256 + PinnedVerifier (leaf DER SHA-256); remote cleartext refused |
| Address validation | ✅ | DarkFi checksum addresses |
| Network-aware addresses | ✅ | Create/import use config network |
| Feature | Status | Notes |
|---|---|---|
| SQLCipher DB (S14) | ✅ | PRAGMA key from MOONSHINE_WALLET_PASS or {db}.pass (0600) |
| Secret wrap | ✅ | Address secret_key BLOBs wrapped (MSK1 + MAC). The wrap key is stored inside the encrypted database. A legacy .wrapkey file is imported once and deleted |
| WAL + secure_delete | ✅ | SQLite pragmas on open |
| 22-word DarkFi mnemonic | ✅ | Same derivation as Nighthawk Android/iOS |
| Feature | Status | Notes |
|---|---|---|
| Log / error redaction | ✅ | Endpoints stripped from sync errors |
| Broadcast without clue refused | ✅ | tx broadcast requires parseable recipient for UnifOMR |
| Capabilities probe | ✅ | Skip digest if server reports OMR disabled / non-UnifOMR |
- Requires darkfi-lightwalletd with
fhe-omr - Cross-client crypto parity: RLWE
n=1024, signed AHE,CLUE_ERROR_BOUND=2, length-prefixed SealPIR limbs - Flow: RegisterCluePublicKey → GetClue → SendTransaction(omr_clue) → GetUnifOmrDigest → FetchPirBatch
- Default sync is trial decryption.
--unifomris experimental and unmaintained until DarkFi mainnet contracts are live. - Send fails closed unless GetClue ownership proof verifies (rejects directory decoys)
- First-run default LWD: loopback
http://127.0.0.1:9067(Tor off). Remote HTTPS still needstls_pin_sha256+ Tor if you want it. - Limits:
docs/unifomr_mvp_limits.md(Param2 active) - MVP fork archive:
docs/unifomr_mvp_archive.md - Checklist:
docs/verification-checklist.md
- One input per transaction. darkfid rejected a two-input transfer proof on the 2026-10-21 localnet. Selection spends a single coin that covers the amount plus the 0.05 DRK fee. It does not combine smaller coins. If no such coin is visible yet, the send stops before broadcast.
- One coin must cover the amount plus fee. Change from the previous send is visible after the next default trial-decrypt sync includes that block. A second coin is not combined into the proof.
- Experimental UnifOMR can hide funds.
--unifomronly sees payments whose clue was registered on this lightwalletd. Leave it off to receivedrkand other clue-less transfers. git.dark.fireturns HTTP 504 and does not publish pin44ec78b6e. Clonenighthawk24/darkfi(or symlink the Androidthird_party/darkfi).kvdb-overlay, RandomX, andrust-urlare fetched from GitLab, Codeberg, and GitHub.
| Sibling directory | Role |
|---|---|
../darkfi-nighthawk-testnet |
DarkFi SDK pin 44ec78b6e (symlink to nighthawk-android-wallet/third_party/darkfi) |
../darkfi-lightwalletd |
gRPC lightwalletd + shared UnifOMR |
../darkfi-mobile-ffi |
Shared UniFFI crate (Android / iOS / desktop) |
../nighthawk-android-wallet |
Android wallet (third_party/darkfi = same pin) |
../nighthawk-ios-wallet |
iOS wallet |
../nighthawk-app-desktop |
Desktop wallet (Tauri) |
AGPL-3.0-only (see source headers).