docs: rewrite for users — drop internal infra context

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.
- aiken-escrow/README.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.
- aiken-escrow/README.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.
This commit is contained in:
Sulkta 2026-05-10 20:56:25 -07:00
parent a7877c32d0
commit 93f0d2ebde
7 changed files with 152 additions and 109 deletions

View file

@ -59,10 +59,9 @@ bip39 = { version = "2", features = ["rand"] }
ed25519-bip32 = "0.4" ed25519-bip32 = "0.4"
cryptoxide = "0.4" cryptoxide = "0.4"
# At-rest encryption for the mnemonic + derived keys on disk. age is # At-rest encryption for the mnemonic + derived keys on disk. Modern,
# what the cauldron Fernet pattern would have been if we'd had it back # audited, FOSS, and the secret never has to round-trip through a
# then — modern, audited, FOSS, and the secret never has to round-trip # daemon password prompt.
# through a daemon password prompt.
age = "0.10" age = "0.10"
# Memory hygiene — wipe key material from RAM when keys go out of scope. # Memory hygiene — wipe key material from RAM when keys go out of scope.

View file

@ -57,12 +57,12 @@ RUN apt-get update && \
COPY --from=builder /build/target/release/aldabra /usr/local/bin/aldabra COPY --from=builder /build/target/release/aldabra /usr/local/bin/aldabra
# Escrow V3 validator CBOR (escrow_wip surface). Baked at /etc/aldabra/ # Escrow V3 validator CBOR. Baked at /etc/aldabra/escrow/validator.cbor.hex
# escrow/validator.cbor.hex so MCP escrow_*_unsigned tools can pass it # so MCP escrow_*_unsigned tools can pass it via `validator_script_path`.
# via `validator_script_path`. The MCP arg-truncation bug at >4500 hex # An MCP arg-truncation bug at >4500 hex chars makes the inline
# chars makes the inline `validator_script_cbor_hex` option unusable # `validator_script_cbor_hex` option unusable for the 7902-char compiled
# for the 7902-char compiled validator. Validator hash: # validator, so file-path mode is the canonical way to wire it. Validator
# a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d — check # hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d — check
# against the file's compiled hash before invoking on-chain ops. # against the file's compiled hash before invoking on-chain ops.
COPY aiken-escrow/validator.cbor.hex /etc/aldabra/escrow/validator.cbor.hex COPY aiken-escrow/validator.cbor.hex /etc/aldabra/escrow/validator.cbor.hex
COPY aiken-escrow/plutus.json /etc/aldabra/escrow/plutus.json COPY aiken-escrow/plutus.json /etc/aldabra/escrow/plutus.json

179
README.md
View file

@ -1,64 +1,58 @@
# aldabra # aldabra
Rust-native Cardano lite wallet with an MCP-server interface — built Rust-native Cardano lite wallet with an MCP-server interface. Built
for LLM-first usage (send, receive, mint, Plutus interaction). for LLM-first usage — send/receive ADA + native assets, mint, Plutus
script interaction, Conway governance, and a full Agora-on-Cardano
DAO client.
> **Status: Phase 1 scaffold (2026-05-04).** Compiles, structure in Named for the Aldabra giant tortoise: long-lived, defended, slow but
> place, real wallet primitives still landing. See `ROADMAP.md`. unstoppable.
## Why ## What it does
The existing Cardano MCP servers are either read-only doc gateways - **Wallet primitives.** Address derivation (CIP-1852), balance +
([Jimmyh-world/Cardano_MCP](https://github.com/Jimmyh-world/Cardano_MCP)) UTXO queries, ADA + native-asset transfers, multi-sig partial
or built on Blockfrost ([web3-mcp](https://github.com/strangelove-ventures/web3-mcp)) signing, encrypted-at-rest mnemonic.
which is a centralized API we deliberately don't depend on. We want a - **Minting.** CIP-25 + CIP-68 native assets, custom timelock /
wallet that talks directly to Koios + Ogmios endpoints — public, self- multisig policies, unsigned-tx flows for cold signing.
hosted, or whatever the operator points it at. - **Plutus V3.** Spending script-locked UTXOs with redeemers,
reference scripts, inline datum support.
- **Stake + Conway governance.** Pool delegation, DRep registration
+ deregistration, vote delegation, DRep vote casting on governance
actions.
- **DAO.** Agora-on-Cardano client — register multiple DAOs, view
stakes, create + cosign + vote on proposals, advance state-machine,
retract votes, destroy stakes.
- **Escrow.** Two-party agreement-with-veto Plutus V3 validator with
off-chain builders for the full open / deposit / agree / veto /
settle / refund lifecycle.
## Architecture ## Architecture
Three crates in a Cargo workspace: Cargo workspace with four crates:
| Crate | Responsibility | | Crate | Responsibility |
|---|---| |---|---|
| `aldabra-core` | Pure crypto + types. Mnemonic → root key (CIP-3), root → payment + stake key (CIP-1852), address construction, signing. **No I/O, no network.** This is the security boundary. | | `aldabra-core` | Pure crypto + types. Mnemonic → root key (CIP-3), root → payment + stake keys (CIP-1852), address construction, signing. **No I/O, no network.** This is the security boundary. |
| `aldabra-chain` | Pluggable backends for chain queries. `ChainBackend` trait, with Koios as the phase-1 implementation. Ogmios + submission paths in phase 2. | | `aldabra-chain` | Pluggable backends for chain queries. `ChainBackend` trait, with Koios as the default implementation. |
| `aldabra-mcp` | Binary. MCP server speaking stdio. Glues core + chain together, exposes tools to the LLM client. | | `aldabra-dao` | Off-chain side of the Agora DAO + escrow validators. Codecs + unsigned-tx builders. |
| `aldabra-mcp` | Binary. MCP server speaking stdio. Wires the other crates together and exposes tools to the LLM client. |
``` ```
┌─────────────────────────────┐ ┌─────────────────────────────┐
LLM client │ aldabra-mcp (bin) │ stdio LLM client │ aldabra-mcp (bin) │ stdio
─────────► │ tool handlers, lifecycle │ ────► ─────────► │ tool handlers, lifecycle │ ────►
└──────────┬──────────────────┘ └──────────┬──────────────────┘
┌────────┴────────┐ ┌────────┼────────┐
▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────┐ ┌─────────┐
│ aldabra-core │ │ aldabra-chain │ │ -core │ │-chain│ │ -dao │
│ keys, sign │ │ Koios/Ogmios │ │ keys/sig │ │ Koios│ │ Agora/ │
└──────────────┘ └──────────────┘ │ │ │ │ │ escrow │
└──────────┘ └──────┘ └─────────┘
``` ```
## MCP tools (target)
Phase 1:
- `wallet.address` — derived base address at account 0, index 0
- `wallet.balance` — ADA + native asset balance at the wallet's address
- `wallet.utxos` — list UTXOs
Phase 2:
- `wallet.send` — build, sign, submit a payment (ADA or native)
- `wallet.tx_status` — poll a submitted tx hash
Phase 3:
- `wallet.mint` — mint a CIP-25 / CIP-68 native asset
- `wallet.policy.create` — generate a policy script (timelock, multisig)
Phase 4:
- `wallet.script.attach` — attach an inline datum + reference script
- `wallet.script.spend` — spend a Plutus-locked UTXO with redeemer
- `wallet.stake.delegate` — delegate to a pool
## Build ## Build
```bash ```bash
@ -66,40 +60,101 @@ Phase 4:
cargo build --release cargo build --release
``` ```
For the Plutus validators (escrow) you also need [Aiken](https://aiken-lang.org/):
```bash
cd aiken-escrow
aiken build # produces plutus.json blueprint
```
## Run ## Run
```bash ```bash
# Direct invocation (smoke test only — does nothing useful in phase 1) # Smoke test (does nothing useful standalone — needs an MCP client)
./target/release/aldabra ./target/release/aldabra
# As an MCP server registered with Claude Code: # As an MCP server registered with Claude Code, add to ~/.claude.json:
# add to ~/.claude.json:
# "aldabra": { # "aldabra": {
# "command": "/path/to/aldabra", # "command": "/path/to/aldabra",
# "env": { # "env": {
# "ALDABRA_DATA": "/mnt/cache/appdata/aldabra" # "ALDABRA_DATA": "/path/to/wallet-data-dir",
# "ALDABRA_NETWORK": "preprod",
# "ALDABRA_KOIOS_BASE": "https://preprod.koios.rest/api/v1"
# } # }
# } # }
``` ```
Bootstrap a wallet on first run by setting `ALDABRA_BOOTSTRAP=new`
or `ALDABRA_BOOTSTRAP=import` in env — the binary prompts for a
passphrase, generates or imports a mnemonic, and writes an
age-encrypted `mnemonic.age` to `ALDABRA_DATA`.
## Configuration
Environment variables consumed at startup:
| Var | Required | Default | Notes |
|---|---|---|---|
| `ALDABRA_DATA` | yes | — | Directory holding `mnemonic.age`. Must exist; bootstrap before first MCP run. |
| `ALDABRA_NETWORK` | no | `preprod` | One of `mainnet`, `preview`, `preprod`. |
| `ALDABRA_KOIOS_BASE` | no | public Koios for the chosen network | Override to point at a self-hosted Koios. |
| `ALDABRA_PASSPHRASE` | yes | — | Unlocks `mnemonic.age`. Source from a docker secret or systemd `EnvironmentFile` — never commit it. |
| `ALDABRA_BOOTSTRAP` | no | (unset) | Set to `new` or `import` to enter bootstrap mode on next launch. |
## MCP tools
The server exposes ~40 tools across four prefixes. A summary:
- `wallet_*` — read (address/balance/utxos/network/stake_address),
send (with optional inline datum for script locks), mint, Plutus
script spending, stake delegation, Conway governance (vote
delegation, DRep operations).
- `chain_*` — read-only Koios passthroughs (tx info, address info,
pool list/info, epoch params, asset info, account info, tip).
- `dao_*` — Agora DAO client. Multi-DAO via config files. Live reads
(governor state, stake list, my stake) plus the full write set:
proposal create / cosign / vote / advance / retract-votes /
stake-destroy.
- `escrow_*` — two-party agreement-with-veto escrow. Build unsigned
txs for open / deposit / agree / veto / settle / refund-timeout.
Every write tool produces an unsigned tx for the caller to sign +
submit. No tool ever holds private keys outside the in-memory
derived-key scope.
## Security model ## Security model
- **Mnemonic source:** interactive bootstrap on first run, paste once, encrypted at - **Mnemonic source:** interactive bootstrap on first run, paste once
rest with [age](https://github.com/FiloSottile/age). Never written to disk in or generate, encrypted at rest with
plaintext. [age](https://github.com/FiloSottile/age). Never written to disk
in plaintext.
- **Derived keys:** in-memory only, `ZeroizeOnDrop` on every container. - **Derived keys:** in-memory only, `ZeroizeOnDrop` on every container.
- **Network exposure:** stdio MCP transport — never opens a TCP socket. - **Network exposure:** stdio MCP transport — the binary never opens
Only the spawning client process can reach it. a TCP listener. Only the spawning client process can talk to it.
- **Multi-network:** mainnet by default, but `--network preview` / - **Multi-network:** safe to point at preprod for development; the
`--network preprod` for testing without real ADA. same binary handles mainnet when you flip `ALDABRA_NETWORK`.
## See also ## Status
- `ROADMAP.md` — phased buildout Wallet + governance paths exercised on **mainnet**. DAO + escrow
- `docs/architecture.md` — deeper design notes paths exercised end-to-end on **preprod**; the escrow validator has
- [txpipe/pallas](https://github.com/txpipe/pallas) — the Rust Cardano undergone internal review (`audits/`) but **no third-party audit**.
building blocks we depend on Treat the escrow flows as use-at-own-risk until external review lands
- [Emurgo/cardano-serialization-lib](https://github.com/Emurgo/cardano-serialization-lib) — — see `aiken-escrow/README.md` for the WIP threat model.
reference TX builder if pallas-txbuilder doesn't cover something
- [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) — ## License
rmcp, the Rust MCP server SDK we use
See `LICENSE`.
## Dependencies of note
- [txpipe/pallas](https://github.com/txpipe/pallas) — Rust Cardano
primitives. Aldabra uses a [fork](https://github.com/Sulkta-Coop/pallas)
on the `feat-aux-data` branch that adds `auxiliary_data` +
`voting_procedures` support to `pallas-txbuilder`. PR upstream
pending.
- [Aiken](https://aiken-lang.org/) — Plutus V3 validator language
used for the escrow contract.
- [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk)
(`rmcp`) — MCP server framework.
- [age](https://github.com/FiloSottile/age) — at-rest encryption.

View file

@ -1,19 +1,14 @@
# aiken-escrow # aiken-escrow
> ⚠️ **WIP — UNAUDITED.** Preprod testing only. Do **NOT** route mainnet > ⚠️ **UNAUDITED.** No third-party security review has been performed.
> funds through this validator. No third-party security review has been > Internal review only. Treat as use-at-own-risk for high-value flows
> performed. > until external audit lands.
Two-party agreement-with-veto escrow validator (Plutus V3, Aiken Two-party agreement-with-veto escrow validator (Plutus V3, Aiken
v1.1.21). The off-chain (Rust) side lives in `crates/aldabra-dao` behind v1.1.21). The off-chain (Rust) side lives in `crates/aldabra-dao` and
the `escrow_wip` feature flag. is wired into the MCP tool surface via `aldabra-mcp`.
## Spec ## State machine
`aiken-escrow/README.md` documents the state machine, datum
shape, and redeemer invariants.
State machine:
``` ```
Open ──(both sign Agree)──▶ Agreed{at} ──(lock_period elapsed, no veto)──▶ Settle (→ recipient) Open ──(both sign Agree)──▶ Agreed{at} ──(lock_period elapsed, no veto)──▶ Settle (→ recipient)
@ -34,23 +29,21 @@ aiken build # produces plutus.json blueprint
The blueprint at `plutus.json` is consumed by aldabra's escrow builders The blueprint at `plutus.json` is consumed by aldabra's escrow builders
to construct script addresses + spending witnesses. to construct script addresses + spending witnesses.
## Threat model (out-of-scope for v1) ## Threat model — known gaps (out-of-scope for v1)
These are KNOWN gaps the validator does not protect against. They These are KNOWN gaps the validator does not protect against:
inform the WIP designation:
- **Datum CBOR canonicality.** The Deposit redeemer compares - **Datum CBOR canonicality.** The Deposit redeemer compares
`cbor.serialise(expected) == cbor.serialise(new.deposits)`. If the `cbor.serialise(expected) == cbor.serialise(new.deposits)`. If the
Aiken stdlib's CBOR encoder is non-canonical for any input shape Aiken stdlib's CBOR encoder is non-canonical for any input shape
(e.g. map ordering), an attacker could submit a continuing output (e.g. map ordering), an attacker could submit a continuing output
with the same logical content but byte-different and bypass the with the same logical content but byte-different and bypass the
check. We mitigate by using `List<Deposit>` (not Map) which has check. Mitigated by using `List<Deposit>` (not Map) which has
deterministic order, but external review should re-confirm. deterministic order, but external review should re-confirm.
- **Stake credential preservation on refund outputs.** Refund outputs - **Stake credential preservation on refund outputs.** Refund outputs
are derived from contributor PKHs as null-stake base addresses. If a are derived from contributor PKHs as null-stake base addresses. If a
contributor's wallet uses a custom stake credential, refund value contributor's wallet uses a custom stake credential, refund value
bypasses their stake-delegation pool. Acceptable v1 tradeoff; bypasses their stake-delegation pool. Acceptable v1 tradeoff.
documented in spec.
- **Min-utxo per refund leg.** Validator does not enforce min-utxo - **Min-utxo per refund leg.** Validator does not enforce min-utxo
per refund output — assumes the off-chain builder has already per refund output — assumes the off-chain builder has already
ensured each deposit cleared min-utxo at deposit time. A pathological ensured each deposit cleared min-utxo at deposit time. A pathological
@ -62,12 +55,11 @@ inform the WIP designation:
## Status ## Status
- [x] Validator compiles (`aiken build` produces `plutus.json`). - Validator compiles cleanly (`aiken build` produces `plutus.json`).
- [x] Off-chain codecs in `aldabra-dao::agora::escrow`. - Off-chain codecs in `aldabra-dao::agora::escrow`.
- [ ] Off-chain unsigned-tx builders (5 paths). - Off-chain unsigned-tx builders for all 6 paths (open / deposit /
- [ ] MCP tool wrappers. agree / veto / settle / refund-timeout) implemented + unit-tested.
- [ ] Preprod E2E (open → both deposit → agree → settle). - MCP tool wrappers exposed under `escrow_*` prefix.
- [ ] Preprod E2E (open → agree → veto). - Lifecycle paths exercised end-to-end on preprod (settle / veto /
- [ ] Preprod E2E (open → refund-timeout). refund-timeout) — findings in `audits/`.
- [ ] External audit. - **Outstanding:** external third-party audit before mainnet release.
- [ ] Mainnet release gate.

View file

@ -193,12 +193,9 @@ fn hash_to_hex_32(h: &[u8; 32]) -> String {
// Generous overhead for the vkey witness + redeemer ex_units inflation + // Generous overhead for the vkey witness + redeemer ex_units inflation +
// CBOR length-prefix flips between unsigned (def-length) and signed // CBOR length-prefix flips between unsigned (def-length) and signed
// (indef-length) array tags. Original 128 underbid by 144 bytes on a // (indef-length) array tags. 512 is generous head-room for any single-
// 3-input + inline-V2-policy mint (preprod_test2 governor bootstrap // vkey case (~+22k lovelace overestimate worst-case, trivial); reconsider
// 2026-05-08, FeeTooSmallUTxO @ 6353 lovelace short). Bumping to 256 // for multi-sig where many vkey witnesses are added.
// got within 16 bytes on retry — still rejected. 512 is generous head-
// room for any single-vkey case (~+22k lovelace overestimate worst-case,
// trivial); reconsider for multi-sig where many vkey witnesses are added.
const WITNESS_OVERHEAD_BYTES: u64 = 512; const WITNESS_OVERHEAD_BYTES: u64 = 512;
/// Build + sign a Plutus-policy mint with a fully-specified output. /// Build + sign a Plutus-policy mint with a fully-specified output.

View file

@ -596,8 +596,8 @@ pub fn build_unsigned_proposal_create(
// knows to require + emit the corresponding witness. // knows to require + emit the corresponding witness.
// Range width must be ≤ governor.create_proposal_time_range_max_width // Range width must be ≤ governor.create_proposal_time_range_max_width
// (in ms; slot length on every Shelley+ network is 1 second). For // (in ms; slot length on every Shelley+ network is 1 second). For
// Sulkta-shape governors with 30min windows, the legacy 1799-slot // typical 30min governor windows the legacy 1799-slot const fits.
// const fits. For tiny test DAOs (preprod_test: 30s) it must shrink // For governors configured with tighter test windows it must shrink
// to the per-DAO budget. Subtract 1 slot for safety against round-up. // to the per-DAO budget. Subtract 1 slot for safety against round-up.
let max_width_slots = ((args.governor.datum.create_proposal_time_range_max_width / 1_000) let max_width_slots = ((args.governor.datum.create_proposal_time_range_max_width / 1_000)
as u64) as u64)

View file

@ -19,9 +19,9 @@ auditable in isolation.
loading, MCP transport, tool registration, error mapping. The loading, MCP transport, tool registration, error mapping. The
thinnest layer. thinnest layer.
This is the same pattern PetalParse + Cauldron use with their The split is a deliberate auditability + replaceability boundary —
`<service>-core` / `<service>-web` split. Consistent across Sulkta each crate has a single responsibility and the security-sensitive
codebases. one has no I/O dependencies.
## Threat model ## Threat model