Skip to content

Feat/update platform to latest contract + adding devnet support - #6

Merged
Kammerlo merged 29 commits into
mainfrom
feat/update-platform-to-latest-contract
Aug 14, 2026
Merged

Feat/update platform to latest contract + adding devnet support#6
Kammerlo merged 29 commits into
mainfrom
feat/update-platform-to-latest-contract

Conversation

@Kammerlo

Copy link
Copy Markdown
Member

This PR updates the platform to use the latest Aiken implementation of the registry.
Additionally it gives devnet support. The issue is that the devnet can't be tested end to end right now since a browser wallet is missing.

Kammerlo and others added 20 commits August 11, 2026 08:45
…nfracking, upgrade_multisig)

Load all v0.4.0 validators from plutus.json and rebuild the parameterization
chain: always_fail -> coordination_spend -> protocol_params_mint -> {
programmable_logic_base, programmable_logic_global, unfracking } ->
upgrade_multisig -> issuance_cbor_hex_mint -> registry_spend ->
registry_mint. programmable_logic_base/global and unfracking are now
anchored on the protocol-params policy id (read live from the coordination
datum) instead of a fixed PLG credential, matching the in-place upgradability
model. The rest of deploy() is stubbed with `if (true) return;` pending
Task 2 (datums) and Task 3 (tx assembly).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…view

Review of the bootstrap-tx assembly found: the flat 50% margin on
IssuanceCborHex's min-UTxO permanently burns extra ADA at an
unspendable always_fail address for no benefit (the min-ada figure is
already ledger-exact); the >=160 ADA funding precondition was stale
once that output's ADA was raised; and dryRun/max-tx-size had no
explicit guard. Switch to a small fixed buffer, raise the funding
threshold with updated math, add a dryRun guard, and assert max-tx-size
explicitly instead of relying on it having silently fit.
…mplete-deployment JSON

Review finding: PreprodProtocolDeploymentMintTest.deploy() has dryRun=false and is a plain
@test, so running it submits a real preprod transaction and logs a BootstrapParams JSON that
is silently missing the four v0.4.0 fields (coordinationParams, unfrackingParams,
upgradeMultisigParams, unfrackingRefInput) this file has no wiring for. Disable it rather than
fabricate values, until the file is ported to v0.4.0.
Flips PreviewProtocolDeploymentMintTest.deploy() to dryRun=false and submits the CIP-113
v0.4.0 bootstrap transaction to the local Yaci devnet. Verified on-chain: the three protocol
UTxOs landed at their required addresses, the three withdraw-0 stake registrations
(programmable_logic_global, unfracking, upgrade_multisig) are all present with matching
script-hash credentials, and the coordination datum's five embedded credentials match the
deployed script hashes. Records the emitted BootstrapParams as
docs/deployments/devnet-v0.4.0.json (tx 2209835bd7e0914c9e58b62dc867f3ad4c04d80fa046fa9f174f07304678d75c).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… test

- ProgrammableLogicBaseParams: ignore unknown properties so pre-v0.4.0
  bootstrap JSONs (which still carry the old
  programmableLogicGlobalScriptHash key) deserialize again instead of
  throwing UnrecognizedPropertyException. Document that
  protocolParamsPolicyId is null for those legacy entries rather than
  silently renaming the stale key to a value it doesn't mean.
- Remove the dead, hazardous registerAddress() test from both preview
  and preprod deployment-mint tests: it targets a stake credential none
  of this branch's withdraw-0 hashes match, has no assertions, and its
  allWalletUtxos.get(2) access can throw under the new >=2-UTxO
  precondition; drop the now-unused QuickTxBuilder import in both files.
- Correct the stale "wallet-split fallback" rationale in
  DevnetFundingTest's funding comment now that deploy() registers all
  three withdraw-0s inline instead.
…ce, handoff file

- skip reward accounts already registered on chain: upgrade_multisig is
  parameterised only by (signers, threshold), so redeploying with the same
  signer set collided with StakeKeyRegisteredDELEG and rejected the whole tx
- derive the coordination nonce from the bootstrap UTxO so each deployment
  gets its own coordination address instead of all sharing one
- write BootstrapParams to build/bootstrap-params.json (gradle swallows
  log.info, and this record is the deployment's only durable output)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…otstrap

WP-1: add protocol-bootstraps-devnet.json, populated from the fresh, verified
devnet deployment (build/bootstrap-params.json, tx 96343b91...ec015d). Make
ProtocolBootstrapService fail fast at load time, naming the exact missing
v0.4.0 component(s), instead of silently handing out a record with null
limbs that NPEs at first use. protocol-bootstraps-{preview,preprod}.json are
deliberately left incomplete (pre-v0.4.0 deployments) rather than fabricated.

WP-2: fix three ProtocolScriptBuilderService builders to match plutus.json's
validators[].parameters:
  - programmable_logic_base: bare protocol-params policy id ByteArray
    (was Constr(1, PLG hash) — wrong shape AND wrong value; this hash is the
    payment credential of every programmable-token UTxO)
  - registry_mint: add 3rd param registry_spend_cred (Credential)
  - issuance_mint: add 4th param plg_stake_cred (Credential)

Added a unit test (no chain, no network) that re-derives programmable_logic_base,
programmable_logic_global, registry_mint, registry_spend, issuance_cbor_hex_mint
and protocol_params_mint from the devnet bootstrap JSON and asserts each against
the hash recorded there. Verified the test fails against the pre-fix builders
(2/7 cases) before restoring the fix. issuance_mint has no recorded ground-truth
hash in the bootstrap record (its 3rd param is a per-token substandard script,
not a protocol constant), so it gets a smoke assertion only, documented as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ProtocolScriptBuilderServiceHashDerivationTest: replace the vacuous
  issuance_mint smoke assertion (any applied-parameter count yields a
  well-formed 56-hex-char hash, so it never exercised the plg_stake_cred
  fix) with a real check — split the applied script body on the
  substandard credential and assert PLG's hash lands in the postfix.
  Verified this now fails against the pre-fix 3-param builder alongside
  the two existing PLB/registry_mint failures. Also corrected the class
  Javadoc: it named the wrong deployment tx as this record's corroboration.
- ProtocolBootstrapService.requireV040Complete: extend from 5 to all 13
  ProtocolBootstrapParams components so a truncated/hand-edited bootstrap
  file (not just the two known-legacy ones) fails at load time by name
  instead of NPEing downstream.
- ProtocolScriptBuilderService: add the missing contractOpt.isEmpty()
  guard to the programmable_logic_base builder, matching every sibling
  builder, so a renamed/missing blueprint entry fails with a named
  IllegalStateException instead of NoSuchElementException.
- AppConfig.Network: add a "devnet" case (network id 0, magic 42 — this
  project's established local Yaci devnet convention) to
  getCardanoNetwork(). Without it, network=devnet silently fell through
  to Networks.mainnet(), so every address derived against the new
  protocol-bootstraps-devnet.json would be mainnet-shaped while the
  actual deployment lives at addr_test1.../stake_test1... — the same
  "valid-looking hash, wrong address" failure mode WP-2 fixed, at the
  network-tag layer instead of the script-parameter layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every script parameterisation, datum shape and the hash-derivation test on this
branch is written against this blueprint; leaving it uncommitted meant the branch
did not build from a fresh clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (WP-3/WP-4)

RegistryNode grew from 5 to 7 fields on-chain (lib/registry_node.ak): minting_logic_script
at index 2 and unfracking_logic_script at index 5 are new, and every credential field now
carries its Aiken constructor tag (VerificationKey vs Script), not just a bare hash. The old
Java model wrote a 5-field constr (rejected outright by v0.4.0's registry_mint) and, on read,
silently shifted fields into the wrong names while always emitting Script(...) even for the
unset "empty_vkey" default that registry_mint's whole-record equality check requires to be a
VerificationKey.

- Add PlutusCredentialCodec: bidirectional Credential <-> PlutusData, keeping the constructor
  tag as part of the value instead of collapsing it to a bare hash.
- Rework model/onchain/RegistryNode + RegistryNodeParser (and the duplicate parser,
  model/DirectorySetNode) to the 7-field shape. The parser now walks the deserialized
  PlutusData graph directly and throws on any arity/shape mismatch instead of defaulting a
  missing field to a plausible-looking value.
- Extend RegistryNodeEntity/DTO with mintingLogicScript and unfrackingLogicScript; update
  RegistryEventListener and RegistryService.deleteOrphanedNodes to populate them.
- Flyway V12: existing registry_node rows are silently wrong (not merely incomplete) under
  the old parser, so the migration clears them rather than leaving corrupt data queryable,
  then adds the two columns. Does not by itself replay chain history — the SQL comments spell
  out the manual yaci-store cursor rewind an operator needs, since no automated reindex
  trigger exists in this codebase today. Added RegistryReindexHealthCheck to surface that gap
  at startup instead of the /registry/* endpoints silently returning zero tokens.
- Add protocol-params-devnet.json (transaction-id allowlist), previously missing for the
  devnet network profile — without it protocol_params never indexes, which starves
  registry_node regardless of any reindex.
- Minimal compile-only fixes to the 5 substandard handlers (WP-5 scope, not ported here):
  wire the already-computed minting/transfer script hashes into their new named slots,
  leave third-party/unfracking at the "forbidden" empty_vkey default with WP-5 TODOs, except
  freeze-and-seize where its own code comment ("Issuer to be used for minting/burning/sieze")
  justifies reusing the admin script for both slots.

Round-trip tests assert the origin node's serialized form is byte-identical to
PreviewProtocolDeploymentMintTest's known-good (on-chain-accepted) datum, and that 5/6-field
constrs are rejected rather than silently accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
issuance_mint's redeemer is now types.MintingRegistryProof directly: the
SmartTokenMintingAction { minting_logic_cred, minting_registry_proof }
wrapper is gone from the blueprint, because the credential became the
validator's compile-time parameter and is pinned to the policy id by
registry_mint's is_programmable_token_id_valid. Every site now emits the
bare proof — Constr 1 [Int] (OutputIndex) on the registration path,
Constr 0 [Int] (RefInput) on subsequent mint and burn.

RegistryInsert's second field is a Credential rather than a bare
ByteArray, so the substandard hash is wrapped as Script(hash) = Constr 1.

The two WP-3 TODO slots get real values. third_party_transfer_logic_script
(index 4) cannot be empty — is_28_byte_credential rejects it on a
non-origin node — and it is the sole gate on ThirdPartyAct seizure via
validate_3rd_party's has_key_or_fail on tx.withdrawals. So:

  freeze-and-seize -> issuer_admin_contract (its declared seize authority)
  security-token   -> third_party_transfer_logic_validator, newly wired
                      through a SecurityTokenScriptBuilderService builder
  dummy, kyc,      -> always_fail. None of the three declares a
  kyc-extended        third-party validator, and their issuer-admin
                      scripts authorise issuance, not seizure; pointing
                      index 4 at an unsatisfiable script forbids seizure
                      structurally instead of inventing an authority.
                      Mutable later via is_field_updated_registry_node.

unfracking_logic_script (index 5) stays empty_vkey in all five — unfracking
forbidden, least permission, and no substandard declares a hook.

Also fixes DummySubstandardHandler.buildMintTransaction looking up
"issue.issue.withdraw", which does not exist in the dummy blueprint
(the validators live in transfer.ak), so every dummy mint died on an
unchecked Optional.get().

Verified end-to-end on the yaci devnet against the live v0.4.0 deployment
96343b91: registration tx 1498366a (block 13290) wrote a 7-field node with
third_party = Script(always_fail) and unfracking = VerificationKey(#""),
re-emitting the covering node under whole-record equality; subsequent mint
tx 61f0dd7c (block 13338) exercised the RefInput branch. Both accepted by
the node, not just the local evaluator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… at 7ae4ce3

Replace the vendored fn-bafin-cardano-sc fork with a byte-identical copy of
FluidTokens/fn-bafin-cardano-sc @7ae4ce3db39a. The fork's deliberate 2<->3
registry-node index swap is gone: upstream's natural 2/3/4 ordering is already
exactly v0.4.0's RegistryNode layout, so the swap became the thing that would
break it.

Blueprint: upstream's committed plutus.json is used verbatim and was proven not
stale — rebuilding its own sources with aiken v1.1.21+42babe5 (the compiler its
preamble records; note its aiken.toml declares v1.1.22) reproduces every
validator hash and compiledCode byte-for-byte. No recompile, so no compiler
divergence to record. Every deployed validator hash except denylist spend
changes versus the fork, so any previously stored security-token script hash,
policy id or address is stale.

Handler port. Upstream is ahead of the fork, and the deltas are silent ones:

  * GlobalStateDatum 9 -> 11 fields. requires_sender_kyc is INSERTED at index 8
    (pushing requires_receiver_kyc 8 -> 9) and network_id appended at 10.
    buildTransferTransaction read the live receiver-KYC flag at index 8, which
    under upstream is requires_sender_kyc — it gated recipient proofs on the
    wrong field. Datum length checks are now == 11, not >= 9, so a pre-pin
    global state is rejected with an explicit "must be re-bootstrapped" error
    instead of being misread.
  * GlobalStateSpendAction constructors shift: SetRequiresSenderKyc inserted at
    6, SetRequiresReceiverKyc 6 -> 7, UpdateMemberRootHash 7 -> 8, RotateAdmin
    8 -> 9. Emitting a stale index still builds a valid tx, it just performs a
    different action.
  * Denylist add/remove redeemers gain power_user_node_ref_input_index, and the
    sanction gate moves from the GS admin credential to a power user holding
    is_admin who must sign. The power-user node is attached as a second
    reference input and BOTH ref-input indices are now derived from the
    lex-sorted (txHash, outputIndex) order rather than a hardcoded 0.
  * network_id moved out of the compile-time env module into the datum, so the
    off-chain side now owns it (kycNetworkId()).

New replaceGsField() mirrors upstream's utils.replace_data_field and now backs
all eleven "preserve the datum, change one field" sites; upstream compares the
whole output datum with equals_data, so one dropped field silently invalidates
the transaction.

Pinning. UPSTREAM_PIN.json records the SHA plus a sha256 per vendored file;
SecurityTokenUpstreamPinTest enforces it offline on every build and also asserts
the backend's own resources copy of plutus.json is byte-identical to the
vendored blueprint (aiken build does not sync it, and the backend loads it);
verify-upstream-pin.sh re-downloads the pinned tarball and diffs, so the
manifest cannot be regenerated over a divergent tree. Both guards were verified
negatively as well as positively. Chosen over a submodule because a submodule
covers neither the resources copy nor a checkout that forgot --recurse-submodules.

Known blocker, reported not worked around: the fork also carried two undeclared
patches upstream does not have — a registration-mode short-circuit in
minting_logic_script.ak and a mint/burn short-circuit in transfer_logic_script.ak.
The platform's registration tx withdraws-0 from the substandard's minting logic,
which upstream answers by deriving the issuance policy id from a registry node
among the reference inputs; at registration time that node is being created in
the same tx. Registration therefore cannot validate as currently shaped. Fixing
it is a platform decision (drop the withdraw at quantity 0, or ask upstream for
a registration branch), so the stale "rubber-stamp" comments were replaced with
accurate BLOCKED notes rather than a guessed restructure. buildMintTransaction
is unaffected — it already references the directory entry and uses the strict path.

Also found: requires_sender_kyc is dead upstream at this SHA —
transfer_logic_script.ak:109 gates the per-SENDER check on requires_receiver_kyc,
so that one flag controls both sides. The field is still written so the datum
matches exactly.

Verified: compileJava + compileTestJava clean; SecurityTokenUpstreamPinTest 2/2
plus two negative cases; verify-upstream-pin.sh green against live GitHub. No
on-chain security-token tx was submitted — there is no JUnit entry point for
that flow and registration is blocked above. The 63 pre-existing suite failures
(Spring/JPA wiring, pre-v0.4.0 bootstrap JSON, chain-dependent and bad-fixture
tests) are unchanged; no failing test touches security-token code.

Note: upstream's own README.md is the one file NOT vendored — this work package
was told never to commit a markdown file, so the conservative reading was taken.
It is documentation only, no compiled artifact depends on it, and the exclusion
is recorded in UPSTREAM_PIN.json and named explicitly in the verify script;
re-adding the file makes the pin test fail with UNEXPECTED README.md. Every
other upstream file is byte-identical with no exclusions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Registry-node slot 4 is the credential CIP-113's ThirdPartyAct branch
demands a withdrawal from (registry_node.with_key_and_3rd_party_logic →
programmable_logic/third_party.ak `has_key_or_fail`), and
buildBurnTransaction takes that branch. WP-5 put the BaFin
third_party_transfer_logic_validator there while the burn tx only
withdraws mintingLogic + programmableLogicGlobal, so every
security-token burn failed Plutus evaluation.

The preferred fix — withdraw from the third-party validator — is not
reachable at the pinned upstream commit:
security-token/validators/third_party_transfer_logic_script.ak:44 passes
`constants.transfer_logic_script_registry_node_index` (= 3) rather than
`..._third_party_..._index` (= 4), so the validator asserts that registry
field 3 holds its OWN hash. Field 3 must hold transferLogic for
validate_transfer, so the third-party validator can never self-locate in
a node that also supports transfers. The vendored tree is verbatim-pinned
(FluidTokens/fn-bafin-cardano-sc 7ae4ce3) and must not be edited.

Slot 4 therefore holds mintingLogic — the one vendored withdraw validator
that self-locates correctly (index 2) and that the burn tx already
withdraws. It still gates on a power-user node, so the branch stays
authority-gated, though on can_burn rather than can_force_transfer and
without the destination KYC/denylist checks. Same slot choice as
FreezeAndSeizeHandler. Rewrites the stale burn-path comment that claimed
field 3 held mintingLogic.

Also:
- IssueTokenTest: port the three issuance_mint parameterisations to the
  v0.4.0 four-parameter form, the directory datums to the 7-field
  RegistryNode (minting/transfer slots were swapped), and mintOnly() to
  a RefInput proof against a real registry reference input.
- freezeandsieze PreviewMintTest: MintingRegistryProof is now the
  redeemer directly; add the missing registry reference input.
  PreviewWipeTest's dead twin fixed and marked as unported.
- DummySubstandardHandler: filter a null scriptHash before it reaches
  Credential.fromScript.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ift guard

WP-6 code-review follow-ups.

Registration cannot validate at the pinned upstream revision: upstream's
minting_logic_script.ak resolves the CIP-113 registry node with list.expect_find
over the transaction's REFERENCE inputs, but registration creates that node in
the same transaction. It used to trap deep in script evaluation as an opaque
UPLC error — after buildGlobalStateInitTransaction had already persisted a
SecurityTokenRegistrationEntity and a bootstrap power-user row, so every attempt
left orphan rows. buildRegistrationTransaction and buildFullRegistrationChain
now return a typed error naming the blocker before any build or DB write; the
implementations are preserved as *Unchecked for when upstream is fixed.

The offline drift guard claimed to run in every build but did not: the vendored
tree sits outside the Gradle project and was not a declared input, so editing
src/substandards/security-token/** left `test` UP-TO-DATE and
SecurityTokenUpstreamPinTest silently never executed. Declared as a test input.

Also:
- transfer: upstream gates the SENDER kyc check on gs_datum.requires_receiver_kyc
  (the replaced fork checked it unconditionally). The senderMpfProofCborHex
  precondition moved below the live GS read and is gated on that flag, emitting
  the placeholder Membership shape when false — otherwise tokens bootstrapped
  with requiresReceiverKyc=false were untransferable, since nobody can produce a
  proof against an empty member root. Fixed the TTL bound that would then NPE.
- RotateAdmin: global_state.ak needs both the outgoing and incoming admin in
  extra_signatories; buildSingleGsUpdateTx now takes a list of required signers.
  The incoming admin's signature must still be collected by the frontend.
- verify-upstream-pin.sh --regenerate: rsync --delete so a dropped upstream file
  cannot abort step 1 with the tree already replaced; refresh vendored_at.
- kycNetworkId() throws on an unrecognised network instead of baking in mainnet
  0x2, which is immutable after genesis and checked against every attestation.
- Align the pin test's `build` prune with the script's (any depth).
- gitignore .claude/ (holds worktrees: full second copies of the repo).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ps-<network>.json

BOOTSTRAP_OUT points the handoff writer at the resource file directly. The file
is an array keyed by txHash that ProtocolBootstrapService selects from via
programmable.token.default.txHash, so deployments are merged in rather than
overwritten and older ones stay addressable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kammerlo Kammerlo self-assigned this Aug 12, 2026
…hed code

Replaces the inline java.net.http.*, java.nio.file.*, Jackson TypeReference and
cardano-client-lib references this branch introduced with imports. Where an
import was added, all existing uses of that same type in the file were converted
too, so no file is left half-qualified.

com.bloxbean.cardano.client.common.model.Network in AppConfig stays qualified —
the enclosing class declares its own nested Network, so importing it would clash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kammerlo
Kammerlo requested a review from nemo83 August 12, 2026 07:19
@Kammerlo
Kammerlo marked this pull request as draft August 12, 2026 07:19
Kammerlo and others added 6 commits August 12, 2026 13:06
Re-vendors src/substandards/security-token/ verbatim from a NEW upstream
repository, not merely a new SHA:

  was  FluidTokens/fn-bafin-cardano-sc      @ 7ae4ce3db39a
  now  easy1staking-com/fn-bafin-cardano-sc @ e69c66afccee
       (branch fix/cmta-requirements-fixes, which merges FluidTokens:main;
        the SHA is pinned, never the branch)

This fixes all three defects in docs/UPSTREAM-BAFIN-DEFECTS.md:

  A  third_party_transfer_logic_script.ak now passes
     third_party_transfer_logic_script_registry_node_index (4) instead of the
     transfer index (3), so forced transfer / seizure is reachable at all.
  B  derive_issuance_policy_id_from_registry_node takes a
     registry_node_ref_input_index and addresses the node BY INDEX instead of
     list.expect_find over the registry policy alone.
  C  MintingLogicScriptWithdrawRedeemer became a sum type with a RegisterToken
     constructor that resolves the node from outputs, making CIP-113
     registration possible.

Blueprint verified NOT stale against its sources: the committed plutus.json
records compiler v1.1.23+8949565, which is also the locally installed aiken, and
rebuilding the vendored sources reproduces byte-identical compiledCode for all
21 validators. (Note the brief's "v1.1.22, same as before" was wrong on both
counts - the old pin was v1.1.21+42babe5.) `aiken check` on the tree is
143/143 green.

Validator hashes: every validator changed except denylist_validator.spend.

Pin machinery updated for the repository change, not just the SHA - neither
verify-upstream-pin.sh nor SecurityTokenUpstreamPinTest hard-codes an owner any
more; both read it from UPSTREAM_PIN.json, so a future re-pin edits one file.
The pin test's note claiming the local compiler differs from the blueprint's is
now false and was corrected rather than left to rot.

Both blueprints synced (the backend loads its own resource copy; aiken build
does not update it).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…workarounds

ABI port
--------
GlobalStateDatum gained `deactivated` at index 1, shifting mintable_amount to 2
and every later field up by one. This is silent and dangerous - reading
mintable_amount at the old index 1 now yields a Bool - so every GS field index
is now a named GS_IDX_* constant and no call site uses a raw literal.
GS_DATUM_FIELD_COUNT 11 -> 12, and the genesis datum writes the new field.

All three withdraw redeemers gained a leading registry_node_ref_input_index.
Each is computed against the LEX-SORTED reference-input list (reference inputs
are ledger-sorted by output reference before the script sees them), reusing the
existing lexIndex helper.

The minting-logic redeemer is now a sum type:
  - registration uses RegisterToken (constructor 1) with an OUTPUT index
  - mint/burn uses MintBurn (constructor 0) with a REF-INPUT index

Mint destinations are now vetted like transfer destinations, so mints carry a
denylist covering-node reference input they did not need before. Added
findDenylistCoveringNode, which picks the greatest key strictly below the target
rather than always the root - the root only covers everything while the list is
empty.

Undo of workaround 1 (commit dffb3d7)
-------------------------------------
buildRegistrationTransaction / buildFullRegistrationChain no longer return
REGISTRATION_BLOCKED_AT_UPSTREAM_PIN; the guard and the constant are deleted and
the real bodies are restored. Registration is now the structural (quantity 0)
RegisterToken shape: GlobalState REFERENCED, no GS spend, no power-user gate, no
issuance mint - exactly what the zero-mint branch validates.

Registration-carrying-a-first-mint is deliberately NOT built. The contract
supports it, but it layers the whole MintBurn gate on top: GS spent under
MintSecurity, a can_mint power user as a reference input that also signs, and a
denylist covering node per destination. No power user exists on chain at
registration time (AddPowerUser is a later tx), so the shape is unreachable for
a first-time issuer anyway. It returns a typed error saying so rather than
emitting a redeemer whose unset indices would trap in the evaluator.

Undo of workaround 2 (commit 0ec401a)
-------------------------------------
Registry-node slot 4 holds the substandard's real
third_party_transfer_logic_validator again instead of mintingLogic.

buildBurnTransaction therefore had to change: CIP-113's ThirdPartyAct requires a
withdrawal keyed on whatever slot 4 names, and the burn can no longer free-ride
on the mintingLogic withdrawal it needed anyway. It now also withdraws from the
third-party validator.

That withdrawal is UNVERIFIED on chain and carries a known risk, recorded here
rather than discovered later: minting_logic's burn path reads GlobalState from
self.inputs (it must be SPENT so MintSecurity enforces the cap), while the
third-party validator reads it from self.reference_inputs. The one GS UTxO is
therefore placed in both lists. Babbage forbade that overlap; Conway permits it.
If the burn is ever rejected for non-disjoint reference inputs, that is the
reason, and the two requirements are genuinely irreconcilable in one tx. The
burn also now needs a power user holding can_force_transfer in addition to
can_burn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gradle discards the test JVM's streams, so log.info and System.out from a test
are invisible — including the deployment test's BootstrapParams and derived
script hashes. --info does not help; only testLogging.showStandardStreams does.
Opt-in rather than always-on so the 116-test suite stays readable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wallet order is arbitrary. On a faucet-funded preview account most entries are
small change, so taking the first two failed the >=175 ADA precondition with the
funds sitting right there. Utxo is not Comparable, so an unqualified sorted()
would have thrown at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l registration

buildFullRegistrationChain passes the genesis tx's GlobalState output into
buildRegistrationTransaction, but the override was only consulted when
willMint. The chain registers structurally (quantity 0), so willMint is false
and the code fell through to a chain lookup for an NFT minted by a transaction
that has not been submitted yet — failing with 'run the genesis init step
first' while holding the UTxO it needed.

Prefer the override in both branches, matching buildAddPowerUserTransaction.
AddPowerUser only readFrom()s GlobalState, so the genesis output is still the
live one at registration time.

Also drops a stale BLOCKED note (resolved by the @e69c66a re-pin) and the
TransactionUtil FQNs added after the import refactor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…egister its stake cred

The genesis init tx was 18027 / 16384 bytes. 7211 of those were minting_logic
attached inline purely so a Cert redeemer could run its publish handler.

Conway's RegCert carries no witness requirement, so that handler never runs and
the script need not be attached. Verified empirically by the protocol
deployment, which registers three script stake credentials with a bare
registerStakeAddress and no validator attached -- including upgrade_multisig,
which has no publish handler at all (else(_) fail) and could not have
registered if a witness were required.

Drops the attach and the Cert redeemer injection; keeps the RegCert rewrite,
which is still needed for the explicit 2 ADA deposit. Inline script bytes in
this tx go 16653 -> 9442.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kammerlo and others added 2 commits August 13, 2026 15:50
…e script witness

aa2f158 removed the attached minting_logic script but left the preBalanceTx
rewrite from StakeRegistration to RegCert. That is the wrong half: RegCert IS
witness-required for a script credential, so the submission failed with
MissingScriptWitnessesUTXOW.

The two must move together. Keep the legacy StakeRegistration that Bloxbean
emits -- still valid in Conway, requires no witness, no script attached, and
7211 bytes smaller. This matches the protocol deployment, and all nine
script-credential registrations currently on preview are STAKE_REGISTRATION;
none is a RegCert.

The PHASE 4 transferLogic cert tx keeps RegCert + attached script + Cert
redeemer, which is internally consistent and left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kammerlo
Kammerlo marked this pull request as ready for review August 14, 2026 13:02
@Kammerlo
Kammerlo merged commit 61374e3 into main Aug 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant