Commit graph

84 commits

Author SHA1 Message Date
0c79231936 fix(mcp): clamp vote validity_upper to voting_end_slot when range overshoots
Same H-2-class issue as the advance Draft→VotingReady clamp. The
vote MCP tool computed validity_upper = tip_slot + 1799 then
errored if it overshot voting_end_check. On 30-min Sulkta-shape
DAOs the voting window is 30 min wide, and the moment chain time
crosses into the voting window the default 1799-slot upper bound
lands ~30 min past tip — already past voting_end if voting_start
was a few minutes ago.

Now: when default upper would overshoot, clamp validity_upper_slot
to voting_end_slot. Reject if remaining slots ≤ 5 (chain has no
room to include).

Caught 2026-05-08 trying to vote on preprod_test2 proposal #1
during a clean voting window — tx_upper landed 135s past
voting_end. Clamp lets the vote tx fit.

Cosign builder still has the same raw tip_slot pattern; deferred
since cosign also requires within-Draft semantics and we don't
have a multi-stake test yet to exercise it.
2026-05-08 11:48:36 -07:00
a485a6f0bf fix(dao,mcp): clamp proposal_advance validity range to fit phase window
The advance builder previously hard-coded the validity range to
[tip_slot, tip_slot + VALIDITY_RANGE_SLOTS=1799]. For early
Draft→VotingReady advance with the wide 1799-slot range, the upper
bound shoots ~30min past starting_time — way past drafting_end on
any DAO with windows narrower than 30 min, including Sulkta-shape
30-min DAOs whose drafting period happens to start partly elapsed.
Validator's getTimingRelation rejects straddles and the MCP layer
then errored 'tx validity range straddles drafting period
boundary'.

Two-part fix:

1. ProposalAdvanceArgs gains optional valid_from_slot_override +
   invalid_from_slot_override fields. None preserves legacy behavior;
   Some(...) lets the MCP layer dictate a clamped range. Builder
   defends against degenerate ranges (invalid_from <= valid_from).

2. MCP-side dao_proposal_advance_unsigned, when transition is
   DraftToVotingReady or VotingReadyToLocked and the natural [tip,
   tip+1799] would overshoot the period end, clamps invalid_from
   to the period_end slot. Refuses if remaining slots < 5 (chain
   has no room to include the tx) and prompts the caller to wait
   for the failed-too-late path instead.

Caught 2026-05-08 trying to drive preprod_test2 proposal #0
through the proper Draft→VotingReady arc — chain time was ~3 min
past starting_time, so 1799-slot range overshot drafting_end by
~27 min. Same code path now clamps to the remaining ~27 min of
drafting period.

Closes audit finding H-2 for the DraftToVotingReady and
VotingReadyToLocked transitions. Cosign + vote builders also have
H-2 (raw tip_slot for validity_from); those are deferred.
2026-05-08 11:09:26 -07:00
bf860dc99b feat(mcp,chain,dao): support Koios paid-tier bearer via ALDABRA_KOIOS_BEARER env
Adds optional Authorization: Bearer <token> on every Koios request,
sourced from ALDABRA_KOIOS_BEARER env var only — never from the
on-disk config.toml, never from CLI args, never hardcoded. Bearers
are credentials and the on-disk config dir gets routinely backed up;
keeping them env-only guarantees rotations don't leak into snapshots.

Wired through three Koios clients:
- aldabra-chain::KoiosClient — new with_timeout_and_bearer ctor;
  legacy new() / with_timeout() route through it with bearer=None.
- aldabra-dao::KoiosDaoReader — new with_bearer ctor; ditto.
- aldabra-dao::KoiosDiscoveryClient — new with_bearer ctor; ditto.

Bearer is set as a default header on the reqwest client builder so
every request inherits it without per-call boilerplate.
HeaderValue::set_sensitive(true) prevents the value from showing
in reqwest's debug-format output.

Config wiring (aldabra-mcp::config::Config):
- New koios_bearer: Option<String> field. Loaded ONLY from
  ALDABRA_KOIOS_BEARER env var; absent or empty-string means None.
- Startup tracing logs koios_bearer_set: bool — never the value.

WalletInner caches the bearer alongside the koios_base so the
on-demand KoiosDiscoveryClient (constructed inside
dao_discover_scripts) inherits paid-tier auth too.

Motivation: 2026-05-08 preprod_test2 bringup tripped Koios free-tier
daily quota (5240 req/day, 'Exceeded Tier Limit') mid-deploy. Cobb
provided a paid-tier JWT (Aldabra project, exp 2026-06-26). Wiring
via env var lets the operator (systemd EnvironmentFile, docker run
-e, or k8s Secret) inject it without touching code or config files.
2026-05-08 10:19:06 -07:00
fbc4955c1d fix(core): bump WITNESS_OVERHEAD_BYTES from 256 to 512
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.
2026-05-08 07:42:06 -07:00
4472007dae fix(core): bump WITNESS_OVERHEAD_BYTES from 128 to 256
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.
2026-05-08 07:12:04 -07:00
5235a5d4c3 fix(dao): center starting_time in proposal_create validity window
For tiny-window test DAOs (preprod_test: 30s), the prior anchor
(valid_from = starting_time, invalid_from = starting_time + 30)
gave zero past-side slack. With koios block_time vs real chain
clock skewing ±60s on the public endpoint, hitting that window
is essentially a coin flip — the tx submits but never confirms
because the next block lands after invalid_from.

Centering keeps the validator-required width unchanged but moves
valid_from to starting_time - 15, so the chain now has 15s of
past-side slack to land the tx in a block. Same width, same
in-script time check (starting_time still ∈ [valid_from, invalid_from)),
better landing odds.

Sulkta-shape DAOs (1800s windows) are unaffected: 900s of slack
each side is plenty either way.
2026-05-07 20:19:27 -07:00
b7074fd81b fix(dao): prepend (cons) Created lock instead of append in proposal_create
Agora's stake validator (ppermitVote) enforces output_locks =
pcons NEW_LOCK old_locks (head-cons, not append). proposal_create
was using Vec::push which appends, so when the stake had any
pre-existing locks the output lock order didn't match what the
validator expected and the chain rejected with CekError on the
stake validator (5178-byte script, hash 57d6b17f...).

The bug went undetected on 2026-05-07 because that day's first
proposal_create ran on a stake with locked_by = [], where push
and prepend produce the same single-element vector. Today's
proposal #1 attempt — stake already holding a Created lock for #0 —
flushed it out.

Mirror the prepend pattern that proposal_cosign and proposal_vote
already use: build new_locks with the new lock at index 0, then
extend with the old locks.
2026-05-07 19:43:10 -07:00
5e6cb7056b fix(dao): wire V2 cost model into advance/cosign/vote/stake_destroy
Same trap that hit proposal_create yesterday: every spending tx that
witnesses a PlutusV2 script (Agora's proposal_validator, stake_validator,
proposalSt policy on burn) needs language_view in the tx body so the
chain-side script_data_hash matches the off-chain one. Without this the
chain rejects with PPViewHashesDontMatch.

proposal_create got the fix in 044ebd23. The other four builders shipped
without it, so dao_proposal_advance_unsigned (Draft → Finished on
preprod_test #0) hit PPViewHashesDontMatch on submit. Mirror the same
language_view + ScriptKind::PlutusV2 + PLUTUS_V2_COST_MODEL_PREPROD
wiring into the remaining four staging chains.
2026-05-07 18:56:57 -07:00
1bc4e949ab fix(dao): use PermitVote (not DepositWithdraw) for stake spend on proposal_create
Stake validator's DepositWithdraw branch requires locked_by to stay
EMPTY. proposal_create wants to ADD a Created lock for the new
proposal — that's PermitVote's job, not DepositWithdraw's.

Caught by base64-decoding the CekError's failing-script header on
preprod_test today: 0x59 0x14 0x37 = bytes(5175) ⇒ 5178-byte script
= stake validator (57d6b17f), not the governor we'd been suspecting.

This is the audit C-2b fix that landed late.
2026-05-07 18:09:20 -07:00
f44a4f209c fix(dao): anchor proposal-create validity range on starting_time_slot
Public Koios's tip endpoint can lag the actual chain by 100+ slots.
Under a 29-slot governor window that lag pushes invalid_after into
the past before the tx ever reaches a node — every retry hits
OutsideValidityIntervalUTxO no matter how fast we sign+submit.

Now: caller passes starting_time_slot derived from starting_time_ms
via the network's shelley constants; builder uses it as valid_from.
Caller can shift starting_time_ms slightly into the future to
compensate for MCP roundtrip latency. The on-chain
'pvalidateProposalStartingTime' is still satisfied because
starting_time_slot ∈ validRange by construction.
2026-05-07 17:31:45 -07:00
66eacf5749 fix(dao): clamp proposal-create validity range to per-DAO max_width
VALIDITY_RANGE_SLOTS const was hardcoded to 1799 (Sulkta's 30min budget
minus 1 slot). For tiny test DAOs (preprod_test: 30s) this overshoots
the governor's create_proposal_time_range_max_width and the validator
rejects with CekError on submit. Now: derive max width from
GovernorDatum.create_proposal_time_range_max_width / 1000 - 1, capped
at VALIDITY_RANGE_SLOTS for safety.
2026-05-07 17:12:10 -07:00
044ebd2379 fix(dao): wire V2 cost model into proposal_create staging
Without staging.language_view(), pallas does not compute
script_data_hash. Chain rejects the tx with PPViewHashesDontMatch.
Same trap that plutus_mint hit on 2026-05-07 — same fix here.

Caught attempting first dao_proposal_create_unsigned on preprod_test
DAO 2026-05-07 PM after deploying governor + proposal validator ref
UTxOs via the new file-path workaround for the MCP large-string bug.
2026-05-07 16:57:05 -07:00
2d4c2163a9 mcp: add reference_script_path arg to bypass MCP large-string transport bug
Caught 2026-05-07: hex strings passed to MCP tool args > ~4500 chars
get a 1-byte truncation + structural rearrangement somewhere between
Claude Code and aldabra's stdio reader. Pallas + aldabra-core's
hex_decode are byte-clean (verified via crates/aldabra-dao/examples/
repro_script_corruption.rs); the corruption is purely in the
JSON-RPC-over-stdio transport layer.

Workaround: accept reference_script_path that points at a file
inside the aldabra container. Caller docker-cp's the hex file in,
passes the path via MCP arg, aldabra reads bytes locally — no
large strings cross the JSON-RPC wire.

Applies to wallet_send + wallet_send_unsigned. wallet_plutus_mint_*
tools still ride the hex-string path (small policies only, < 4500
chars). When we hit a Plutus policy that needs the workaround, port
the same pattern.
2026-05-07 16:45:53 -07:00
288e5815a0 diag: also exercise aldabra-core's build_unsigned_payment_extras 2026-05-07 16:30:20 -07:00
f685e53889 diag: also test through aldabra's hex_decode 2026-05-07 16:26:55 -07:00
a627403492 diag: reproducer also reports script bytes-header consistency 2026-05-07 16:24:34 -07:00
d71b543ae6 fix repro_script_corruption imports 2026-05-07 16:10:58 -07:00
340a4ee408 diag: standalone reproducer for large-bytestring ref-script corruption
cargo run --example repro_script_corruption -p aldabra-dao --release

Reads a hex-encoded Plutus V2 script, builds a minimal Conway tx
with that script as inline reference, calls build_conway_raw, then
searches the tx body for the input bytes verbatim. Also tests the
known on-chain block-swap corruption fingerprint (bytes 2390-2398
swapped with bytes 2416-2424) to determine whether pallas
reproduces the corruption locally.

If verbatim found: pallas is byte-clean, bug is downstream
(transport / Koios / chain submit). If swapped variant found:
pallas itself produces the corruption.

No chain query, no MCP, no JSON-RPC — pure local serialization.
2026-05-07 16:09:41 -07:00
a2a1f72a74 plutus_mint: thread additional_signers into tx body's required_signers
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.
2026-05-07 10:33:55 -07:00
6708d448d8 feat(examples): dump_stake — emit StakeDatum CBOR hex for bootstrap
Companion to dump_governor (committed earlier this branch). Edit
owner_pkh_hex + staked_amount in the source, then `cargo run` to
print the inline datum CBOR for a wallet_plutus_mint_unsigned call
that mints StakeST + sends to stakes_addr.

No locks at bootstrap (locked_by = []) and no delegation
(delegated_to = None). For a stake that's been used in proposals,
locked_by would carry the ProposalLock entries; reuse this scaffold
when reseeding a stake from a snapshot.
2026-05-07 10:21:27 -07:00
ca2f69d28e feat(plutus_mint): set language_view per Plutus version + add V2 cost model
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.
2026-05-07 08:52:59 -07:00
b50d45b5de feat(mcp): wallet_plutus_mint_unsigned MCP tool
Wires the new aldabra-core::plutus_mint module into MCP. Tool name
mirrors the unsigned-first DAO write convention (wallet_send_unsigned,
dao_proposal_*_unsigned, etc).

Args:
- policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex
- mint_assets: array of {asset_name_hex, quantity}
- dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex
- required_input_refs: array of 'txhash#index' UTxOs that MUST be spent
- ex_units (mem + steps, optional — defaults to DEFAULT_EX_UNITS)

Returns the standard unsigned-payment shape ({cbor_hex, summary})
ready for wallet_sign_partial → wallet_submit_signed_tx.

Used for governor + stake + proposal bootstrap of any Plutus DAO.
For Agora preprod bringup, the typical call is:
- governor: required_input_refs=[gstOutRef], mint=[(GST,1)],
  dest=governor_addr, datum=GovernorDatum
- stake: mint=[(StakeST,1)], dest_extras=[(tTRP,N)], dest=stakes_addr,
  datum=StakeDatum
- proposal: spends GST input under the existing governor's spend
  redeemer + mints ProposalST under the proposal policy
2026-05-07 06:36:05 -07:00
86bc4e45cd feat(plutus): plutus_mint module — Plutus-policy mint with custom output
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.
2026-05-07 06:32:59 -07:00
a65ab7803e chore(mcp): drop unused build_signed_payment_with_assets / build_unsigned_payment_with_assets imports
Replaced direct calls in wallet_send + wallet_send_unsigned with the
new _extras variants in the previous commit. The _with_assets re-exports
are still in aldabra-core::lib.rs for any other callers, just not
imported into tools.rs anymore.
2026-05-07 06:21:33 -07:00
b9124ee5d9 feat(wallet): reference-script + extras on payment outputs
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.
2026-05-07 06:17:07 -07:00
82e8273969 build(docker): mount git credentials as buildkit secret for pallas SSH→HTTP fetch
The pallas patch in [patch.crates-io] is now ssh://git@gitea after the
2026-05-06 token-scrub. Inside a docker build the rust container has no
SSH key and no known_hosts for gitea, so cargo's libgit2 / system-git
both reject the fetch.

Mount /root/.git-credentials as a BuildKit secret (mode=0400, required)
and set a build-time `url.HTTP.insteadOf SSH` rewrite. Cargo.toml and
Cargo.lock keep their SSH URLs — the rewrite is git-CLI-level so no
credential ever lands in the lock file or in any image layer.

Build invocation:
  docker build --secret id=git_credentials,src=<creds-file> ...

where <creds-file> is one line `http://USER:PAT@192.168.0.5:3001`.

This mirrors the pattern crafting-table already uses on its runner
(.git-credentials + url.insteadOf rewrite). nightly-builds.sh on Lucy
will need an analogous --secret arg before it can rebuild this branch.
2026-05-06 13:45:55 -07:00
c695fb02f2 Revert "feat(dao): Phase 4c-bis-1 + 4c-bis-2 — typed EffectsMap + GAT policy config"
This reverts commit 09e8bb3e1d.
2026-05-06 10:39:51 -07:00
09e8bb3e1d feat(dao): Phase 4c-bis-1 + 4c-bis-2 — typed EffectsMap + GAT policy config
## Type port effects_raw → EffectsMap (4c-bis-1)

Replace ProposalDatum.effects_raw: PlutusData with typed
effects: EffectsMap. Required precondition for the upcoming
proposal_mint_gats builder — every downstream piece reads typed
effects, not opaque bytes.

- ProposalEffectMetadata { datum_hash: 32 bytes, script_hash: Option<28 bytes> }
  ProductIsData → CBOR Array. Field order matches Agora.Proposal.hs:296.
  Maybe ScriptHash → Constr 0 [bytes]/Constr 1 [].
- EffectsMap: Vec<(ResultTag i64, Vec<(ScriptHash, ProposalEffectMetadata)>)>.
  Encoded as nested PlutusData::Map. Keys preserved in insertion
  order (Plutus map equality is set-like, but validator builds the
  expected datum in same order so we stay consistent).
- EffectsMap::info_only(&[0, 1]) helper for the proposal_create case
  (every result_tag → empty inner map). Replaces the hand-rolled
  PlutusData::Map.
- has_neutral_effect() + keys() helpers for validator preflight checks.

Live-decode test (decodes_sulkta_live_proposal_zero) tightened:
Sulkta #0 is NOT pure InfoOnly — tag 1 has a real effect targeting
script hash 92b7..96f with datum_hash 046dff..e83c (no auth-script
wrapper). Tag 0 is empty so phasNeutralEffect still passes. Test
asserts the full typed shape now + round-trips via decode↔encode.

All 4 builders' fixtures updated: effects_raw: constr(0, vec![]) →
effects: EffectsMap::info_only(&[0, 1]). Unused constr/PlutusData/
KeyValuePairs imports pruned.

## DaoConfig GAT policy fields (4c-bis-2)

- DaoConfig.gat_policy: Option<String> (56 hex)
- ScriptRefs.gat_policy_ref: Option<String> (txhash#index)
- DaoConfig::validate now checks gat_policy + stake_st_policy +
  proposal_st_policy are 56 hex chars when set
- All DaoConfig fixtures updated with gat_policy: None
- DaoRegisterArgs gains gat_policy + gat_policy_ref optional fields
- dao_show output includes the new fields automatically (serde)

Sulkta-specific note: gat_policy hash isn't observable on chain
yet (no MintGATs tx has fired). Hand-populate from MLabs deployment
record when ready, or compute from the deployed governor's CBOR
parameters.
2026-05-06 10:13:11 -07:00
7d440288bd test(dao): live-stake round-trip checks decoded struct, not bytes
The first cut asserted byte-exact CBOR round-trip, but pallas-codec
emits def-arrays (`81`) while chain CBOR uses indef (`9f...ff`).
Both are Plutus-structurally-equal — validator's `==` accepts
either — but Vec<u8> equality doesn't. Switch to assert
`decode(reencode(decode(cbor)))` equals `decode(cbor)` instead.
That's the actual validator-relevant invariant: typed fields
preserved, no silent drift.
2026-05-06 08:53:04 -07:00
6a408c319a feat(dao): multi-net slot↔ms + live-decode StakeDatum tests
## Multi-net slot↔ms (drops mainnet-only gate)

Replaces mainnet_slot_to_posix_ms with slot_to_posix_ms(network, slot)
that handles all three Cardano networks. Vote + advance MCP tools no
longer hard-error on preprod/preview.

Per-network Shelley HF constants (slot, posix_ms_at_slot):
- mainnet:  4_492_800   1_596_059_091_000  (2020-07-29 21:44:51 UTC)
- preprod:  86_400      1_655_769_600_000  (2022-06-21 00:00 UTC)
- preview:  0           1_666_656_000_000  (2022-10-25 00:00 UTC)

Pre-Shelley (Byron) slots still rejected — they had different lengths
and DAO operations don't need them. Routed via cfg.network at every
call site so the MCP tool's behavior follows the active DAO's chain.

## Live-decode StakeDatum regression tests

Anchors the StakeDatum type port to two real on-chain UTxOs at the
Sulkta stakes_addr:

- decodes_sulkta_live_kayos_stake — 50 Terrapin, owner pkh 84d0..f2f3,
  utxo d5b73a9d...#0
- decodes_sulkta_live_cobb_stake  — 250 Terrapin, owner pkh c5e3..bfda,
  utxo 0823a940...#1

Both assert byte-exact CBOR round-trip after decode → to_plutus_data
→ encode. This is the validator-critical property: the proposal
validator does `mkRecordConstr expectedDatum #== outputDatum`
on every output, so any drift in field order, integer width, or
empty-list shape would silently break vote/cosign/advance txs.
Companion to decodes_sulkta_live_proposal_zero in proposal.rs (which
covers the 8-field ProposalDatum side).
2026-05-06 08:38:15 -07:00
569c336f1d fix(dao-mcp): vote tool H-3 preflight reads from prop_datum (post-move)
The H-3 fix in commit a0daadf referenced target.datum.starting_time
+ target.datum.timing_config after target.datum had already been
moved into prop_datum upstream in the function. Switch to reading
from prop_datum directly. Pure compile fix; logic unchanged.
2026-05-06 08:08:35 -07:00
a0daadf38e fix(dao): audit punch list — H-1 to H-4 + M-2 + pallas bump
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.
2026-05-06 08:06:44 -07:00
b93bda75c9 build: switch aldabra-pallas patch URLs to SSH
Per rescope 2026-05-06: real code repos get SSH for git auth, no
embedded credentials in URLs at all. Companion to commit a3a8421
which dropped the embedded token; this drops the HTTP transport
in favor of pure SSH key auth.

- Cargo.toml [patch.crates-io] URLs now ssh://git@192.168.0.5:23/...
- Cargo.lock source URLs match.
- .cargo/config.toml [net] git-fetch-with-cli = true unchanged —
  still required so cargo defers to system git, which uses the
  configured SSH identity.

Hosts that build aldabra need:
- /root/.ssh/config alias 'gitea' → 192.168.0.5:23, IdentityFile
  pointing at a key registered to the kayos Gitea account
- The corresponding private key

The ed25519 key generated for this is at
/root/.openclaw/keys/id_ed25519_kayos_gitea on the dev box. Pubkey
registered on kayos's Gitea account 2026-05-06. The crafting-table
runner on Lucy still uses an HTTP credential helper for now — that's
operational state in a kayos-controlled container, allowed under
the rescope. Will migrate to SSH later if Cobb wants full parity.
2026-05-06 07:55:32 -07:00
a3a842138c build: strip Gitea token from pallas patch URLs + add cargo config
Hard rule from Cobb 2026-05-06: zero secrets hardcoded in committed
source. The [patch.crates-io] block had the kayos Gitea PAT embedded
in the URL, which cargo then duplicated into Cargo.lock's source URLs.

Fix:
- Cargo.toml [patch.crates-io] URLs are now tokenless
  (http://192.168.0.5:3001/...)
- Cargo.lock source URLs scrubbed to match
- .cargo/config.toml adds [net] git-fetch-with-cli = true so cargo
  defers to system git for fetches; system git authenticates via
  the user's git credential helper (~/.git-credentials chmod 600).

Operators (devs + crafting-table runner) need a working git credential
helper for the LAN Gitea, configured out-of-band (NOT in this repo).
Pattern: `git config --global credential.helper store` +
`echo http://USER:TOKEN@192.168.0.5:3001 > ~/.git-credentials &&
chmod 600 ~/.git-credentials`. After Cobb rotates the kayos PAT,
update that file on every host that builds aldabra.
2026-05-06 07:45:37 -07:00
6443dcd858 feat(governance): wallet_drep_vote_cast + pallas voting_procedures patch
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.
2026-05-06 07:14:17 -07:00
4d3ef03978 feat(governance): Phase 5 — vote_delegate + drep_register/deregister
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.
2026-05-06 07:08:08 -07:00
d007817796 feat(dao): proposal_advance state machine + stake_destroy + MCP tools
Phase 4c + 4d. Closes the DAO write-path arc (excluding GAT minting,
which is Phase 4c-bis since Sulkta has never executed a proposal).

## proposal_advance (Phase 4c)

State-machine builder with 5 transitions:
- Draft → VotingReady (cosigner threshold met, all cosigner stakes
  ref'd as txInfo.referenceInputs, sum staked_amount ≥ to_voting)
- Draft → Finished (drafting period elapsed without enough cosigners)
- VotingReady → Locked (winner outcome exists with votes ≥ execute,
  no tie)
- VotingReady → Finished (locking period elapsed without winner)
- Locked → Finished (executing period elapsed; for InfoOnly proposals)

Validator (PAdvanceProposal in Proposal/Scripts.hs:657) requires
output proposal datum equals input with ONLY status mutated. Builder
mirrors exactly. Per-transition preflights match validator gates.

Cosigner stake refs go in as txInfo.referenceInputs (not regular
inputs) per witnessStakes pattern (Proposal/Scripts.hs:366) — sum
of staked_amount is computed from the ref-input set.

GAT-minting Locked→Finished path (effected proposals) deferred to
4c-bis. The pmintGATs governor redeemer is a separate tx that fires
ONLY when the executing period is in window AND the winner outcome
has effects to mint GATs for. Sulkta's first proposal was InfoOnly
so this path never exercised on chain yet.

11 unit tests covering every transition + every preflight reject.

## stake_destroy (Phase 4d)

Burns StakeST token + returns gov-tokens to owner. From
Stake/Redeemers.hs pdestroy (~L432): owner signs (no delegatees),
all locks empty, no stake output at stakes_addr. From stakePolicy
burn branch (~L161): burntST quantity = -spentST.

Tx shape: spend stake (Destroy redeemer) + maybe a funding utxo +
collateral; mint -1 StakeST; one wallet output carrying gov-tokens
+ (stake.lovelace + funding - fee). Funding optional — stake's own
lovelace usually covers fees.

4 unit tests including the funding-optional path.

## MCP tools

dao_proposal_advance_unsigned auto-picks the right transition from
proposal status + chain tip vs window boundaries. Mainnet-only gate.
Fetches cosigner stake refs by matching owner pkh against
proposal.cosigners.

dao_stake_destroy_unsigned fetches the wallet's stake (via owner
pkh match), pulls StakeST asset name from chain, burns it.
2026-05-06 07:00:48 -07:00
39b56223f9 feat(dao): proposal_cosign builder + dao_proposal_cosign_unsigned tool
Phase 4b. Cosign extends a Draft proposal's cosigners list — the
multi-stake bridge for clearing to_voting threshold when a single
stake doesn't have enough TRP. Validator (PCosign branch in
Proposal/Scripts.hs:433) requires:

- Status == Draft
- Exactly one stake input (ptryFromSingleton)
- New cosigner = stake.owner (delegatees rejected)
- Cosigner inserted into list via pinsertUniqueBy (sorted, no dupes)
- len(cosigners) ≤ max_cosigners (DaoConfig.max_cosigners)
- stake.staked_amount ≥ thresholds.cosign

Stake-side (ppermitVote PCosign branch): owner signs (not delegatee),
single stake input, new lock = ProposalLock { proposal_id, Cosigned }
prepended via paddNewLock = pcons.

Insertion order mirrors Plutarch's pfromOrdBy-derived Credential Ord:
variant index first (PubKey=0 < Script=1), then 28-byte hash lex.
`insert_unique_sorted` test-covered for low/mid/high positions + the
PubKey-before-Script invariant.

Also extract pull_wallet_utxos free function in tools.rs — shared
between the (future) refactor of create/vote and immediately by
cosign. Inline duplication in create/vote left as a future cleanup.

11 unit tests on the builder. Tool args: dao? + proposal_id +
fee_lovelace.
2026-05-06 06:51:53 -07:00
68e493dd2f refactor(dao): wire KoiosDaoReader::list_proposals + use it from vote tool
The first attempt's vote MCP tool inlined a Koios address_info pull
helper in tools.rs that needed reqwest + pallas_codec + pallas_primitives
as direct deps on aldabra-mcp — which it doesn't have. Compile failed.

Cleaner: move the work into the dao crate where those deps already live.

- ProposalUtxo gains `lovelace` + `proposal_st_asset_name_hex`. The
  vote builder needs both to construct the new proposal output.
- KoiosDaoReader::list_proposals (was stubbed) now reads cfg.proposal_addr,
  decodes every UTxO's inline datum to ProposalDatum, and matches the
  ProposalST asset name against cfg.proposal_st_policy when set, falling
  back to the first asset on the utxo when not (Sulkta convention is one
  ProposalST + nothing else).
- KoiosAsset.asset_name no longer #[allow(dead_code)] — it's read now.
- tools.rs::dao_proposal_vote_unsigned switches to dao_reader.list_proposals
  + drops the inline pull helper. ~150 LOC simpler.
2026-05-06 06:41:52 -07:00
3b0e0dd9bf feat(dao-mcp): wire dao_proposal_vote_unsigned + slot↔ms helper
- DaoProposalVoteArgs (dao? + proposal_id + result_tag + fee_lovelace)
- mainnet_slot_to_posix_ms: Shelley genesis constants (slot 4_492_800,
  posix 1_596_059_091_000) for converting tip+VALIDITY_RANGE_SLOTS
  into the Voted lock's posix_time field
- pull_proposal_utxos helper: address_info → decode every UTxO's
  inline datum into ProposalDatum, return matching proposal_id by id
  match (`KoiosDaoReader::list_proposals` is still stubbed; this is
  a focused write-path read)
- mainnet-only network gate (preprod/preview slot↔ms is Phase 5)
- get_info instructions text mentions write tools
- drop unused pallas_addresses::Address + pallas_txbuilder::ExUnits
  imports surfaced by clippy
2026-05-06 06:37:31 -07:00
a19439f640 feat(dao): proposal_vote.rs builder — Phase 3 unsigned tx
Mirrors proposal_create's shape: 3 inputs (stake script + proposal
script + funding wallet), 2 reference inputs (stake validator +
proposal validator), 2 outputs (mutated stake + mutated proposal +
maybe change), 2 plutus spends (PermitVote + Vote tag), no mints.

Pre-flight matches every Plutarch validator check from
Agora/Proposal/Scripts.hs PVote (~L484) + Stake/Redeemers.hs
ppermitVote (~L196):

- voter pkh is owner OR delegatee
- proposal status == VotingReady
- stake doesn't already have Voted lock for this proposal_id
- stake.staked_amount >= proposal.thresholds.vote (single-stake v1)
- result_tag is in proposal.votes keys (== effects keys)
- validity upper bound inside [starting_time + draft_time,
  starting_time + draft_time + voting_time]

New stake datum prepends the Voted lock per paddNewLock = pcons.
New proposal datum increments votes[result_tag] by stake amount;
all other fields preserved bit-exact since validator does record `==`.

Voted.posix_time = caller-supplied validity_upper_ms — matches
PFullyBoundedTimeRange _ upperBound the validator extracts. Caller
(MCP tool) computes ms-from-slot via mainnet Shelley genesis.

9 unit tests covering happy path + every preflight reject + delegated
voter accepted.
2026-05-06 06:31:22 -07:00
5102c77972 chore(dao): drop unused imports — ScriptRefs to test scope, StakeDatum gone from tools.rs 2026-05-05 20:58:03 -07:00
893e3f23da feat(dao-mcp): wire dao_proposal_create_unsigned to fetch C-2 inputs from chain 2026-05-05 20:57:36 -07:00
afd0cfb298 test(dao): update proposal_create test fixture for new args (stake_in + tip_slot + GST) 2026-05-05 20:56:09 -07:00
ea2ee01503 fix(dao): audit C-1 + C-2 + C-3 — informed by reference tx 7c8db1432a07 2026-05-05 20:55:20 -07:00
9556b7812d fix(dao): audit H-2 + H-5 + H-6 (per memory/audit-aldabra-dao-2026-05-05)
H-2: drop ExUnits to 5M/2G for spend, 2M/1G for mint
  Was 14M/10G each = per-tx Conway cap. With 3 plutus contracts running
  (governor spend + stake spend + ProposalST mint), total claim 42M/30G
  exceeds per-tx limit and node rejects pre-phase-2.

H-5: propagate malformed wallet asset keys instead of silently dropping
  Previous filter_map silently dropped any key < 56 chars. Could let a
  corrupt Koios response burn assets on submit. Now returns explicit
  Err with the offending UTxO + key.

H-6: tighten StakeST detection to asset_name == stake_validator_hash
  Per Stake/Scripts.hs:188-190 (pscriptHashToTokenName), StakeST
  asset_name is the stake validator's script hash. Previous code took
  "first non-gov-token asset" which would silently pick a wrong policy
  if a stake UTxO accidentally carried a junk NFT. Regression test
  h6_junk_token_does_not_pollute_stake_st_detection added.

3 of 7 audit punch-list items closed. C-1 + C-2 + C-3 next.
2026-05-05 20:52:22 -07:00
101c85c0a0 fix(dao): scope as_constr import to test that uses it 2026-05-05 20:14:33 -07:00
edd1948dec feat(dao): dao_discover_scripts MCP tool + Koios discovery client
New `aldabra-dao::discovery` module:
- `DiscoveryClient` trait + `KoiosDiscoveryClient` impl
- `discover_scripts(cfg, client, deployers)` — auto-finds:
  - governor_validator_ref + stake_validator_ref via deployer ref-script search
  - stake_st_policy from any existing stake UTxO (gov-token + non-gov-token asset)
  - stake_st_policy_ref via deployer search
- `apply_discovery(cfg, report)` — merges into DaoConfig (never overwrites)
- `script_hash_from_addr(bech32)` — extract 28-byte script hash from a script address

New MCP tool:
- `dao_discover_scripts { dao?, extra_deployers? }` — runs the audit logic
  against any registered DAO + persists the discovered fields back to the
  DaoConfig. Returns JSON with what was found + a gaps list for things
  v1 can't auto-discover (proposal_addr, proposal_st_policy).

Plus 4 unit tests with stub Koios responses validating the full pipeline:
script-hash extraction, StakeST discovery from stake UTxO assets,
validator ref-utxo matching at deployer, apply_discovery merge semantics.

WalletInner now caches `koios_base` so the discovery client can be
constructed on demand without re-passing the URL through args.
2026-05-05 20:14:13 -07:00
5913b9266a feat(dao-mcp): dao_register accepts Phase-4 fields in one call
Closes the gap between DaoConfig schema (already has fields) and the
dao_register tool (was rejecting/ignoring them). Now Sulkta DAO can be
registered with all audit-discovered values in one call:
  proposal_addr / stake_st_policy / proposal_st_policy +
  5 reference UTxO refs (governor / stake / proposal validators +
  StakeST / ProposalST minting policies).

All fields remain optional. dao_proposal_create_unsigned errors clearly
when one's missing. Future dao_discover_scripts tool will auto-populate
from chain queries.
2026-05-05 20:10:22 -07:00
3d95369536 chore(dao): drop unused as_constr import after EnumIsData fix 2026-05-05 19:50:34 -07:00