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
123 lines
4.7 KiB
Markdown
123 lines
4.7 KiB
Markdown
# 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-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.
|
|
- `aldabra-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.
|
|
- `aldabra-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 `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<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 `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.
|