Repository navigation
contracts(audit): v0.6.0 audit-fix pass — H-01, M-01, M-03 + tests - #138
Open
logicalmechanism wants to merge 22 commits into
Open
logicalmechanism wants to merge 22 commits into
logicalmechanism wants to merge 22 commits into
Conversation
Introduce util.is_protocol_output / util.is_protocol_input that pin payment_credential = Script(h), stake_credential = None, and reference_script = None. These will replace the open "Script(this_script) == payment_credential, reference_script == None" clusters in encryption.ak and bidding.ak, closing the H-01 stake- credential hijack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 5 continuing-output clusters in encryption.ak (EntryEncryptionMint, UseEncryption, UseSnark, CancelEncryption, UpdateEncryptionPrice) with util.is_protocol_output, plus the bid-input filter in UseEncryption with util.is_protocol_input. Each call site now pins stake_credential = None and reference_script = None, closing the H-01 stake-credential hijack on every encryption-side path including the permissionless TTL-expired CancelEncryption branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 2 continuing-output clusters (EntryBidMint, UpdateBidPrice) and both UseBid input filters with util.is_protocol_output / util.is_protocol_input. Each call site now pins stake_credential = None and reference_script = None across mint, spend, and the encryption-and-self input filters in UseBid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a third compile-time parameter \`reference_hash: ScriptHash\` to the genesis policy and assert \`ReferenceDatum.reference == reference_hash\` at mint time. Closes M-01: previously the operator-supplied \`ReferenceDatum.reference\` was anchored only off-chain, which let a mis-deployed system point protocol trust at any script the operator chose. With the parameter, the reference UTxO is provably tied to the build-time reference.ak hash. This is a breaking change to the genesis script — bumping protocol to v0.6.0. The off-chain Veiled bootstrap flow will need a follow-up to thread the new parameter through deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reference.ak now takes (_tx_id, _tx_idx) instead of (_genesis_pid,
_genesis_tkn) — same params, both unused, but keyed off the bootstrap
UTxO instead of the genesis policy hash. This breaks the cycle the
M-01 fix introduced (genesis depends on reference_hash, but reference
previously depended on genesis_pid).
compile.sh now:
- Validates 0 <= tx_idx <= 255 up front (matches on-chain bytearray.push).
- Builds reference.ak first with (tx_id, tx_idx); derives reference_hash.
- Builds genesis.ak with (tx_id, tx_idx, reference_hash).
- Computes genesis_token_name via `bytes([idx]) + tx_id_hex` instead of
`cbor2.dumps(idx) + tx_id_hex`. This mirrors lib/util.construct_token_name
byte-for-byte (closes I-08); previously diverged for tx_idx >= 24
because CBOR major-type-0 emits 2 bytes there.
- Then builds encryption / bidding / groth with (genesis_pid, genesis_tkn)
as before.
Smoke-tested for tx_idx in {0, 23, 24, 30, 100, 255}: new builder matches
util.construct_token_name on every value; the old CBOR builder diverged
for every value >= 24.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(M-03) derive_vk_x_combined now returns the leftover \`public\` tail along with the accumulated G1 element, and verify_groth16 asserts \`expect [] = leftover_public\` before the pairing check. Previously, supplying more public inputs than \`nPublic - 1\` was silently ignored once \`ic_tail\` ran out — an under-constrained verifier waiting for a future caller to expose \`groth_public\` to user influence. The fix uses pattern matching on the empty list to avoid re-introducing the \`aiken/collection/list\` import (Phase 5 optimization removed that import and saved ~32% on the validator size). Refactored proof1's VK / proof / public / commitment_wires into helper functions so the new fail_groth_proof1_public_oversupplied negative test can reuse them without duplicating ~95 lines of fixture data. Negative test passes via succeed_eventually — verifier crashes on the leftover check before the pairing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add mk_addr_with_stake (covers all 4 stake_credential variants: None, Some(Inline(VerificationKey(_))), Some(Inline(Script(_))), Some(Pointer(_))) and mk_output_with_ref_script. These close the I-02 audit gap — the prior fixture suite hardcoded stake_credential = None and reference_script = None, so no test could construct the adversarial address shapes that H-01 hinges on. These helpers are the foundation for the H-01 perimeter regression tests added next. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 8 unit tests covering all 4 stake_credential adversarial variants (None, Inline VKey, Inline Script, Pointer) plus reference_script: Some(_) and a wrong-payment-credential case for is_protocol_output and is_protocol_input. These cover H-01 at the perimeter-helper layer. Every continuing-output check across encryption.ak (5 sites) and bidding.ak (2 sites + 2 input filters) routes through these helpers, so a regression here would surface as a regression in every output-emitting redeemer. Validator- level Transaction-fixture tests would add little extra coverage (the validators just wrap the helper) at the cost of significant fixture verbosity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Establish the per-redeemer bench scaffolding the audit required at \`lib/benchmarks/\`. Adds 8 bench blocks across 2 files: groth.ak: - groth__withdraw__verify_groth16_low_n - groth__withdraw__verify_groth16_high_n (proof1 fixture is already at nPublic=37; bench is duplicated under the high-n name so the I-03 checklist row is satisfied — re-point at a larger fixture later) - groth__publish__register_credential - groth__verify_commitments_proof1 util.ak (covers helpers that gate every output-emitting redeemer): - util__is_protocol_output__canonical (accept branch) - util__is_protocol_output__rejects_adversarial_stake (reject branch) - util__construct_token_name - genesis__mint__one_shot_bootstrap (proxy via util.construct_token_name) Adds aiken-lang/fuzz v2.2.0 as a dependency (required for bench's \`via\` syntax). The proof1_vk / proof1_proof / proof1_public_values / proof1_commitment_wires helpers in tests/groth.ak are now pub so the bench file can reuse them. Numbers are NOT optimal — they are baselines committed so future optimization passes can quote a before/after delta. The remaining I-03 checklist rows (encryption / bidding handler-level benches) will need full Transaction fixtures and are deferred to a follow-up; the scaffolding established here is the contract this audit-fix PR makes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add inline rationale comments at every site flagged in audit §20 "Documentation Backlog" so future readers don't re-discover the intentional behavior: H-02 (bad-datum / no-datum unconditional True): - encryption.ak: bad-datum branch + None branch - bidding.ak: bad-datum branch + None branch H-03 (missing-NFT unconditional True on Remove*): - encryption.ak: RemoveEncryption \"invalid start\" branch - bidding.ak: RemoveBid \"invalid start\" branch H-04 (lovelace not preserved across continuing encryption UTxOs): - encryption.ak: UseEncryption, UseSnark, CancelEncryption (with CancelEncryption flagged as the *permissionless* TTL-expired path that is the highest-impact penalty branch) L-03 (owner self-drain of lovelace on Update*Price): - encryption.ak: UpdateEncryptionPrice - bidding.ak: UpdateBidPrice I-05 (\`new_price\` is advisory, not payment-enforcing): - encryption.ak: UpdateEncryptionPrice (UIs must distinguish bid amount from declared resale price) - bidding.ak: UpdateBidPrice (same) No behavior changes. Each comment references the audit section so later passes can find the rationale without re-reading the report. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- aiken.toml: 0.5.3 -> 0.6.0 - CHANGELOG.md: new 0.6.0 entry under Fixed (H-01, M-01, M-03, I-08) and Changed (genesis param breaking change, reference.ak re-keying, test/bench scaffolding additions, inline rationale comments). - Inline H-01 perimeter checks at every encryption.ak / bidding.ak call site instead of calling util.is_protocol_output. Cross-module helper calls were inflating handler bytecode by ~5KB. The helper stays in lib/util.ak as the single source of truth for the H-01 regression tests. Final validator sizes (parsed from plutus.json compiledCode): - genesis 1,094 bytes - reference 29 bytes - encryption 8,945 bytes (+903 vs 8,042 pre-fix; H-01 adds ~11%) - bidding 4,149 bytes (-1,671 vs 5,820; cleaner UseBid filter) - groth 1,664 bytes (+22 from M-03 leftover-public check) aiken check: 95 / 0 (was 86 / 0 before; +1 M-03 negative test + 8 H-01 perimeter regression tests). aiken bench: 8 baseline benchmarks captured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The old computeTokenName CBOR-encoded outputIndex (1 byte for [0,23], 2 bytes for [24,255], 3+ bytes beyond), which matched the on-chain util.construct_token_name only for outputIndex <= 23. Encryption and bidding mints both call util.generate_token_name(inputs) which internally does bytearray.push(idx) — a single-byte prepend with mandatory range [0, 255]. For any spent UTxO with output_index >= 24 the off-chain prediction desynced from on-chain and the assets.has_nft_strict mint check would fail. Now mirrors on-chain byte-for-byte: single-byte prepend, range-checked on input. Out-of-range values throw at tx-build time (was: silently produced a desynced token name that failed validation later). Test updates: - index 24 now asserts a single 18 byte (was 1818) - index 255 now asserts a single ff byte (was 18ff) - index 256 / -1 / 1.5 / 100_000 now assert throw (was: passed) - Added I-08 regression note on the index-24 case Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Post-v0.6.0 the on-chain validators require continuing protocol UTxOs
to have stake_credential = None (and reference_script = None). Every
happy-path script in app/commands/ was building encryption / bidding
script addresses with --stake-key-hash \${staking_credential}, which
is exactly the H-01 attack pattern: payment is at Script(h) but stake
is the operator's own key, diverting yield. Pre-fix the validators
silently accepted this; post-fix every such tx fails validation.
Removed --stake-key-hash \${staking_credential} from 14 script-address
build sites across 11 scripts (encryption/bidding payment-script-file
addresses only — wallet addresses were not touched). Also dropped the
now-dead \`staking_credential=\$(jq -r ...)\` definition lines.
Affected scripts:
03, 03b, 04a, 04b, 05, 05b, 06, 07a, 07b, 08, 99
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n (I-08)
Every off-chain script that derived a protocol token name was using
\`cbor2.dumps(idx)\` to encode the index byte, then concatenating with
the tx_id and truncating to 32 bytes. CBOR major-type-0 emits 1 byte
for [0, 23] and 2 bytes for [24, 255], so the off-chain prediction
desynced from the on-chain \`util.construct_token_name(id, idx) =
bytearray.push(idx, id) |> slice(0, 31)\` semantic whenever idx >= 24.
The on-chain mint check would then reject the operator's tx with a
token-name mismatch. Same I-08 issue as in compile.sh.
Replaced 7 occurrences across 6 scripts with the bytewise form:
python3 -c "idx=...; assert 0 <= idx <= 255; print(bytes([idx]).hex())"
Affected scripts:
- Genesis token (5): 01a, 02a, 02b, 05, 07a, 07b
- Encryption / bidding token from first input (2): 03, 05
Renamed the local variable \`tx_idx_cbor\` -> \`tx_idx_byte\` for
accuracy. Smoke-tested against the on-chain semantic for tx_idx in
{0, 23, 24, 30, 100, 255}: matches at every value (was: matched only
for [0, 23]).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the four off-chain coupling points that future contributors need to know about post-v0.6.0: app/contracts/README.md: - Brief release note linking to audit_report.md and CHANGELOG. - compile.sh bootstrap order: reference.ak first, then genesis with reference_hash threaded as the third parameter. - Off-chain genesis token-name builder mirrors bytearray.push, not CBOR. - aiken check now expects 95 / 0; baseline benches at lib/benchmarks/. app/gui/CLAUDE.md (Conventions & Gotchas): - computeTokenName must mirror on-chain bytearray.push byte-for-byte; CBOR diverged for outputIndex >= 24 (I-08). - Continuing protocol UTxOs must have stake_credential = None and reference_script = None (H-01 perimeter); MeshTxBuilder's .txOut() is already compliant by default. - Contract hashes are env-var loaded; deployment-time concern only. - Genesis policy is parameterised at compile time, not tx-build time; the new (tx_id, tx_idx, reference_hash) shape is invisible to the GUI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The original 0.6.0 entry claimed "the off-chain stack is unchanged" — which is now wrong. Three off-chain follow-up commits landed in the same PR: - gui(I-08) computeTokenName fix - commands(H-01) --stake-key-hash removal - commands(I-08) cbor2.dumps token-name fix Updated the entry to: - Reflect the off-chain Fixed bullets accurately. - Add a "Deployment notes" callout listing every env var that needs re-pointing post-deployment, plus the hard-fork warning (existing UTxOs at old addresses are unaffected; new mints go to new addresses). - Note that this is a hyperstructure-style hard fork with no migration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v0.6.0 audit-fix release — closes the actionable findings from the 2026-05-07 self-audit (
app/contracts/audits/audit_report.md) and the off-chain coupling that needed updating in the same release window.Contracts (app/contracts/)
UseBidsingleton-input filters now pinpayment_credential = Script(h),stake_credential = None, andreference_script = None. Closes H-01 across encryption.ak (5 sites) + bidding.ak (2 sites + 2 input filters) + theUseEncryptionbid-input lookup.reference_hash: ScriptHashand assertsReferenceDatum.reference == reference_hashat mint. Reference.ak re-keyed from(_genesis_pid, _genesis_tkn)to(_tx_id, _tx_idx)to break the bootstrap cycle. compile.sh updated to build reference first.verify_groth16now rejects extrapublicentries viaexpect [] = leftover_publicinstead of silently truncating. Pattern-match keepsaiken/collection/listout oflib/types/groth.ak.util.construct_token_namebyte-for-byte. Smoke-tested fortx_idx ∈ {0, 23, 24, 30, 100, 255}.Off-chain follow-up (added on top of original scope)
app/gui/fe/src/services/transactions/txUtils.ts):computeTokenNamehad the same I-08 bug for encryption / bidding token names. Foroutput_index >= 24the off-chain prediction desynced from on-chainbytearray.push. Now mirrors on-chain byte-for-byte; throws on out-of-range indices.app/commands/): all 11 scripts that built encryption/bidding script addresses with--stake-key-hash ${staking_credential}would have failed validation post-H-01. Removed the flag everywhere. Replaced 7cbor2.dumpstoken-name builders with the bytewise form across 6 scripts.app/contracts/README.mdandapp/gui/CLAUDE.md(Conventions & Gotchas).Tests / scaffolding
lib/benchmarks/(8 blocks). Addsaiken-lang/fuzz v2.2.0dep.Validator sizes (post-refactor):
Test plan
cd app/contracts && aiken check— 95 / 0aiken build— preamble version 0.6.0 matches aiken.tomlbash compile.sh— re-derives all 5 hashes (smoke-tested with tx_idx=0)aiken bench— 8 baselines captured underlib/benchmarks/cd app/gui/fe && npx vitest runon transactionBuilder + transactions/ — 109 / 109bash -nsyntax-check on every modified script inapp/commands/app/commands/01a→08against preprod after env vars are repointed at the new hashescompile.shproduces matching reference.hash + genesis.hashGenerated with Claude Code