aldabra/docs/architecture.md
Kayos 489b58cc1e phase 1 scaffold: cargo workspace + 3 crates + roadmap + architecture
Repo skeleton for sulkta-wallet, the rust-native cardano lite wallet
with MCP server interface. Builds end-to-end, types in place,
real cardano primitives land next pass.

Crates:
  wallet-core   — pure crypto + types. mnemonic, key derivation,
                  signing. No I/O. Security boundary.
  wallet-chain  — pluggable backends. ChainBackend trait, Koios
                  client (stub for now). Ogmios + submit in phase 2.
  wallet-mcp    — the binary. stdio MCP transport via rmcp.

Phase plan in ROADMAP.md, threat model in docs/architecture.md.

This is also Cobb's first Rust project + a real-world workout for
crafting-table's rust toolchain.
2026-05-04 10:02:32 -07:00

123 lines
4.7 KiB
Markdown

# sulkta-wallet architecture notes
Deeper design notes than the README. Read this before extending.
## Crate boundaries — and why
The three-crate split exists to keep the security-sensitive code
auditable in isolation.
- `wallet-core`**no I/O.** Given a mnemonic + a derivation path,
produces keys + addresses + signatures. Deterministic, no
dependencies on tokio, reqwest, MCP, or anything that could
introduce side channels. Easy to audit because it's narrow.
- `wallet-chain`**all the I/O lives here.** Trait-first so the MCP
layer never knows whether it's talking to Koios, Ogmios, or a future
backend. Future contributors swap implementations without touching
the security-sensitive crate.
- `wallet-mcp`**the binary glue.** Owns process lifecycle, config
loading, MCP transport, tool registration, error mapping. The
thinnest layer.
This is the same pattern PetalParse + Cauldron use with their
`<service>-core` / `<service>-web` split. Consistent across Sulkta
codebases.
## Threat model
The wallet is single-user, single-machine, behind an MCP transport
that's only reachable by the spawning process. Threats we care about,
roughly in order:
1. **LLM mistake.** The most likely threat: the LLM (me, future-me,
or an agent) constructs a wrong transaction and asks the wallet
to sign it. Mitigations: hard caps on outbound value (config), TX
review tool that returns a human-readable summary before signing,
`--dry-run` flag for any state-changing tool.
2. **Daemon process compromise.** If the wallet binary is exploited
(e.g. via a malformed Koios response triggering memory corruption),
the keys are at risk. Mitigations: keep `wallet-core` narrow
(smaller attack surface), zeroize on drop, future: drop
privileges + seccomp the daemon.
3. **Disk read.** The encrypted mnemonic on disk could be exfiltrated.
Mitigations: age encryption (audited modern primitive), passphrase
never persisted, separate disk path from the daemon's runtime
data.
4. **Memory dump / swap.** Live key material in RAM could leak via
swap, hibernate state, or a core dump. Mitigations: zeroize on
drop, no swap on Lucy (Cobb confirms), future: `mlock` the
key-holding pages.
5. **Network.** No exposure — stdio MCP transport only. If we ever
add a TCP listener that's a separate threat-modeling exercise.
## Mnemonic lifecycle
```
First run:
user pastes mnemonic at interactive prompt
wallet-mcp asks for an encryption passphrase
age-encrypt the mnemonic phrase
write to $SULKTA_WALLET_DATA/mnemonic.age
derive RootKey, hold in RAM, zeroize the source phrase
daemon ready
Subsequent runs:
read $SULKTA_WALLET_DATA/mnemonic.age
prompt for passphrase
age-decrypt → ephemeral String
derive RootKey, immediately zeroize the decrypted phrase
daemon ready
```
The decrypted phrase exists in RAM only between age-decrypt and
RootKey derivation — measured in milliseconds.
## Why pallas over cardano-serialization-lib
Pallas is rust-native and modular. cardano-serialization-lib is
rust-with-WASM-as-the-primary-target — its API shape reflects the
JS ecosystem more than the Rust ecosystem (Result-as-Option-as-error,
ToString-heavy, Box<dyn Error> at the boundary). Pallas reads more
idiomatic.
That said: if pallas-txbuilder is ever missing something we need, fall
back to cardano-serialization-lib via the `@emurgo/cardano-serialization-lib`
Rust crate. It's the canonical TX builder, used by Yoroi.
## Why rmcp over rolling our own MCP server
The MCP wire protocol is JSON-RPC 2.0 with specific lifecycle messages
(`initialize`, `tools/list`, `tools/call`, etc.). It's possible to
hand-roll, but rmcp handles the boilerplate, the param schema
generation from Rust types, and the stdio framing. Standard SDK
choice for any Rust MCP server.
If rmcp turns out to be unstable / too slow to compile / API churn,
the fallback is to write the JSON-RPC 2.0 handlers directly with
serde — a few hundred lines.
## Future: hot vs cold signing split
For mainnet operations over a configurable lovelace threshold, the
phase-2 design is:
1. `wallet.send` validates the tx, builds it, but **doesn't sign**.
2. Returns the unsigned CBOR + a one-line human summary ("send 100
ADA to addr1xyz, fee 0.17 ADA, expected balance after: …").
3. LLM relays the summary to Cobb, gets approval.
4. Cobb runs a separate `sulkta-wallet-cold-sign` CLI on a different
box (offline laptop, cardano-signer, whatever) — paste the CBOR,
approve, paste back the signed CBOR.
5. `wallet.submit_signed_tx` takes the signed CBOR + submits.
This mirrors the ADAMaps MAP treasury cold-signing pattern. Avoids
mainnet auto-sign by an LLM agent.