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.
- 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.
This commit is contained in:
parent
bc538a71fb
commit
93f11ecef0
10 changed files with 166 additions and 222 deletions
|
|
@ -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.
|
||||
|
|
|
|||
12
Dockerfile
12
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
|
||||
|
|
|
|||
179
README.md
179
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.
|
||||
|
|
|
|||
97
ROADMAP.md
97
ROADMAP.md
|
|
@ -1,97 +0,0 @@
|
|||
# aldabra roadmap
|
||||
|
||||
Phased buildout. Each phase ships a usable increment + leaves the
|
||||
codebase in a state where Phase N+1 picks up cleanly.
|
||||
|
||||
## Phase 1 — MVP read path (current scaffold)
|
||||
|
||||
**Goal:** address + balance + UTXOs from a real mnemonic, working
|
||||
end-to-end through the MCP transport.
|
||||
|
||||
- [x] Cargo workspace
|
||||
- [x] Crate skeletons: `aldabra-core`, `aldabra-chain`, `aldabra-mcp`
|
||||
- [x] Type stubs + ZeroizeOnDrop scaffolding for keys
|
||||
- [ ] `aldabra-core::Mnemonic::into_root_key` — real CIP-3 derivation via `pallas-crypto`
|
||||
- [ ] `aldabra-core::derive_base_address` — real CIP-1852 + bech32
|
||||
- [ ] `aldabra-chain::KoiosClient::get_utxos` — real `reqwest` to `/address_utxos`
|
||||
- [ ] `aldabra-chain::KoiosClient::get_balance`
|
||||
- [ ] Interactive mnemonic bootstrap CLI: paste once, age-encrypt to disk
|
||||
- [ ] On-startup decryption — single passphrase prompt, derived key in
|
||||
RAM only
|
||||
- [ ] Wire MCP server (rmcp) — register `wallet.address`,
|
||||
`wallet.balance`, `wallet.utxos` tools
|
||||
- [ ] Smoke test against testnet (preprod)
|
||||
|
||||
**Done = `claude` can invoke `wallet.address` and get the right
|
||||
preprod address back; `wallet.balance` returns matching numbers from
|
||||
a Koios query.**
|
||||
|
||||
## Phase 2 — write path (send)
|
||||
|
||||
**Goal:** the wallet can spend.
|
||||
|
||||
- [ ] `aldabra-chain::ChainBackend::submit_tx` — POST CBOR to Koios `/submittx`
|
||||
- [ ] `aldabra-chain::tx_status` — poll `/tx_info`
|
||||
- [ ] Build + sign ADA-only payment via `pallas-txbuilder`
|
||||
- [ ] MCP tool `wallet.send` with `to_address`, `lovelace` args
|
||||
- [ ] MCP tool `wallet.tx_status` with `tx_hash` arg
|
||||
- [ ] Add native-asset send (multi-asset value bundle)
|
||||
- [ ] Add `wallet.send.sign_only` for offline / multisig flows
|
||||
- [ ] Hard guard: reject outbound TXs over $X equivalent unless flag set
|
||||
(preventable LLM mistake)
|
||||
|
||||
**Done = the wallet successfully sends 1 tADA on preprod, then 1 ADA
|
||||
on mainnet to a known test address, both initiated via an MCP tool
|
||||
call from Claude Code.**
|
||||
|
||||
## Phase 3 — minting
|
||||
|
||||
**Goal:** wallet can mint Sulkta native assets.
|
||||
|
||||
- [ ] Policy script construction — pure-timelock + multisig variants
|
||||
- [ ] CIP-25 metadata serialization (legacy 721 metadatum)
|
||||
- [ ] CIP-68 ref-NFT pattern (300/100/333 standards)
|
||||
- [ ] MCP tool `wallet.policy.create` — returns `policy_id` + serialized script
|
||||
- [ ] MCP tool `wallet.mint` — args: `policy`, `assets`, `metadata`
|
||||
- [ ] Integration with the MAP treasury minting pattern (2-of-2 multisig)
|
||||
|
||||
**Done = the wallet can mint a test asset on preprod with both CIP-25
|
||||
and CIP-68 metadata, queryable via Koios `/asset_info`.**
|
||||
|
||||
## Phase 4 — Plutus interaction
|
||||
|
||||
**Goal:** consume Plutus-locked UTXOs, attach reference scripts, delegate stake.
|
||||
|
||||
- [ ] Inline datum support
|
||||
- [ ] Reference input attachment
|
||||
- [ ] `wallet.script.spend` — args: `utxo_ref`, `redeemer_cbor`,
|
||||
`script_cbor` or reference, `additional_signers`
|
||||
- [ ] Script execution unit estimation (call out to a local cardano-cli
|
||||
or a reasonable approximation)
|
||||
- [ ] Stake key derivation (chain index 2)
|
||||
- [ ] `wallet.stake.delegate` — args: `pool_id`, optional drep_id (Voltaire era)
|
||||
- [ ] Drep voting tools if Cobb cares (separate ask)
|
||||
|
||||
**Done = the wallet successfully spends a UTXO locked by a trivial
|
||||
Plutus validator (e.g. "always succeeds") on preprod.**
|
||||
|
||||
## Out-of-scope (deliberately)
|
||||
|
||||
- **Hot-key signing for high-value mainnet** — for any tx over a
|
||||
per-config threshold, the wallet should write the unsigned TX to a
|
||||
file and require a separate cold-signing flow (mirrors the
|
||||
ADAMaps treasury pattern at `memory/MEMORY.md` ADAMaps section).
|
||||
- **Smart contract deployment / Plutus compilation** — that's an Aiken
|
||||
/ plutus-tx job. This wallet only consumes pre-compiled scripts.
|
||||
- **Browser / Web UI** — pure MCP-as-the-interface. Humans interact
|
||||
via the LLM client.
|
||||
- **Multiple wallets in one daemon** — instance-per-wallet by design.
|
||||
Run multiple binaries if needed.
|
||||
|
||||
## Performance / size targets (informal)
|
||||
|
||||
- Cold-start time: < 200 ms (mnemonic decrypt + key derive)
|
||||
- Per-tool latency: dominated by chain backend (Koios round-trip
|
||||
~50-200 ms); the wallet itself should add < 10 ms
|
||||
- Binary size: < 30 MB stripped release
|
||||
- Memory: < 50 MB RSS steady-state
|
||||
|
|
@ -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
|
||||
|
||||
`audits/2026-05-09-escrow-spec.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<Deposit>` (not Map) which has
|
||||
check. Mitigated by using `List<Deposit>` (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.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
# Escrow v1 — preprod E2E results, 2026-05-09
|
||||
|
||||
End-to-end execution of the escrow_wip surface on `preprod_test2`,
|
||||
post-internal-audit (validator hash
|
||||
End-to-end execution of the escrow surface on Cardano preprod, post-
|
||||
internal-audit (validator hash
|
||||
`a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d`).
|
||||
|
||||
## Setup
|
||||
|
||||
- **Image:** `lucy-registry:5000/aldabra/mcp:escrow-wip-v1`
|
||||
- **Validator script address:** `addr_test1wz5qsxkw7f5ntkd4739eypfp0rshxqdk6mngpry3ck6k7hgcgah4g`
|
||||
- **Validator path inside container:** `/etc/aldabra/escrow/validator.cbor.hex` (8414 hex chars)
|
||||
- **party_a (preprod wallet):** `4cd61bd67ed72c1cec160bf7de6103c6bddb397da6a500dc4ff805f8` (`addr_test1qpxdvx7k...`)
|
||||
- **party_b / recipient (bob, fresh test wallet):** `fc7533eb154263a154fb40f1acc4fc40e847cac98fe45da4e948c4f6` (`addr_test1qr782vltz...`)
|
||||
- **Validator size:** 8414 hex chars (~4 KB binary)
|
||||
- **party_a:** `4cd61bd67ed72c1cec160bf7de6103c6bddb397da6a500dc4ff805f8` (`addr_test1qpxdvx7k...`)
|
||||
- **party_b / recipient:** `fc7533eb154263a154fb40f1acc4fc40e847cac98fe45da4e948c4f6` (`addr_test1qr782vltz...`)
|
||||
|
||||
## Veto path (4 of 6 builders)
|
||||
|
||||
|
|
@ -47,9 +46,9 @@ Total tADA cycled through validator: 10 ADA (5 from each party). Refunded to ent
|
|||
|
||||
- ✅ Plutus V3 validator deploys at the bech32-derived script address
|
||||
per validator hash + testnet header byte.
|
||||
- ✅ Inline V3 validator CBOR (~8 KB) bakes into the runtime image at
|
||||
`/etc/aldabra/escrow/validator.cbor.hex` and the MCP `validator_script_path`
|
||||
arg correctly resolves it.
|
||||
- ✅ Inline V3 validator CBOR (~8 KB) attaches as the spending script
|
||||
witness; the MCP `validator_script_path` arg correctly resolves it
|
||||
from whatever path the operator deploys the blueprint to.
|
||||
- ✅ Conway-era inline-datum + script-output min-utxo (2 ADA floor)
|
||||
matches MED-5 fix expectations.
|
||||
- ✅ HIGH-1 fix (`value_geq_value(new_value, in_value)` on Deposit)
|
||||
|
|
@ -83,8 +82,8 @@ Total tADA cycled through validator: 10 ADA (5 from each party). Refunded to ent
|
|||
|
||||
## Wallet movement summary
|
||||
|
||||
party_a (preprod): pre-flow ~9844 tADA → post-flow ≈ 9844 - 100 (sent to bob) - ~5 (fees + topups) ≈ ~9739 tADA.
|
||||
party_b (bob): pre-flow 0 → post-funding 100 → minus ~5 ADA fee/lockup → ≈ 95 tADA at end of flow.
|
||||
party_a: pre-flow ~9844 tADA → post-flow ≈ 9844 - 100 (sent to party_b) - ~5 (fees + topups) ≈ ~9739 tADA.
|
||||
party_b: pre-flow 0 → post-funding 100 → minus ~5 ADA fee/lockup → ≈ 95 tADA at end of flow.
|
||||
|
||||
Refunds landed at enterprise (null-stake) addresses, NOT the base
|
||||
addresses of the wallets — this is by validator design (`pkh_to_base_address`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
# Escrow internal audit — 2026-05-09
|
||||
|
||||
Subagent-driven correctness/security audit of the `escrow_wip` surface
|
||||
on `kayos/escrow-wip-v1` before preprod deployment. Two HIGH findings
|
||||
on the validator, several MED on the off-chain builders + MCP layer,
|
||||
LOW polish items.
|
||||
Subagent-driven correctness/security audit of the escrow surface
|
||||
before preprod deployment. Two HIGH findings on the validator,
|
||||
several MED on the off-chain builders + MCP layer, LOW polish items.
|
||||
|
||||
This is **internal review only** — third-party audit still required
|
||||
before any mainnet deployment. Findings here are best-effort by an
|
||||
|
|
@ -176,7 +175,7 @@ side-steps them.
|
|||
## Tests after fixes
|
||||
|
||||
- 36 escrow builder tests pass (added `rejects_no_initial_contributor`).
|
||||
- 132 dao tests pass under `--features escrow_wip`.
|
||||
- 132 dao tests pass.
|
||||
- aldabra-mcp release build clean.
|
||||
|
||||
## Audit lineage
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`<service>-core` / `<service>-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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue