Audit report — fixpoint skill contribution (final external audit)
- Auditor: fixpoint/auditor-2 (EXTERNAL-AUDITOR, read-only)
- Audited tree: /code/cardano-dev-skills-fixpoint @
c373e79(branchfeat/fixpoint-skill, 8 commits over85e05ae) - Predecessor: auditor-1 @
9b8763f - Date: 2026-08-19
Verdict
FINDINGS — 0 BLOCKER, 3 SHOULD-FIX, 3 NIT.
The nine version-pinned action rules in Step 4 are, with one exception, accurate: I re-derived them from upstream sources at the pinned commits rather than from the probe handoffs, and every identifier, signature, and structural claim I checked matched. The exception is the Evolution SDK rule (finding 1), where a public build-option callback that receives the candidate transaction was missed by the probe and is contradicted by the skill's wording. The other two SHOULD-FIX items are a regression of an auditor-1 repair (finding 2) and a precondition stated more narrowly than the ledger ADR the skill itself cites (finding 3).
Independent verification (dimension 1 + 2)
Everything below was checked against upstream sources at the pinned commits/versions, not against the probe handoffs.
| Skill claim (SKILL.md) | Independently checked against | Result |
|---|---|---|
Cardano Tx Tools 0.2.3.0+14 / 7bfe95b: balanceTx, balanceFeeLoop (Coin -> Either String (StrictSeq (TxOut ConwayEra))), Peek :: (ConwayTx -> Convergence a) -> TxInstr q e a, FeeNotConverged; build not draft closes fee/ExUnits (L171-174) |
/code/cardano-tx-tools @ 7bfe95b (git describe = v0.2.3.0-14-g7bfe95b), Balance.hs:128,146,535; Build.hs:334,537,1314,1439 |
ACCURATE |
cardano-client-lib 0.8.0-pre5 / 4f1ea9c: UpdateOutputFunction(fee, outputs) + composable TxBuilder transforms over the assembled transaction (L174-176) |
fresh clone @ 4f1ea9c (gradle.properties version = 0.8.0-pre5): TxBuilder.java (void apply(TxBuilderContext, Transaction), andThen), FeeCalculators.java:63,176,201 (updateOutputWithFeeFunc.accept(totalFee, tbody.getOutputs())), BalanceTxBuilders.java (composes fee calc + change adjust + collateral) |
ACCURATE — matrix cells CCL P2/P3 SUPPORTED confirmed. Note: the in-builder bounded loop is ChangeOutputAdjustments.MAX_NO_RETRY_TO_ADJUST = 3 around change adjustment; the UpdateOutputFunction itself fires once per pass, which is exactly why the skill's "add an explicit bound" clause is needed and correct |
Scalus 1.0.0 / 6844943: DiffHandler(Value, Transaction) invoked inside a bounded balance loop; delayed redeemer/datum builders run before min-UTxO and balancing (L176-180) |
fresh clone @ 68449438: TransactionBuilder.scala:30 (type DiffHandler = (Value, Transaction) => Either[TxBalancingError, Transaction]), :1020-1059 (balanceFeeAndChangeWithTokens: @tailrec, MaxBalancingIterations, BalanceDidNotConverge, diffHandler(diff, txWithFees) each pass, fixpoint exit tx == trialTx), :637-640 (ensureMinAdaAll then .balance), TransactionStepsProcessor.scala:44-66 (delayed redeemers/datums replaced during applySteps, i.e. before balanceContext) |
ACCURATE — matrix cells Scalus P2/P3 SUPPORTED confirmed; also confirmed the same API exists at released tag v1.0.0 (see NIT 4 on the pin) |
Evolution SDK 0.5.12: fixed payment assets; redeemer callbacks get indexed inputs, not the candidate (L181-184) |
npm i @evolution-sdk/evolution@0.5.12: Operations.d.ts:38-46 (PayToAddressParams{address, assets, …}), RedeemerBuilder.d.ts:28-53 (IndexedInput{index, utxo}, SelfRedeemerFn, BatchRedeemerFn) |
ACCURATE |
| Evolution SDK: "exposes fee and transaction only on the completed result" (L182-183) | TransactionBuilder.d.ts:150-157 (BuildOptions.evaluator?: Evaluator, evaluate: (tx: Transaction, …) => …), phases/Evaluation.js:410-415 (evaluator.evaluate(transaction, …) inside the balance/evaluation loop) |
INACCURATE — finding 1 |
CSL 17.0.0 / 6b42538: add_change_if_needed must come after all other fields; min_fee/build + fresh reconstruction for deeper recursion (L184-186) |
upstream file at that exact commit: rust/Cargo.toml version 17.0.0; rust/src/builders/tx_builder.rs:1822-1826 ("Warning: this function will mutate the /fee/ field … call this function last after setting all other tx-body properties"), :1136 set_fee, :2367 build, :2596 min_fee |
ACCURATE |
cardano-api 10.19.1.0 / b951a63: fixed TxBodyContent in, BalancedTxBody out, no candidate callback (L186-189) |
/code/cardano-api @ b951a638 (= "Release cardano-api-10.19.1.0", cabal version: 10.19.1.0), Fee.hs makeTransactionBodyAutoBalance signature verbatim |
ACCURATE |
Mesh SDK @meshsdk/core 1.9.1: fixed outputs/redeemers, CBOR after complete (L190-191) |
npm i @meshsdk/core@1.9.1: @meshsdk/transaction d.ts (txOut(address, amount), complete(): Promise<string>, getActualFee()), impl index.js:4352-4360 |
ACCURATE as written (see finding 1 for the matrix cell) |
cardano-cli 11.0.0.0: auto-balances fixed outputs; output/redeemer flags expose no callback (L192-195) |
bundled docs/sources/cardano-node-wiki/reference/cardano-node-cli-reference.md:38 ("builds an automatically balanced transaction"); flag surface from probe-1 (no local cardano-cli binary in this seat, so the --help dump was not re-executed) |
ACCURATE (flag-surface half accepted from probe evidence) |
Pallas txbuilder 1.1.1 / cdee91d: no balancing or fee/ExUnits calculation in build_conway_raw (L196-198) |
upstream at that exact commit: Cargo.toml workspace version 1.1.1; pallas-txbuilder/src/conway.rs:27-35 ("no automatic fee/ex-units calculation", "\"Raw\" means no balancing") |
ACCURATE |
Conway fee = a*size + b + tiered ref-script charge, 25 KiB tiers, 1.2 escalation from minFeeRefScriptCostPerByte (L59-63) |
bundled docs/sources/cardano-ledger/adr/2024-08-14_009-refscripts-fee-change.md:43-63 |
ACCURATE |
estimateMinFeeTx pads dummy VKey wits; prefer calcMinFeeTx / calcMinFeeTxNativeScriptWits with resolved UTxOs (L122-128) |
/code/cardano-ledger libs/cardano-ledger-core/src/Cardano/Ledger/Tools.hs:11-13,100-197 — all three exported; :194 "better and easier to use calcMinFeeTx" |
ACCURATE (see NIT 5 on the missing pin) |
| Repo mechanics | scripts/validate.py → PASSED, 17 skills; scripts/update-doc-counts.sh --check → skills 17 / sources 64, exit 0; SKILL.md = 226 lines (< 500); all 10 relative links in SKILL.md resolve on disk; frontmatter, section set, and <!-- Documentation lookup path … --> comment match the other 16 skills; README skills-table row accurate; manifest total_files: 3280 matches disk (3281 minus .manifest.yaml); docs/sources/cardano-tx-tools/ = 13 files, exactly what the tightened globs (README.md, docs/*.md) select; git status clean before and after |
PASS |
Regression check on auditor-1 findings (dimension 3)
| auditor-1 item | State at c373e79 |
|---|---|
| SHOULD-FIX 1 — Step 4 cites files outside the bundled corpus | PARTIALLY REGRESSED — file paths are gone and everything is version-pinned, but the "not bundled here; verify in a current upstream checkout" caveat that 23535f7 added was dropped by the 4409c6c rewrite → finding 2 |
| SHOULD-FIX 2 — all examples from one project | RESOLVED — nine surfaces across four capability classes; the author-affiliated project is one entry among three in the first bullet |
| SHOULD-FIX 3 — Conway-incomplete fee statement | RESOLVED — L58-63, verified against the bundled ADR |
SHOULD-FIX 4 — missing calcMinFeeTx alternative |
RESOLVED — L124-128, all three identifiers verified upstream |
NIT 5 — draft vs build |
RESOLVED — L173 ("use full build, not draft") |
| NIT 6 — "from zero" / "assumes" phrasing | RESOLVED — that prose no longer exists |
| NIT 7 — no inbound links | RESOLVED — build-transaction/SKILL.md:39, debug-transaction/SKILL.md:33 |
| NIT 8 — bare-noun skill name | Unchanged; accepted, triggering is description-driven |
| NIT 9 — 30-file Amaru lattice case study | RESOLVED — globs tightened to docs/*.md; side effect in NIT 6 below |
Both post-audit-1 prose edits read correctly in the final text: the illustrative pseudocode label (L132-133) and — where it survived the rewrite — the antecedent fix (Step 4 now names each project explicitly rather than saying "its").
Findings
1. SHOULD-FIX — the Evolution SDK rule says "only on the completed result"; a public build option gets the candidate mid-build
skills/fixpoint/SKILL.md:181-184
Evolution SDK (
@evolution-sdk/evolution0.5.12) accepts fixed payment assets, exposes fee and transaction only on the completed result, and gives redeemer callbacks indexed inputs rather than the candidate
BuildOptions.evaluator is a documented public extension point
("Implement this interface to provide custom script evaluation strategies") whose
method is
evaluate: (tx: Transaction.Transaction, additionalUtxos, context) => Effect<ReadonlyArray<EvalRedeemer>, EvaluationError>
(dist/sdk/builders/TransactionBuilder.d.ts:150-157). It is invoked with the
assembled candidate transaction inside the build loop
(dist/sdk/builders/phases/Evaluation.js:410-415; the phase doc states
"Re-evaluation happens every Balance pass"). So a caller can observe every
post-balance candidate; what it cannot do is return anything but execution units.
The same class of gap exists in Mesh SDK: MeshTxBuilderOptions.evaluator
(@meshsdk/common IEvaluator.evaluateTx(tx: string, …)) receives the serialized
candidate at @meshsdk/transaction/dist/index.js:4352-4360. The skill's Mesh
sentence survives this ("returns CBOR after complete" — true, no exclusivity
claimed); Evolution's does not, because of the word "only".
This also means the probe matrices are incomplete on their own terms: probe-1's
Evolution P3 cell ("toTransaction() is only on the completed result; internal
PhaseContext is not a public observation/re-entry callback") and probe-2's Mesh
P3 cell ("internal selection callbacks are not application hooks") both reason
about internal state and miss a public callback that takes the candidate. Under
the skill's own Step 2 definition of the pattern — "a candidate-transaction
callback whose result participates in another balance pass" (L116-118) — the
evaluator arguably qualifies structurally, even though it cannot carry
fee-dependent outputs or a final-output redeemer.
The action the rule prescribes (rebuild in an outer bounded loop for fee-dependent outputs and final-output redeemers) remains correct; only the justification is overstated.
Suggested fix: "…exposes the fee and the transaction on the completed build
result — a custom BuildOptions.evaluator sees each candidate mid-build but can
return only execution units — and gives redeemer callbacks indexed inputs…"; and
amend the two P3 cells to say "no hook that can supply values back into the loop"
rather than "no hook".
2. SHOULD-FIX — the "not bundled here, verify upstream" caveat was dropped when Step 4 was rewritten
skills/fixpoint/SKILL.md:169-198
At 96c449c, Step 4 opened with: "The upstream source details mentioned below
are not bundled here; verify them in a current upstream checkout before
relying on identifiers or types", plus a link to the one bundled anchor
(docs/sources/cardano-tx-tools/README.md). The 4409c6c rewrite removed both.
The final text now asserts nine version-pinned API branches under the heading
"Apply the verified API branch" with no statement of when, how, or against
what they were verified, and grep -n -i 'upstream\|bundled\|checkout'
skills/fixpoint/SKILL.md returns only the ADR reference at L62.
Concretely: a loaded agent has Read Grep Glob and no WebFetch, and not one of
the nine pins (7bfe95b, 4f1ea9c, 6844943, 0.5.12, 6b42538, b951a63,
1.9.1, 11.0.0.0, cdee91d) is checkable against the bundled corpus — the
weekly refresh covers only docs/sources/cardano-tx-tools/. Pinning makes the
claims falsifiable, which is a real improvement over 9b8763f, but it does not
tell the reader they must go outside this repo to falsify them. This is the
CONTRIBUTING:32 "no refresh mechanism and rots silently" concern that auditor-1's
finding 1 raised and that 23535f7 had answered.
Suggested fix: restore one sentence under the Step 4 heading — "Checked against the listed versions/commits; these sources are not bundled in this repo, so re-check identifiers and types in a current upstream checkout before relying on them."
3. SHOULD-FIX — the contraction argument's invariance precondition names only reference inputs
skills/fixpoint/SKILL.md:165-167
With the reference-input set fixed, the tiered reference-script charge is constant across these iterations, so it does not weaken that contraction argument.
The bundled ADR the skill cites says the charge is computed by combining all
regular inputs and reference inputs, resolving them, and summing every
reference script found in the resulting outputs
(docs/sources/cardano-ledger/adr/2024-08-14_009-refscripts-fee-change.md:27-33;
"All Plutus scripts contribute to this calculation, regardless if they are being
used or not"). A balance loop that selects an additional fee-paying input whose
UTxO carries a reference script therefore changes the tiered charge mid-loop —
and Step 3's assembleFullTransaction explicitly re-does selection each pass.
cardano-client-lib implements exactly this (FeeCalculators.java, the
totalRefScriptBytesInInputs block), which is corroborating evidence that the
input-side contribution is real and not theoretical.
Impact is bounded — the loop re-estimates from the real candidate each pass, and the skill mandates a bound and post-convergence validation regardless — so the consequence is a reader who believes an oscillation is impossible when it is merely unlikely.
Suggested fix: "With the input and reference-input sets fixed, the tiered reference-script charge is constant across these iterations…".
4. NIT — two pins name development snapshots, in two different notations
skills/fixpoint/SKILL.md:174-180
- cardano-client-lib
0.8.0-pre5,4f1ea9c:4f1ea9cis a merge commit on the development branch, and0.8.0-pre5is a pre-release. The cited capability is not new —UpdateOutputFunctionandfeeCalculator(int, UpdateOutputFunction)are present at the latest stable tagv0.7.2(verified bygit grep UpdateOutputFunction v0.7.2 -- '*/FeeCalculators.java'). Pointing the reader at a pre-release for a capability that ships in the current stable release is avoidable. - Scalus
1.0.0,6844943:git describeat68449438isv1.0.0-38-g68449438e— 38 commits past the release, presented as a bare1.0.0, while Cardano Tx Tools in the same list uses the honest0.2.3.0+14form for the same situation. The claim itself survives:DiffHandler, the boundedbalanceFeeAndChangeWithTokensloop (MaxBalancingIterations = 20),BalanceDidNotConverge, andredeemerBuilder: Transaction => Dataall exist at tagv1.0.0, which I checked out specifically to confirm this. Supporting evidence is also mislabelled: probe-1'sscalus.txtrecords "stable package version: 1.0.0 (build.sbt)", butval scalusStableVersioninbuild.sbt:14is the MiMa binary-compatibility baseline, not the version being built.
Suggested fix: pin 0.7.2 for cardano-client-lib (or say "0.8.0-pre5; also in
stable 0.7.2"), and write Scalus as 1.0.0+38, matching the Cardano Tx Tools
notation.
5. NIT — the ledger-level bullet is the one unpinned API claim
skills/fixpoint/SKILL.md:122-128
Step 4 pins every surface; Step 2's ledger bullet names estimateMinFeeTx,
calcMinFeeTx, and calcMinFeeTxNativeScriptWits with no version. All three are
correct today (cardano-ledger-core Cardano/Ledger/Tools.hs:11-13), so this is
purely about symmetry: these identifiers rot the same way the Step 4 ones do and
carry no marker for a future reader or refresher.
Suggested fix: add the cardano-ledger-core version alongside them, in the
Step 4 style.
6. NIT — the tightened glob leaves two dangling links inside the bundled corpus
docs/sources/cardano-tx-tools/docs/prior-art.md:46,94
Both link to may-2026-amaru-lattice/index.md, which the new
docs/*.md glob no longer fetches. They resolved at 9b8763f and do not at
c373e79, so this branch created them. Context that keeps it a NIT: dangling
relative .md links are the corpus norm, not an exception — 30 of the 39 sources
that have relative links have at least one dangling (e.g. haskell-nix 24,
mithril 21, cardano-wallet 13), cardano-tx-tools is at 3 of 43, and no
script or CI check enforces link integrity.
Suggested fix: none required; if desired, add docs/may-2026-amaru-lattice/*.md
back or accept the two dangling references as consistent with the rest of the
corpus.
Evidence caveats
- Upstream trees were verified at the pinned revisions:
/code/cardano-tx-tools@7bfe95b,/code/cardano-api@b951a638,/code/cardano-ledger(working copy, unpinned), fresh clones ofbloxbean/cardano-client-lib@4f1ea9candscalus3/scalus@68449438(plus tagv1.0.0), single-file reads ofEmurgo/cardano-serialization-lib@6b42538andtxpipe/pallas@cdee91d, and clean installs of@evolution-sdk/evolution@0.5.12and@meshsdk/core@1.9.1. Those checkouts andnode_modulestrees are reproducible from the revisions recorded here and were retired at COMPLETE. - Two probe claims were not re-executed in this seat: the
cardano-cli conway transaction build --helpflag dump (no localcardano-cli; the bundled wiki reference corroborates only the auto-balancing half) and thecardano-tx-tools#checks.x86_64-linux.unitnix build (accepted from probe-1; the identifiers it evidences were re-read directly from source). scripts/check-pr-policy.pywas not run (live GitHub API; this seat is read-only/offline-by-policy).scripts/validate.pyandscripts/update-doc-counts.sh --checkwere run under anix shellpython and both pass.- auditor-1's suggestion to disclose the authorship relationship to
lambdasistemi/cardano-tx-toolsin the PR description is not verifiable from this seat and remains open for the ticket owner. - No file in the audited tree was modified;
git statuswas clean at start and at finish.