aldabra/docs/architecture.md
Kayos 1f1993ed97 rename: sulkta-wallet → aldabra (per Cobb 2026-05-04)
Aldabra giant tortoise (Aldabrachelys gigantea) — endemic to the
Aldabra atoll, up to 250 kg, 150-year lifespan. Long-lived,
defended, slow but unstoppable. Better metaphor for the wallet
than 'sulkta-wallet' which was on-the-tin descriptive.

All renames in one pass:
- repo: Sulkta-Coop/sulkta-wallet → Sulkta-Coop/aldabra (via gitea API)
- workspace dir: sulkta-wallet → aldabra
- crate dirs: wallet-{core,chain,mcp} → aldabra-{core,chain,mcp}
- crate names + path imports in Cargo.toml workspace + each crate
- binary name: sulkta-wallet → aldabra
- README, ROADMAP, docs/architecture: all references swept
2026-05-04 10:11:23 -07:00

4.7 KiB

aldabra 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.

  • aldabra-coreno 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.
  • aldabra-chainall 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.
  • aldabra-mcpthe 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 aldabra-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
  ↓
  aldabra-mcp asks for an encryption passphrase
  ↓
  age-encrypt the mnemonic phrase
  ↓
  write to $ALDABRA_DATA/mnemonic.age
  ↓
  derive RootKey, hold in RAM, zeroize the source phrase
  ↓
  daemon ready

Subsequent runs:
  read $ALDABRA_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 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 aldabra-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.