Drops the ~60 ticket-prefix comments (CRIT-N, HIGH-N, MED-N, LOW-N,
L-N, M-N, AUDIT-N, PLUTUS-N, "audit fix (date):", "Phase N" labels,
"Adversarial-review fix:") that had accumulated in inline + doc
comments over several audit cycles. Where the surrounding prose
still carried useful WHY context it gets kept and tightened; where
the ticket WAS the comment it gets dropped entirely.
No logic, no renames, no behavior change. Audit history lives in
commit messages and the audits/ tree where it belongs — eternal
comments don't need to mirror it.
Net 138 LOC shorter. 253 tests pass, no new clippy or fmt warnings.
Wide sweep across the codebase to remove leftover artifacts of internal
development sessions, internal entity naming, and audit-code references
that point at non-public docs. The technical reasoning for each piece
of code stays; the "Caught 2026-05-XX while debugging XYZ at preprod"
narrative goes.
Categories scrubbed:
- Dated session-log comments ("Caught/Surfaced/Discovered 2026-05-XX")
→ rewritten as neutral technical reasoning.
- Internal audit codes (AUDIT-H2, AUDIT-C2, AUDIT-M2, AUDIT-H5, etc.)
referencing a non-public audit doc → labels stripped, fix reasoning
kept.
- Internal-entity names in code comments (Sulkta-specific, Sulkta runs
X, Terrapin/TRP as gov-token names) → generic phrasing.
- Test fixture helper `sulkta_cfg` → `test_dao_cfg`; test DAO name
string `"sulkta"` → `"test-dao"`. On-chain addresses in test fixtures
kept (they're real-world wire-byte test data on public chain).
- Cross-references to memory files / non-public audit docs
(`audit-sulkta-agora-2026-05-05.md`, `audits/2026-05-09-escrow-spec.md`)
→ reasoning inlined or removed.
- Test names renamed: `decodes_sulkta_live_governor_datum` →
`decodes_live_governor_datum`, `decodes_sulkta_live_proposal_zero` →
`decodes_live_finished_proposal`, etc.
Kept (legitimate):
- Cross-references to in-repo audit docs (audits/2026-05-09-escrow-
internal-audit.md, audits/2026-05-09-escrow-e2e.md) — they ARE the
public artifacts being referenced.
- HIGH-1/HIGH-2/MED-2/LOW labels on escrow fixes — these correspond to
findings in the in-repo audit doc.
- TODO markers — legitimate work-still-to-do.
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.
Surfaced by Track #38 code audit (2026-05-09):
1. cargo fmt --all: 217 formatting diffs across 35 files. Pure
whitespace; no semantic changes.
2. cargo clippy --fix: 30 warnings -> 10. Auto-applied:
- useless format!() (3 sites in builder/proposal_*.rs)
- needless_borrow_for_generic_args (4 sites)
- cloned_ref_to_slice_refs (1 site, builder/proposal_cosign.rs)
- derivable_impls (1 site, dao/config.rs)
- unused imports/variables (3 sites)
Remaining 10 warnings are non-trivial (too_many_arguments on a
constructor at 8 args, FromStr trait shadow, doc_lazy_continuation
on a few comment blocks). Filed as tech-debt; no action this pass.
3. cargo audit: 0 vulnerabilities. 2 unmaintained advisories on
transitive deps:
- paste 1.0.15 (RUSTSEC-2024-0436) via rmcp + pallas-traverse
- proc-macro-error 1.0.4 (RUSTSEC-2024-0370) via age->i18n-embed-fl
Both upstream; tracked but no action needed locally.
4. Test failure surfaced: builder::proposal_retract_votes::tests::
voting_ready_in_window_subtracts_vote_weight failed — cooldown
check was applied unconditionally for RemoveVoterLockOnly mode,
blocking the legitimate 'retract during voting window' path
where the proposal datum mutates (vote weight subtraction). Per
Agora's premoveLocks rule, cooldown only applies when retracting
AFTER voting closed but BEFORE Finished — not during the active
voting window. Fixed by gating cooldown on
'!proposal_datum_will_change' so the in-window retract path
bypasses cooldown the same way RemoveAllLocks does.
Test: 87/87 aldabra-dao lib tests pass post-fix (was 86/87).
256 still underbid by ~16 bytes (721 lovelace) on the same
preprod_test2 governor mint shape that 128 missed by 144 bytes.
The actual CBOR delta between the unsigned tx (def-length witness
set arrays, no vkey witness, no redeemer expansion) and the signed
tx (indef-length flips, vkey witness, finalized redeemer) is
~270 bytes for this shape, not the 144 the first FeeTooSmallUTxO
suggested.
512 gives plenty of headroom — worst-case ~22k lovelace overestimate
which is trivial. For multi-sig flows with N vkey witnesses, this
needs revisiting; plutus_mint's only signer today is the wallet's
own payment key so a single-vkey budget is correct.
128 underbid by ~144 bytes on a 3-input plutus_mint with inline V2
policy CBOR (preprod_test2 governor bootstrap 2026-05-08, hit
FeeTooSmallUTxO 6353 lovelace short). The original constant covered
the vkey witness alone but missed redeemer ex_units final-pass
inflation + CBOR length-prefix shifts between unsigned (def-length)
and signed (often indef-length) witness-set arrays.
256 is plenty for any single-vkey case and still cheap fee-wise
(56-lovelace difference at the Cardano per-byte rate). For multi-sig
flows we'd revisit, but plutus_mint's only signer is the wallet's
own payment key.
On Babbage+, Plutus's TxInfo.signatories is populated only from the
tx body's required_signers field — vkey witnesses alone don't surface
in the script context. Without this, any Agora script that calls
pauthorizedBy/txSignedBy fails. Stake-bootstrap on preprod erred
because the owner pkh wasn't in signatories despite the wallet
signing the tx.
MCP layer always passes [wallet_pkh]; callers can append cosigner
pkhs for multi-sig flows.
Without language_view, pallas does not compute script_data_hash on
the tx body. Plutus txs without script_data_hash get rejected with
ConwayUtxowFailure (PPViewHashesDontMatch SNothing (SJust ...)).
Caught 2026-05-07 attempting governor bootstrap on preprod against
Agora's V2 GST policy. Previous code only set language_view when
the policy was V3 — every V2 mint hit the chain rejection.
Three changes:
1. crates/aldabra-core/src/plutus_cost_models.rs — append
PLUTUS_V2_COST_MODEL_PREPROD constant (175 i64 entries), pulled
live from preprod Koios epoch_params 2026-05-07. Same protocol-
version convention as the existing V3 constant: V2 cost model
is identical mainnet vs preprod (cost models are protocol-version
parameters, not network), so the _PREPROD suffix is naming
convention, not a separation point.
2. crates/aldabra-core/src/plutus_mint.rs — replace the V3-only
language_view block with a per-PlutusVersion match. V2 wires
the new constant; V3 keeps the existing
params.plutus_v3_cost_model path; V1 left as TODO with a note
(no V1 mint use case yet).
3. crates/aldabra-dao/examples/dump_governor.rs — small cargo
example that encodes a sample GovernorDatum to CBOR hex via
the existing aldabra_dao::agora::GovernorDatum::to_plutus_data
path. Used during preprod DAO bringup to construct the inline
datum for the governor bootstrap tx. Edit values + re-run for
any DAO bringup. Builds against the existing pallas-codec
dev-dependency.
Adds aldabra-core::plutus_mint with build_signed_plutus_mint and
build_unsigned_plutus_mint. Designed for Agora-style DAO bringup
where every "mint a single ST token under a Plutus policy and
deposit it at a script address with inline datum" tx shares the
same shape (governor / stake / proposal bootstrap).
PlutusMintArgs takes:
- required_inputs: UTxOs that MUST be spent (e.g. gstOutRef the
GST policy is parameterized on)
- policy_cbor + policy_version + redeemer + ex_units
- mint_assets: list of (asset_name_hex, qty) under this policy
- dest_address + dest_lovelace + dest_extra_assets to forward
+ optional inline datum
Same collateral/funding pattern as plutus.rs::build_signed_plutus_spend
(smallest ADA-only ≥ 5 ADA for collateral, separate funding picks
to cover dest + fee + min_change). PlutusV3 cost-model wired into
language_view per the existing PLUTUS-4 fix.
3 unit tests cover empty-policy / empty-mint / required-input-not-
in-available rejections + the governor-bootstrap shape produces
valid Conway CBOR.
This is Phase 2 of the Track B-fast preprod DAO bringup. Together
with the kayos/wallet-ref-script Phase 1 commits (b9124ee + a65ab78)
it gives aldabra everything needed to deploy 11 Agora script ref
UTxOs + bootstrap a governor + bootstrap stakes on preprod.
Adds Babbage/Conway-era reference-script attachment to wallet_send
and wallet_send_unsigned. The output can now carry any combination
of {native assets, inline datum, reference script}.
Why: deploying Plutus validators / minting policies as on-chain
reference UTxOs is the standard Cardano dApp pattern (Agora, Liqwid,
SundaeSwap all do this). Without it every spend or mint that uses
a script has to inline-witness the full CBOR — kilobytes per tx and
quadratic with tx size. Reference scripts let downstream txs witness
via `read_only_input` for ~32 bytes overhead.
API surface:
aldabra-core:
- new `ReferenceScriptSpec<'a> { kind: ScriptKind, cbor: &'a [u8] }`
- `ScriptKind` re-exported from pallas_txbuilder (PlutusV1/V2/V3/Native)
- new `build_signed_payment_extras(...)` and
`build_unsigned_payment_extras(...)` — supersets of the existing
`_with_assets` functions; take both `to_inline_datum_cbor` and
`to_reference_script` Options
- existing `_with_assets` functions kept as thin wrappers that pass
None for ref script — backwards compatible
- internal `output_with_assets`, `prepare_payment`, and
`build_staging_with_fee` thread the new ref-script Option through
aldabra-mcp:
- `SendArgs` and `UnsignedSendArgs` gain
`reference_script_cbor_hex: Option<String>` and
`reference_script_kind: Option<String>`
- Both must be set or both omitted; mismatched returns a clean error
- `parse_script_kind` helper — case-insensitive, accepts
PlutusV1/V2/V3/Native (plus shortcut V1/V2/V3)
Reference scripts are intentionally never attached to change outputs.
The change goes back to the wallet's own address, where a script
attachment would lock value into a publicized script that we'd then
have to spend BACK out — pointless. Ref-script attachment is only
on the recipient (`to`) output.
This unblocks Track B-fast step 3 of the preprod DAO bringup —
deploying the 11 Agora script bytecodes (governor / stakes /
proposal / treasury / mutate / noOp / treasuryWithdrawal validators
+ GST / StakeST / ProposalST / GAT minting policies) as reference
UTxOs against Kayos's preprod wallet.
No new tests in this commit — Phase 2 (Plutus-policy mint with
custom output) lands in a follow-up that will exercise this path
end-to-end against a real chain submit on preprod.
Lands the high-priority fixes from the 2026-05-06 audit before any
mainnet submit of the new vote/cosign/advance/destroy txs.
## H-1: Locked→Finished gate (MCP tool)
`dao_proposal_advance_unsigned` now refuses LockedToFinished unless
`tx_lower_ms > executing_end`. During the executing period the
validator demands gstMoved=true (governor input present); builder
doesn't include the governor input, so a tx in that window would
fee-burn. The proper Locked→Finished + GAT-mint flow is Phase 4c-bis;
this gate keeps us out of the broken middle.
## H-2 + H-4: strict-boundary + tx-upper-inside-period (MCP tool)
Validator's pgetRelation is strict on PAfter (`period_end < lb`)
and demands `ub <= period_end` on PWithin. Tool now picks PWithin
only when `tx_lower_ms >= period_start && tx_upper_ms <= period_end`,
PAfter only when `tx_lower_ms > period_end` strictly, and explicit-
errors on the boundary-straddling case (when tx validity range
crosses out of the target period). Same logic mirrored for the
VotingReady→Locked + VotingReady→Finished branches.
## H-3: vote builder lower-bound preflight (MCP tool)
`dao_proposal_vote_unsigned` previously checked only validity_upper
vs voting_end_ms. Validator demands BOTH `voting_start <= lb` AND
`ub <= voting_end`. Vote-too-early would hit "too early or invalid"
script error. New preflight on tx_lower_ms vs voting_start.
## M-2: DRep deposit pulled from ProtocolParams
Hardcoded constant (500 ADA) was wrong if the protocol changes
drep_deposit OR if the DRep was originally registered at a different
deposit amount (deregistration must match registration). Added
`drep_deposit_lovelace: u64` to ProtocolParams (default 500 ADA),
governance.rs build_signed_drep_registration / deregistration now
read from params instead of the constant. Constant kept for
backward compat with a doc note pointing at the params field.
## Pallas fork bump 507fd9da → 8091abd1
M-4 from the audit landed on the fork: voting_procedures builder
debug_assert_ne!s against empty CBOR map (0xa0) and docs the
upstream NonEmptyKeyValuePairs::decode footgun.
L-1 from the audit was a false finding — the audit subagent
misread the constants. PROPOSAL_CREATE_*_EX_UNITS are already at
the post-2026-05-05-H-2 values (5M mem / 2G steps per spend, 2M / 1G
per mint). The new builders alias these correctly. No change needed.
Phase 6, key-credentialed slice (script-DRep bridge for the DAO is the
remaining sub-arc).
## pallas-fork patch (Sulkta-Coop/pallas feat-aux-data HEAD 507fd9da)
Threads voting_procedures through StagingTransaction → conway::
build_conway_raw, mirroring the auxiliary_data + certificates patches.
- pallas-txbuilder/src/transaction/model.rs: voting_procedures field +
builder methods .voting_procedures() / .clear_voting_procedures()
- pallas-txbuilder/src/conway.rs: VotingProcedures::decode_fragment on
the way out, assigned to TransactionBody.voting_procedures
- BRANCH-NOTES.md: section 3 added documenting the new patch
- 2 new tests (round-trip + negative path) on the txbuilder side
aldabra Cargo.lock SHAs bumped to the new HEAD.
## aldabra-core/src/governance.rs
- VoteChoice enum (Yes/No/Abstain) with into_pallas() conversion
- build_signed_drep_vote_cast — assembles VotingProcedures CBOR
(NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<GovActionId,
VotingProcedure>>) with this wallet's stake credential as a
Voter::DRepKey, attaches via the new pallas API, dual-witness signs.
- Optional CIP-100 anchor on the vote.
## aldabra-mcp/src/tools.rs
- wallet_drep_vote_cast tool: gov_action_tx_hash + gov_action_index +
vote (yes/no/abstain) + optional anchor.
What's still scope-of-Phase-6:
- Script-credentialed DRep voting (the DAO governor as DRep, with
redeemer-driven authorization). Needs a different signing path
since the voter is a script credential, not a key credential.
Separate builder; defer until Sulkta wants to actually bridge.
Conway-era governance MCP tools, key-credentialed (script credentials
deferred to Phase 6).
aldabra-core/src/governance.rs (new ~500 LOC):
- DRepTarget enum + parse_drep_target (handles bech32 drep1.../
drep_script1... + named 'abstain' / 'no_confidence')
- build_signed_vote_delegation — Certificate::VoteDeleg(stake_cred,
drep), reuses the dual-witness 2-pass-fee pattern from stake.rs.
Optional register_first prepends StakeRegistration.
- build_signed_drep_registration — Certificate::RegDRepCert with
optional CIP-100/119 anchor + 500 ADA deposit
- build_signed_drep_deregistration — Certificate::UnRegDRepCert with
refund-aware change calc (deposit returns to wallet)
- DREP_REGISTRATION_DEPOSIT_LOVELACE constant (500 ADA, mainnet)
Made stake_key_as_payment_proxy pub(crate) so governance.rs can reuse
the stake-key-as-witness trick.
aldabra-mcp/src/tools.rs:
- wallet_vote_delegate (drep + register_first)
- wallet_drep_register (optional anchor_url + anchor_data_hash_hex)
- wallet_drep_deregister (no args)
3 unit tests on parse_drep_target + DRepTarget→DRep round-trip.
Phase 6 (vote_cast for DReps voting on Conway gov actions) blocked
on extending Sulkta-Coop/pallas-txbuilder to thread voting_procedures
through StagingTransaction (currently TODO at conway.rs:254). Same
pattern as the aux_data + certificates patches already in the fork.
Estimated ~300-500 LOC fork patch + ~400 LOC vote-cast builder. Surface
to Cobb before starting.
wallet_send + wallet_send_unsigned now accept an optional
datum_inline_cbor_hex field. When set, the recipient output
carries the bytes as an inline datum — the right shape for
locking funds at a script address with a datum the validator
can read.
Without this, sends to script addresses created un-spendable
utxos (Babbage/Conway rejects spending script utxos that
don't carry a datum). Surfaced 2026-05-04 audit-4 phase F2
when the always-succeeds Aiken validator's locked utxo
couldn't be spent back due to NotAllowedSupplementalDatums +
PPViewHashesDontMatch chain errors.
Plumbed through:
build_signed_payment_with_assets (added arg)
build_unsigned_payment_with_assets (added arg)
prepare_payment (added arg)
build_staging_with_fee (added arg)
output_with_assets (added arg)
SendArgs / UnsignedSendArgs (new optional MCP field)
Change outputs never get a datum — they go back to the wallet
which has no validator to satisfy, so the field is wired only
to the recipient output.
Test lock_with_inline_datum_attaches_datum_to_output decodes
the resulting tx CBOR and confirms the recipient output's
datum_option is populated.
Unblocks mainnet Plutus testing — the spend round trip can
now build a lock that the spend side can satisfy.
Adds RootKey::from_root_xsk_bech32() / from_xprv_bytes() /
to_root_xsk_bech32() so RootKey can ingest + emit the same
bech32 root extended secret key shape that cardano-cli +
cardano-address + the IOG node priv/wallet/<name>/root.prv
file already use. HRP is strictly root_xsk — refuses
acct_xsk/addr_xsk to keep the import scoped to the actual
HD root.
New CLI flag --bootstrap-from-xprv runs an interactive
import: paste root_xsk1... bech32, prompt passphrase,
encrypt, persist as root-xprv.age (parallel to mnemonic.age).
Refuses to overwrite either existing key file (per Cobb's
no-delete-crypto-keys rule — caller has to move aside, not
delete).
Startup path now checks for either mnemonic.age OR
root-xprv.age; refuses if both exist (ambiguous). Same
RootKey downstream — derivation tree, signing, all of it
works identically whether the key came in via mnemonic
or xprv import.
Test root_xprv_round_trip proves the imported xprv derives
to the same address as the mnemonic-imported equivalent.
Two coupled fixes for the same root cause: the coin selector was too
conservative for "send most of what I have" cases.
1. min_change_required now drops to 0 for ada-only sends (kept at
min_utxo_lovelace for asset-bearing sends where change has to
carry leftover policy IDs). Downstream pass2 already folds
sub-min change into fee on the ada-only happy path; the selector
was reserving slack the chain doesn't actually need.
2. fee_pass1 dropped from 500_000 to 200_000. Real fees:
1-in 1-out ada-only send : ~166 k
1-in 2-out (with change) : ~178 k
CIP-25 mint w/ metadata : ~210 k
500_000 was overgenerous safety budget. 200_000 is enough headroom
for the basic-send case (which is the one that needed to drain to
fee) without crowding mint paths (which typically have plenty of
lovelace headroom anyway).
Surfaced 2026-05-05 zeroing out the mainnet test wallet:
2 ADA balance, 1.8 ADA send refused upstream as
"need 3300000 (target+fee+min_change), have 2000000"
even though the chain math was fine. New regression
ada_only_send_can_drain_to_fee covers the case.
max_send_lovelace default is now network-aware: mainnet 10 ADA,
preprod/preview 100 (t)ADA. Mainnet handles real value, so the cap
should bite earlier — anything > 10 ADA needs explicit force=true.
Test ada on preprod/preview is faucet-replaceable, no need to
sand off the test surface. New regression
mainnet_default_max_send_is_tighter locks the rule in.
PLUTUS_V3_COST_MODEL_PREPROD docstring updated: confirmed identical
to mainnet PV3 cost model (preprod epoch 286 = mainnet epoch 629,
both 297 params, byte-identical). Cost models are protocol-version
parameters, not network parameters; using the same constant on
both is correct. Re-snapshot from mainnet Koios after any major
hard fork. Naming kept as _PREPROD for git churn reasons.
build_signed_plutus_spend was picking the LARGEST ada-only utxo
for collateral and the next-largest for funding. Wallets with
one big change utxo + a small leftover (the typical shape after
any send) hit this with funding=tiny, collateral=huge —
funding+locked couldn't cover payout + script-execution fee +
change min_utxo even with billions of lovelace sitting unused
in collateral.
Fix: pick the SMALLEST ada-only utxo that still qualifies (≥5 ADA)
for collateral, and the LARGEST for funding. Collateral never
gets consumed on the happy path, so its size beyond the 5-ADA
floor is wasted budget; funding has to cover real spend.
Surfaced 2026-05-04 audit-4 phase F2 on the deployed Lucy
container against the always-succeeds Aiken validator.
New regression test picks_smallest_qualifying_collateral_largest_funding
covers the mixed-size-utxo scenario the prior tests missed
(both old utxos were 50-100M ada, so the inversion didn't show).
prep for deployment. cargo clippy --workspace --all-targets now passes
clean. cargo audit unchanged (same 2 unmaintained-warning macro-support
transitives; no cves).
cleanup applied:
- ProtocolParams construction in tools.rs uses struct-update syntax
(clippy::field_reassign_with_default).
- main.rs collapsed two else-if branches with identical bodies
(clippy::if_same_then_else).
- mint/plutus/stake sort_by(|a,b| b.cmp(&a)) → sort_by_key(Reverse(_))
(clippy::manual_sort_by). 4 sites.
- metadata/mint/tx odd-length hex check uses .is_multiple_of(2)
(clippy::manual_is_multiple_of). 3 sites.
- stake.rs witness_overhead conditional removed — both branches
produced TWO_WITNESS_OVERHEAD_BYTES (left over from when
registration was thought to add a third witness; it doesn't).
WITNESS_OVERHEAD_BYTES const removed (only the two-witness one
is used).
- Public spend/mint/stake build_signed_*_with_assets fns get
#[allow(clippy::too_many_arguments)] — they ARE the API surface.
- ex_units_default_is_generous test gets explicit allow for the
tautological-on-const assertion (kept the intent comment).
97 unit tests still pass. release build clean.
PLUTUS-1 (HIGH) — value-not-conserved on happy path. collateral isn't
consumed unless script fails, so total_in counted lovelace that wasn't
actually available for outputs. now picks a SEPARATE ada-only funding
utxo as a regular input alongside the locked utxo; collateral stays
collateral. error message tells callers to "split a UTXO first or top
up" if a second ada-only utxo isn't available.
PLUTUS-2 (HIGH) — collateral containing native assets. chain forbids
that; our picker grabbed largest-overall. now filters available_utxos
to assets.is_empty() before picking, errors clearly if no ada-only
utxo ≥ 5 ADA exists.
PLUTUS-3 (HIGH) — fee underestimation. plutus tx fees are
size_fee + exunits_fee. only size_fee was being charged. new
ProtocolParams::ex_units_fee() does ceil(mem * priceMem) +
ceil(steps * priceStep). conway-era prices in defaults
(577/10000 mem, 721/10_000_000 steps). fee jumps from ~0.17 ADA →
~1.7 ADA for the default ExUnits budget — matches what chain demanded.
PLUTUS-4 (LOW, becomes blocking under the others) — script_data_hash
not computed. pallas-txbuilder only computes the body hash field when
language_view is set on staging. plutus v3 path now calls
.language_view(version, cost_model) when the caller-supplied
ProtocolParams::plutus_v3_cost_model is Some. mcp wallet_script_spend
populates with the canonical preprod V3 cost model from
plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD (297 i64 params,
fetched from koios epoch_params 2026-05). when ProtocolParams has no
cost model, we skip language_view and the chain rejects with
PPViewHashesDontMatch — explicit-failure mode, no silent shipping
of broken txs.
new tests:
- ex_units_fee_matches_known_values: 14M mem * 0.0577 + 10B steps *
7.21e-5 ≈ 1.529 ADA ± ceil-rounding. locks the conway price math.
- rejects_when_no_funding_input_separate_from_collateral: catches
the PLUTUS-1 single-utxo case.
- rejects_when_collateral_candidate_has_assets: PLUTUS-2 ada-only.
verified on preprod against a real script-locked utxo (the placeholder
script we locked 5 tADA at earlier). chain rejection went from 5
distinct errors to 1 (MalformedScriptWitnesses — expected, our
placeholder UPLC isn't valid). structural body shape now passes
every chain-rule check; only the script bytecode itself fails to
compile, which is a test-env limitation (no aiken in our toolchain
yet) not a wallet-code limitation.
97 unit tests pass. ProtocolParams gained 5 new fields + ex_units_fee
helper; went from Copy to Clone (cost_model is a Vec).
HIGH:
- HIGH-1 enforce_value_cap helper applied to wallet.send,
wallet.mint, wallet.mint.cip68_nft, wallet.script.spend. each
gained a `force` arg; cap also covers the user_lovelace+ref_lovelace
sum on cip68_nft. wallet.stake.delegate skipped (2 ada deposit is
protocol-fixed, not a transfer to a non-wallet destination).
- HIGH-2 wallet.tx_summary mcp tool — read-only decode of a conway
tx cbor → typed TxSummary (inputs, outputs+assets, fee, certs,
mint, witness count, aux-data presence). new aldabra-core::inspect
module. callers MUST run this before wallet.sign_partial /
wallet.submit_signed_tx on any cbor they didn't build themselves.
MEDIUM:
- M-1 zeroize stack-resident extended_bytes after SecretKeyExtended
consumes them. tx.rs::payment_key_to_private + sign.rs::add_witness.
- M-2 atomic 0o600 mnemonic file create via OpenOptions+
OpenOptionsExt. removes the prior toctou window between fs::write
(default umask) and chmod 600.
- M-3 prompt_or_env_passphrase + unlock_passphrase helpers wrap the
passphrase in Zeroizing<String>. ALDABRA_PASSPHRASE env still
unzeroizable in the env block itself (documented headless tradeoff).
- M-4 is_hex_64 validator on submit_tx response — koios error wrapped
in quotes can no longer round-trip as a fake tx_hash.
LOW + cleanup:
- L-1 checked_add for inner sums of checked_sub patterns in tx.rs.
remaining sites (mint.rs, stake.rs, plutus.rs) deferred — same
pattern, can't overflow with realistic cardano amounts but
defensive. picked up next.
- L-2 root key scoped to a block in main.rs — XPrv drops + wipes
after deriving payment_key + stake_key + address. saves ~96 bytes
of secret material lifetime.
- L-3 TxStatus gained a Pending variant for the mempool-but-not-yet-
confirmed case. previously rendered as Confirmed{block_height: None}
which was misleading.
- L-4 .expect("we built this key") → typed ? propagation in
tx.rs::prepare_payment.
- L-5 removed dead fns (build_and_sign, decode_hex) + unused imports.
WALLET GENERATION (audit prompted gap-find):
aldabra had only an import path. no "generate fresh wallet" tool.
- Mnemonic::generate() — bip39::Mnemonic::generate_in(English, 24)
with the rand feature. returns (Mnemonic, Zeroizing<String>) so
the caller can display the phrase once for cold backup.
- aldabra --generate-mnemonic — print fresh phrase, exit. no disk.
- aldabra --bootstrap-new — generate + display + encrypt one-shot.
- bip39 dep gains the rand feature for OsRng-backed generation.
- standard 24-word BIP-39, recoverable from any cardano wallet.
mcp tools: 16 → 17 (added wallet.tx_summary).
unit tests: 88 → 93. cargo audit clean (0 cves), cargo build clean
(0 warnings). all four cli flags smoke-tested:
--generate-mnemonic prints + exits; --bootstrap-new generates +
encrypts + derives a real preprod address; mnemonic.age has 0o600
perms confirmed atomic.
audit doc memory/spec-aldabra-audit-2026-05-04.md updated with
status markers.
stake key + reward address (4.5):
- StakeKey::stake_address(network) — bech32 (`stake1...` mainnet,
`stake_test1...` testnet) via pallas_addresses::StakeAddress::new
(added to the fork in the same commit since the upstream tuple
struct had no public constructor).
- StakeKey::xprv() — crate-internal accessor for signing.
- WalletInner now holds the stake_key alongside the payment_key.
- mcp tool wallet.stake.address surfaces the bech32.
stake delegation (4.6):
- new aldabra-core::stake module:
- parse_pool_id(bech32) → Hash<28>
- build_signed_stake_delegation(payment, stake, network, utxos,
change_addr, pool_bech32, register_first, params) → signed cbor.
- if register_first: prepends a StakeRegistration cert (consumes
a 2 ADA deposit from inputs). otherwise just delegates.
- signs with both payment_key (body witness) and stake_key (cert
witness). reuses sign::add_witness for both — same body-hash
ed25519 signing path regardless of CIP-1852 chain index.
- mcp tool wallet.stake.delegate: pool_id, register_first (defaults
true). signs + submits.
3.6 close-out — wallet.mint.unsigned mcp tool:
- exposes the existing build_unsigned_mint with caller-supplied
PolicySpec (json), so multi-sig / treasury flows can build through
this wallet without it auto-signing. round-trip with
wallet.sign_partial chain → wallet.submit_signed_tx.
depends on Sulkta-Coop/pallas@feat-aux-data which gained two more
patches in the same branch:
- StakeAddress::new public constructor.
- StagingTransaction::add_certificate / clear_certificates +
Conway::build_conway_raw decode-and-plumb for certs (filling in the
`certificates: None, // TODO` upstream).
mcp tools: 12 → 15 (wallet.stake.address, wallet.stake.delegate,
wallet.mint.unsigned).
79 → 84 unit tests. new coverage: stake address bech32 round-trip,
pool_id bech32 parse + reject-wrong-hrp, delegation tx with + without
registration (asserts cert count, witness count, cert variants).
fork tests grew: certificates_plumb_through_to_tx_body and
no_certificates_means_none.
new aldabra-core::cip68 module:
- asset name prefixes 100 (0x000643b0 ref) / 222 (0x000de140 user) /
333 (0x0014df10 ft). prefixed() guards 32-byte total cap so caller
can't blow past the cardano protocol limit by accident.
- json_to_plutus_data: serde_json::Value → PlutusData (recursive).
numbers must fit i64. strings → BoundedBytes (cip-68 convention is
bytes-keyed datum maps, not text). null is rejected, floats rejected.
- build_cip68_datum_cbor wraps the metadata in the canonical
Constr 0 [meta_map, version_int=2, Constr 0 []] shape.
new aldabra-core::mint::build_signed_cip68_nft_mint:
- mints two assets simultaneously under one policy (ref + user, qty 1
each), three outputs (ref @ ref_addr w/ inline datum, user @ user_addr,
change). same two-pass fee refinement as the rest of the path.
- mutable nfts: pass ref_addr == change_addr. wallet's payment key can
later spend the ref UTXO and re-create with new datum.
- immutable: caller passes an always-fails script address (phase 4
concern; today this fn trusts whatever's passed).
new aldabra-core::sign module + add_witness:
- decodes a conway tx (any state — unsigned or partially signed),
signs the body hash with the wallet's payment key, appends a
VKeyWitness to the witness_set, re-encodes. body is invariant
(regression test asserts the body hash before and after the witness
append are identical).
- this is the missing primitive for n-of-k multisig flows: each party
calls add_witness on the previous party's output cbor; any party
submits via wallet.submit_signed_tx.
mcp tools: 10 → 12.
- wallet.mint.cip68_nft — args: user_address, name_body_hex (≤28b),
metadata (json object), user_lovelace? ref_address? ref_lovelace?
invalid_after_slot? — defaults provided for the ergonomic case
(ref_addr=wallet, lovelace=1.5 ADA each).
- wallet.sign_partial — args: cbor_hex — appends our witness, returns
updated hex. usable for MAP treasury 2-of-2 once a
wallet.mint.unsigned-with-policy-arg lands (TODO, deferred).
65 → 79 unit tests. cip68 module: 9 tests covering prefix+datum
shape. sign module: 4 tests covering one-witness, two-witness,
body-hash invariant, garbage rejection. integration test in mint
verifies cip68 build produces 3 outputs with inline datum on the
ref output.
unblocks named mints. wallet.mint now accepts an optional `metadata`
arg (json object); explorers + wallets render the asset with name/image
instead of <asset1xyz...>.
new aldabra-core::metadata module:
- json_to_metadatum: serde_json::Value → Metadatum (recursive). numbers
must fit i64 (cardano metadata Int width). strings >64 bytes split
into Array<Text> chunks at utf-8 char boundaries (CIP-25 v2
long-string convention). null is rejected.
- build_cip25_aux_data(policy_id_hex, asset_name_hex, json_value):
builds the label-721 wrapper (Map { 721: Map { policy_bytes:
Map { name_bytes: attrs }, "version": "2.0" } }), wraps in
AuxiliaryData::PostAlonzo, returns cbor bytes.
mint module:
- new build_signed_mint_with_metadata + build_unsigned_mint now take
optional cip25_metadata. backward-compat: build_signed_mint is a
thin no-metadata wrapper.
- prepare_mint + build_mint_staging plumb aux_data_cbor through.
staging.auxiliary_data(bytes) is the new fork API surface — when
set, conway::build_conway_raw decodes + computes
auxiliary_data_hash automatically.
- regression test build_signed_mint_with_metadata_produces_aux_hash:
decodes the resulting signed cbor, asserts both
body.auxiliary_data_hash is Some and tx.auxiliary_data is present.
catches the failure mode where metadata is silently dropped.
mcp wallet.mint gains a `metadata` arg field surfaced via schemars
JsonSchema. tools/list shape correctly carries the optional json
object.
depends on Sulkta-Coop/pallas@feat-aux-data — vendored via
[patch.crates-io] in the workspace Cargo.toml. PR upstream pending.
56 → 65 unit tests. 8 → 8 mcp tools (count unchanged, wallet.mint
gained an arg).
new aldabra-core::mint module:
- PolicySpec enum: SingleSig, SingleSigTimelock, NofK
- SingleSig{pkh}: ScriptPubkey native script
- SingleSigTimelock{pkh, slot}: ScriptAll[ScriptPubkey, InvalidHereafter(slot)]
- NofK{n, [pkhs]}: ScriptNOfK
- PolicySpec::single_sig(payment) + single_sig_timelock(payment, slot)
convenience constructors that derive the pkh from a PaymentKey.
- policy_id() = pallas_traverse::ComputeHash<28>::compute_hash, which
is blake2b-224 of (0x00 || cbor) — the canonical native-script hash.
- to_cbor() for callers that want the script bytes raw.
build_signed_mint / build_unsigned_mint:
- two-pass fee like the send path, plus a few extras specific to mint:
staging.mint_asset(policy, name, qty), .script(Native, cbor),
.disclosed_signer(payment_pkh) — the disclosed_signer surfaces the
required signature in the tx body so the chain knows which witness
to verify against the script.
- positive qty mints (asset goes into dest output), negative qty burns
(asset comes out of input holdings, change preserves leftover).
- token-bearing change must hold ≥ min_utxo lovelace — same guard as
the send path.
mcp tools:
- wallet.policy.create — args: invalid_after_slot? — returns
{policy_id_hex, script_cbor_hex, type}.
- wallet.mint — args: dest_address, dest_lovelace (≥ 1 ADA),
asset_name_hex, quantity (i64), invalid_after_slot? — auto-generates
a single-sig policy bound to the wallet's payment key, builds, signs,
submits.
8 → 10 mcp tools. 48 → 56 unit tests.
3.2 (CIP-25 metadata) is BLOCKED on pallas-txbuilder 0.32/0.35 — both
hardcode `auxiliary_data: None` in the conway builder. options for next
session: (a) post-build CBOR injection, (b) assemble tx via
pallas-primitives directly, (c) wait for upstream. flagged in the
spec doc.
3.3 (CIP-68) depends on 3.2. 3.6 (MAP 2-of-2) needs the multi-key
signing flow on the build side; PolicySpec::NofK variant is ready but
build_signed_mint only sign with one key today.
InputUtxo gains an `assets: BTreeMap<String, u64>` field matching
aldabra-chain::Utxo's shape (`policy_id_hex(56) || asset_name_hex`
key). new AssetSpec type for the recipient asset list.
asset-aware select_utxos:
- phase 1: per-asset greedy by holding size, pulls UTXOs containing
each requested asset until coverage ≥ target
- phase 2: ada-only greedy to top up lovelace need
this preserves the prior ada-only behavior when assets list is empty.
build_signed_payment_with_assets / build_unsigned_payment_with_assets
build outputs with .add_asset() for each requested + each leftover
(change-side). guards: token-bearing change must hold ≥ min_utxo
ADA — surfaced as a clearer error than letting the chain reject a
sub-min output.
cold-sign flow (phase 2.6):
- new tools wallet.send.unsigned (returns {cbor_hex, summary} json
for human review + cold-signer consumption) and
wallet.submit_signed_tx (takes hex-encoded signed cbor → submit).
- PaymentSummary now carries send_assets + change_assets vecs so the
human reviewer can spot accidental token transfers.
- summary.tx_hash is the predicted body hash; signed CBOR will hash
to the same value (signature is over the body, not the cbor wrapper).
helpers: hex_encode/decode, parse_policy_id, parse_asset_name,
split_asset_key. mcp side defines its own McpAssetSpec with
schemars::JsonSchema derive so the schemars dep doesn't bleed into
the security-boundary core crate.
48 unit tests (was 41). new coverage: asset-aware selection (greedy +
missing-asset error), policy/asset-name parsers, multi-asset cbor
build, change-asset summary correctness.
phase 2.7 (live preprod smoke against funded wallet) procedure
documented in memory/spec-aldabra-buildout.md; needs cobb's faucet ada.
end-to-end working wallet: paste 24-word mnemonic, age-encrypt at rest,
on unlock derive root + payment + stake keys, build cip-19 base address,
serve four tools over mcp stdio (wallet.address, wallet.network,
wallet.balance, wallet.utxos).
deps added: ed25519-bip32 0.4 (pallas only ships raw ed25519, not the
cardano variant of bip32 hd derivation), cryptoxide 0.4 for pbkdf2-hmac-sha512,
age 0.10 for at-rest mnemonic encryption, rpassword 7 for tty-only passphrase
prompts, toml 0.9 for config.toml.
new modules:
- crates/aldabra-core/src/derive.rs — payment + stake key derivation, hash
- crates/aldabra-chain/src/koios.rs — real reqwest impl, asset aggregation
- crates/aldabra-mcp/src/{bootstrap,config,tools}.rs
caught one bug pre-flight: get_balance was clobbering same-asset
quantities across utxos instead of summing. fixed + regression test.
headless support via ALDABRA_PASSPHRASE env (mcp clients own stdin so
the rpassword prompt path can't run). docker secret / systemd
EnvironmentFile sources it in production.
dockerfile: multi-stage rust:1.95-bookworm → debian:bookworm-slim, tini
as pid1, non-root aldabra user, /var/lib/aldabra owned 700.
29 unit tests + 1 ignored live-koios test. preprod smoke test exercised
initialize → tools/list → tools/call wallet.address end-to-end via
piped json-rpc; correct preprod address came back from canonical
abandon-art mnemonic.
phase 2 (send) is next.