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.
5.9 KiB
Escrow v1 — preprod E2E results, 2026-05-09
End-to-end execution of the escrow surface on Cardano preprod, post-
internal-audit (validator hash
a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d).
Setup
- Validator script address:
addr_test1wz5qsxkw7f5ntkd4739eypfp0rshxqdk6mngpry3ck6k7hgcgah4g - 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)
| Step | tx hash | What it proves |
|---|---|---|
| Open | 0972963e8eb46597b37dfa7e1fa15c97fea4e39d55b29c54f723ba1b155bc7cb |
escrow_open builder; deposits=[(preprod, 5 ADA)] at script |
| Deposit (bob) | 0519cb7dbb8c2c1632db5baee90d5440d2905c151f910a085c78d9050c2d6175 |
first Plutus V3 spend on chain. Validator's Deposit branch ran — including the new HIGH-1 value_geq_value(new_value, in_value) check. Continuing-output deposits-list update verified via cbor.serialise byte-equality. |
| Agree | f6664079d96c453e1a6333c33c65744984953c474e57cdb331e78ad3e5429cc7 |
both-party multi-sig (driver=preprod, co-signer=bob). State Open→Agreed{at=upper}. validity_upper ≤ open_deadline_ms enforced. |
| Veto | 14a4be9f233ab02e518356e5963d30517986984f6fd3520151c924057db9c661 |
HIGH-2 fix active: value_eq(deposits_to_value(d.deposits), in_value). Multi-output refund (5 ADA each to enterprise addresses of party_a + party_b). |
Total tADA cycled through validator: 10 ADA (5 from each party). Refunded to enterprise addresses of both contributors.
Refund-timeout path (5th builder)
| Step | tx hash | What it proves |
|---|---|---|
| Open (short deadline) | 0669ef61b8695bedcb4eb4c38fda2bd66c68c9384b57839329a01a745db75305 |
escrow_open with open_deadline = tip_ms + 5 min, initial_lovelace=2 ADA |
| (wait) | — | tip elapsed past open_deadline (~165s past) before next step |
| Refund-timeout | 41590ac6ed069586e650da58858436cfe6be51a865069a7a4b40f795dfcdbff9 |
escrow_refund_timeout builder; lower > open_deadline_ms strict-> time gate proven; HIGH-2 deposits-sum invariant proven on a single-deposit escrow; single-output refund (2 ADA back to party_a's enterprise address). |
Settle path (6th builder)
| Step | tx hash | What it proves |
|---|---|---|
| Open | a878900c09022381f332ca2cea1b4624202ebdbd6f3a83fd9de07475bb98bd6b |
escrow_open with deadline=tip+1h, lock=30min, recipient=bob |
| Deposit (bob) | ef8910101e88b63abb28ec9b511616e3465075b8d34d5eeb9703efe1876a62bf |
bob adds 5 ADA, deposits=[(preprod,5),(bob,5)], second time the V3 validator's Deposit branch ran on chain |
| Agree | bbfd57c3acb68ddb76d6b92c0dbe8ba9cb21ca88ad6370d19f00822c3b69d655 |
both-party multi-sig (driver=preprod, co-signer=bob); state Open→Agreed{at=1778381375000} |
| (wait) | — | tip elapsed past agreed_at + lock_period_ms (~1778383175000) — actual tip at settle: 1778383733 = 558s past |
| Settle | 4b52312ce264dba74a6fde6c2ccb597696022c8919470f23670e2746db10d1ff |
escrow_settle builder; lower > agreed_at + lock_period strict-> time gate proven; MED-2/3 fix held a second time under different timing; recipient (bob) gets 10 ADA at his enterprise address; no party signer required (preprod drove as fee-payer only) |
All 6 builders proven on chain.
What this E2E proved on chain
- ✅ Plutus V3 validator deploys at the bech32-derived script address per validator hash + testnet header byte.
- ✅ Inline V3 validator CBOR (~8 KB) attaches as the spending script
witness; the MCP
validator_script_patharg 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) ran on chain — validator accepted a clean deposit, no token drain attempt was made (v1 ADA-only). - ✅ HIGH-2 fix (
value_eq(deposits_to_value(d.deposits), in_value)on Veto and Refund) ran on chain — the new helper functionsdeposits_to_valueandvalue_eqexecute within the per-tx UPLC budget for a 1-deposit and a 2-deposit escrow. - ✅ Multi-party signing flow works end-to-end: driver signs partial → co-signer signs partial → submit. wallet_sign_partial appends a fresh VKeyWitness without disturbing existing ones.
- ✅ Slot↔ms math at the strict-
>validator boundary (Refund branch) didn't burn fees — the MED-2/3 fix to derivevalidity_lower_msfromslot_to_posix_ms(network, slot)instead of Koiosblock_time*1000held up under a real preprod chain race.
v2 / future TODO (still open)
- Settle path on-chain validation.
- External third-party audit (TxPipe / Anastasia Labs / MLabs / Tweag).
- Reference-script deployment for the validator (~8 KB inline per spend tx is non-trivial; ref-script optimization halves tx size).
aiken/fuzzproperty tests inescrow.akcovering each redeemer branch's invariant violations.- Multi-asset escrows (the v2 frontier — needs the foldr-canonicality
golden test for
Pairs<>serialisation, plus widening the spend builders to takeescrow_in_assetsand emit non-empty asset entries on every output). - Read-side MCP tools (
escrow_show,escrow_list).
Wallet movement summary
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
hard-codes stake_credential: None). The funds are spendable via the
same payment key; just a different bech32 prefix.