README + supporting docs were written for ourselves (deployment paths,
internal product comparisons, internal task lists, build pipeline
artifacts) instead of for users of the software. This pass refocuses
them on what the software is, how to install, configure, and use it.
- README.md: full rewrite. New shape — What it does / Architecture /
Build / Run / Configuration / MCP tools / Security model / Status /
License / Dependencies. Drops the internal "why we built it"
narrative, drops phase-status claims that drifted stale, drops
internal deployment paths.
- ROADMAP.md: deleted. Was an internal task-list with [x]/[ ] items
showing incremental private development. The README's Status
section now communicates what's actually shipped.
- docs/architecture.md: scrub cross-project comparisons referencing
unrelated internal Sulkta codebases.
- aiken-escrow/README.md: drop reference to a non-existent spec file;
rewrite the Status checklist to reflect what's actually done
rather than what was open at the time of writing.
- audits/2026-05-09-escrow-e2e.md: scrub internal image names +
container paths; the audit findings (chain hashes, validator hash,
what each tx proved) are the public-useful part and stay.
- audits/2026-05-09-escrow-internal-audit.md: drop references to
feature-flag-gated branches that no longer exist.
- Dockerfile: drop the dead `escrow_wip surface` phrase from comments.
- Cargo.toml: drop the cross-project comparison comment that named
an unrelated internal service.
- crates/aldabra-{core,dao}: scrub internal preprod-test naming from
source comments — same technical content, generic phrasing.
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.
|
|
|
|
The split is a deliberate auditability + replaceability boundary —
|
|
each crate has a single responsibility and the security-sensitive
|
|
one has no I/O dependencies.
|
|
|
|
## 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.
|