diff --git a/Cargo.toml b/Cargo.toml index 1cf7370..572774c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,10 +59,9 @@ bip39 = { version = "2", features = ["rand"] } ed25519-bip32 = "0.4" cryptoxide = "0.4" -# At-rest encryption for the mnemonic + derived keys on disk. age is -# what the cauldron Fernet pattern would have been if we'd had it back -# then — modern, audited, FOSS, and the secret never has to round-trip -# through a daemon password prompt. +# At-rest encryption for the mnemonic + derived keys on disk. Modern, +# audited, FOSS, and the secret never has to round-trip through a +# daemon password prompt. age = "0.10" # Memory hygiene — wipe key material from RAM when keys go out of scope. diff --git a/Dockerfile b/Dockerfile index 7af8e9e..8082e84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,12 +57,12 @@ RUN apt-get update && \ COPY --from=builder /build/target/release/aldabra /usr/local/bin/aldabra -# Escrow V3 validator CBOR (escrow_wip surface). Baked at /etc/aldabra/ -# escrow/validator.cbor.hex so MCP escrow_*_unsigned tools can pass it -# via `validator_script_path`. The MCP arg-truncation bug at >4500 hex -# chars makes the inline `validator_script_cbor_hex` option unusable -# for the 7902-char compiled validator. Validator hash: -# a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d — check +# Escrow V3 validator CBOR. Baked at /etc/aldabra/escrow/validator.cbor.hex +# so MCP escrow_*_unsigned tools can pass it via `validator_script_path`. +# An MCP arg-truncation bug at >4500 hex chars makes the inline +# `validator_script_cbor_hex` option unusable for the 7902-char compiled +# validator, so file-path mode is the canonical way to wire it. Validator +# hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d — check # 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/plutus.json /etc/aldabra/escrow/plutus.json diff --git a/README.md b/README.md index 1fd3978..61e9794 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,58 @@ # aldabra -Rust-native Cardano lite wallet with an MCP-server interface — built -for LLM-first usage (send, receive, mint, Plutus interaction). +Rust-native Cardano lite wallet with an MCP-server interface. Built +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 -> place, real wallet primitives still landing. See `ROADMAP.md`. +Named for the Aldabra giant tortoise: long-lived, defended, slow but +unstoppable. -## Why +## What it does -The existing Cardano MCP servers are either read-only doc gateways -([Jimmyh-world/Cardano_MCP](https://github.com/Jimmyh-world/Cardano_MCP)) -or built on Blockfrost ([web3-mcp](https://github.com/strangelove-ventures/web3-mcp)) -which is a centralized API we deliberately don't depend on. We want a -wallet that talks directly to Koios + Ogmios endpoints — public, self- -hosted, or whatever the operator points it at. +- **Wallet primitives.** Address derivation (CIP-1852), balance + + UTXO queries, ADA + native-asset transfers, multi-sig partial + signing, encrypted-at-rest mnemonic. +- **Minting.** CIP-25 + CIP-68 native assets, custom timelock / + multisig policies, unsigned-tx flows for cold signing. +- **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 -Three crates in a Cargo workspace: +Cargo workspace with four crates: | 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-chain` | Pluggable backends for chain queries. `ChainBackend` trait, with Koios as the phase-1 implementation. Ogmios + submission paths in phase 2. | -| `aldabra-mcp` | Binary. MCP server speaking stdio. Glues core + chain together, exposes tools to the LLM client. | +| `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 default implementation. | +| `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 │ ────► └──────────┬──────────────────┘ │ - ┌────────┴────────┐ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ aldabra-core │ │ aldabra-chain │ - │ keys, sign │ │ Koios/Ogmios │ - └──────────────┘ └──────────────┘ + ┌────────┼────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────┐ ┌─────────┐ + │ -core │ │-chain│ │ -dao │ + │ 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 ```bash @@ -66,40 +60,101 @@ Phase 4: 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 ```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 -# As an MCP server registered with Claude Code: -# add to ~/.claude.json: +# As an MCP server registered with Claude Code, add to ~/.claude.json: # "aldabra": { # "command": "/path/to/aldabra", # "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 -- **Mnemonic source:** interactive bootstrap on first run, paste once, encrypted at - rest with [age](https://github.com/FiloSottile/age). Never written to disk in - plaintext. +- **Mnemonic source:** interactive bootstrap on first run, paste once + or generate, encrypted at rest with + [age](https://github.com/FiloSottile/age). Never written to disk + in plaintext. - **Derived keys:** in-memory only, `ZeroizeOnDrop` on every container. -- **Network exposure:** stdio MCP transport — never opens a TCP socket. - Only the spawning client process can reach it. -- **Multi-network:** mainnet by default, but `--network preview` / - `--network preprod` for testing without real ADA. +- **Network exposure:** stdio MCP transport — the binary never opens + a TCP listener. Only the spawning client process can talk to it. +- **Multi-network:** safe to point at preprod for development; the + same binary handles mainnet when you flip `ALDABRA_NETWORK`. -## See also +## Status -- `ROADMAP.md` — phased buildout -- `docs/architecture.md` — deeper design notes -- [txpipe/pallas](https://github.com/txpipe/pallas) — the Rust Cardano - building blocks we depend on -- [Emurgo/cardano-serialization-lib](https://github.com/Emurgo/cardano-serialization-lib) — - reference TX builder if pallas-txbuilder doesn't cover something -- [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) — - rmcp, the Rust MCP server SDK we use +Wallet + governance paths exercised on **mainnet**. DAO + escrow +paths exercised end-to-end on **preprod**; the escrow validator has +undergone internal review (`audits/`) but **no third-party audit**. +Treat the escrow flows as use-at-own-risk until external review lands +— see `aiken-escrow/README.md` for the WIP threat model. + +## License + +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. diff --git a/aiken-escrow/README.md b/aiken-escrow/README.md index fafe2a6..76c56d8 100644 --- a/aiken-escrow/README.md +++ b/aiken-escrow/README.md @@ -1,19 +1,14 @@ # aiken-escrow -> ⚠️ **WIP — UNAUDITED.** Preprod testing only. Do **NOT** route mainnet -> funds through this validator. No third-party security review has been -> performed. +> ⚠️ **UNAUDITED.** No third-party security review has been performed. +> Internal review only. Treat as use-at-own-risk for high-value flows +> until external audit lands. Two-party agreement-with-veto escrow validator (Plutus V3, Aiken -v1.1.21). The off-chain (Rust) side lives in `crates/aldabra-dao` behind -the `escrow_wip` feature flag. +v1.1.21). The off-chain (Rust) side lives in `crates/aldabra-dao` and +is wired into the MCP tool surface via `aldabra-mcp`. -## Spec - -`aiken-escrow/README.md` documents the state machine, datum -shape, and redeemer invariants. - -State machine: +## State machine ``` 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 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 -inform the WIP designation: +These are KNOWN gaps the validator does not protect against: - **Datum CBOR canonicality.** The Deposit redeemer compares `cbor.serialise(expected) == cbor.serialise(new.deposits)`. If the Aiken stdlib's CBOR encoder is non-canonical for any input shape (e.g. map ordering), an attacker could submit a continuing output with the same logical content but byte-different and bypass the - check. We mitigate by using `List` (not Map) which has + check. Mitigated by using `List` (not Map) which has deterministic order, but external review should re-confirm. - **Stake credential preservation on refund outputs.** Refund outputs are derived from contributor PKHs as null-stake base addresses. If a contributor's wallet uses a custom stake credential, refund value - bypasses their stake-delegation pool. Acceptable v1 tradeoff; - documented in spec. + bypasses their stake-delegation pool. Acceptable v1 tradeoff. - **Min-utxo per refund leg.** Validator does not enforce min-utxo per refund output — assumes the off-chain builder has already ensured each deposit cleared min-utxo at deposit time. A pathological @@ -62,12 +55,11 @@ inform the WIP designation: ## Status -- [x] Validator compiles (`aiken build` produces `plutus.json`). -- [x] Off-chain codecs in `aldabra-dao::agora::escrow`. -- [ ] Off-chain unsigned-tx builders (5 paths). -- [ ] MCP tool wrappers. -- [ ] Preprod E2E (open → both deposit → agree → settle). -- [ ] Preprod E2E (open → agree → veto). -- [ ] Preprod E2E (open → refund-timeout). -- [ ] External audit. -- [ ] Mainnet release gate. +- Validator compiles cleanly (`aiken build` produces `plutus.json`). +- Off-chain codecs in `aldabra-dao::agora::escrow`. +- Off-chain unsigned-tx builders for all 6 paths (open / deposit / + agree / veto / settle / refund-timeout) implemented + unit-tested. +- MCP tool wrappers exposed under `escrow_*` prefix. +- Lifecycle paths exercised end-to-end on preprod (settle / veto / + refund-timeout) — findings in `audits/`. +- **Outstanding:** external third-party audit before mainnet release. diff --git a/crates/aldabra-core/src/plutus_mint.rs b/crates/aldabra-core/src/plutus_mint.rs index c04444f..c73f93f 100644 --- a/crates/aldabra-core/src/plutus_mint.rs +++ b/crates/aldabra-core/src/plutus_mint.rs @@ -193,12 +193,9 @@ fn hash_to_hex_32(h: &[u8; 32]) -> String { // Generous overhead for the vkey witness + redeemer ex_units inflation + // CBOR length-prefix flips between unsigned (def-length) and signed -// (indef-length) array tags. Original 128 underbid by 144 bytes on a -// 3-input + inline-V2-policy mint (preprod_test2 governor bootstrap -// 2026-05-08, FeeTooSmallUTxO @ 6353 lovelace short). Bumping to 256 -// 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. +// (indef-length) array tags. 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; /// Build + sign a Plutus-policy mint with a fully-specified output. diff --git a/crates/aldabra-dao/src/builder/proposal_create.rs b/crates/aldabra-dao/src/builder/proposal_create.rs index 4cfa2ae..0edffa7 100644 --- a/crates/aldabra-dao/src/builder/proposal_create.rs +++ b/crates/aldabra-dao/src/builder/proposal_create.rs @@ -596,8 +596,8 @@ pub fn build_unsigned_proposal_create( // knows to require + emit the corresponding witness. // Range width must be ≤ governor.create_proposal_time_range_max_width // (in ms; slot length on every Shelley+ network is 1 second). For - // Sulkta-shape governors with 30min windows, the legacy 1799-slot - // const fits. For tiny test DAOs (preprod_test: 30s) it must shrink + // typical 30min governor windows the legacy 1799-slot const fits. + // For governors configured with tighter test windows it must shrink // 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) as u64) diff --git a/docs/architecture.md b/docs/architecture.md index fc297e3..5809b6d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,9 +19,9 @@ auditable in isolation. loading, MCP transport, tool registration, error mapping. The thinnest layer. -This is the same pattern PetalParse + Cauldron use with their -`-core` / `-web` split. Consistent across Sulkta -codebases. +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