Skip to content

Release: develop -> main - #223

Draft
github-actions[bot] wants to merge 3 commits into
mainfrom
develop
Draft

Release: develop -> main#223
github-actions[bot] wants to merge 3 commits into
mainfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Automatic Release PR

Commits: 3 new commit(s)

  • Review all changes
  • Verify CI passes
  • Merge when ready for production

* feat(multi-asset): neutral permissionless minting — no native coin, no minting authority

zkCoins becomes a fully neutral protocol: there is no native/official coin and
no central minting authority. Anyone creates their own asset and mints unlimited
supply of THEIR OWN asset; nobody can forge or inflate a foreign one.

Circuit & types (program-plonky2, script-plonky2, shared):
- AccountState gains an `asset_id` field (per-(owner, asset) model); hash is now
  H(owner || balance || pubkey || asset_id).
- calculate_asset_id uses a fixed-width preimage
  Poseidon(genesis || creator_pubkey || name_hash || decimals), re-derivable
  in-circuit; add calculate_name_hash. Remove NATIVE_ASSET_ID and MINTING_ADDRESS;
  asset_id is now a required field.
- Issuer-gated mint exception: value creation is granted only when, in-circuit,
  asset_id == calculate_asset_id(creator_pubkey, name_hash, decimals)
  AND owner == H(creator_pubkey) AND creator_pubkey == account.public_key.
  Thread MintWitness through the prover.

Node:
- Remove the node-held minting account / MINTING_ADDRESS bootstrap.
- Accounts keyed per (owner, asset_id); receive routes by (recipient, asset_id);
  per-asset balance + GET /api/balance/:address aggregation. Migration 0017
  (composite owner||asset_id key, genesis reset; history stays owner-keyed).
- Mint is now two-phase like send: MintRequest{creator_pubkey, name, decimals,
  amount, signature, timestamp}, verify_mint_signature_pub, proof built on the
  creator's own (owner, asset_id) account.
- SOUNDNESS GATE: at mint commit ingest, enforce commitment.public_key ==
  account_state.public_key (commitment_binds_account_state). The circuit verifies
  no signature, so this off-circuit check ties the on-chain commitment to the
  asset's creator key and prevents forged inflation of a foreign asset.
- Capabilities.multi_asset = true.

* fix(migration-0017): redefine the live accounts_history_capture trigger fn

Migration 0010 renamed account_history_capture() to
accounts_history_capture() (plural) and re-pointed
accounts_history_trigger at the new name. 0017's owner-prefix redefinition
targeted the obsolete singular name, so the live trigger kept writing the
full 64-byte composite accounts.address into account_history and every
account upsert with a composite key failed the 32-byte
account_history_address_length CHECK (caught by
test_load_from_pg_rejects_corrupted_blob once the heavy suite ran).

Replace the plural (live) function instead, preserving 0010's full body —
the zkcoins.request_log_id GUC read and the triggering_request_log_id
column — and only swapping NEW.address for its 32-byte owner prefix.

* fix(mint): rotate next_public_key on issuer mint + heal the test suite

The issuer mint declared next_public_key == creator_pubkey ('no
rotation'). That broke the entire create->mint->send flow: the global
commitment SMT is keyed by sha256(public_key) and is insert-only
(idempotent only for identical values), while the recursive
AccountUpdate copy-constrains the inner proof's next_public_key to the
follow-up transition's current key. A post-mint send was therefore
forced to re-commit under the creator key and died at ingest with
'Key already exists in the tree with different value' (witnessed as
the 'Partition ... set twice' conflict when a fresh key was tried
instead). Never caught: draft PRs skip CI and every account_node test
funded senders via raw imported balances, which the new circuit
(correctly) rejects.

- MintRequest gains next_public_key (compressed secp256k1, hex); bound
  through the two-phase flow (the wallet countersigns the staged proof
  that committed to it), mirroring the send shape.
- prepare_mint threads it into prove_initial_with_in_and_out_coins
  (all slots inactive) — the Initial-branch entry point with an
  explicit rotation target. The commitment itself stays signed by the
  creator key (the issuer gate demands account.public_key ==
  creator_pubkey), so the soundness gate is unchanged.
- account_node_tests: replace every provenance-less
  'import_account { balance, proof: None }' fixture with a real
  issuer mint via the new mint_funded_asset helper (prove -> creator-
  signed commitment -> state advance -> commit_mint); test_asset_id()
  now returns the DERIVED asset id for the fixture creator key so the
  issuer gate admits it. New test pins the re-mint rejection arm.
- router_tests: creator-signed mint bodies (signed_mint_body /
  signed_mint_request) for the jobs admit suite; seed_account_history
  writes the 64-byte owner||asset_id composite key (0017 CHECK).
- self_heal_tests: composite-key seeding for the same reason.

Verified locally: account_node::tests 28/28, the previously failing
router/self_heal/jobs classes 50/50, cargo fmt --check, clippy clean in
all three CI scopes, node builds (MVP + --all-features).

* revert: unsound mint next_public_key rotation; document create->mint->send blocker

The previous commit rotated the issuer mint's next_public_key to a
fresh wallet key to dodge the commitment-SMT collision on the first
post-mint send. A dedicated soundness test
(commitment_binds_account_state_checks_creator_verifying_key) proves
that is UNSOUND: the circuit commits account_state_hash (ProofData
PI[0]) with the transition's next_public_key, so binding the commit-leg
gate to a rotated key lets a forger sign the mint commitment with their
own rotation key and inflate/forge a foreign asset. Revert to the
no-rotation mint (PI[0] == creator key), which the gate requires.

The create->mint->send loop is therefore blocked at the circuit layer:
no-rotation is mandatory for soundness, but it forces the creator's
first send to re-commit under sha256(creator_pubkey), which the
insert-only commitment SMT rejects. The two constraints are mutually
exclusive without a circuit change (a dedicated creator_pubkey public
input the gate binds against, decoupled from the rotated state key).
Documented in MULTI_ASSET.md §17.

Kept from the heal: migration 0017 trigger-name fix, the router/
self_heal 64-byte composite-key + creator-signed mint-body test fixes,
and the soundness + MintStore + zero_asset_id coverage tests. The
account_node create->mint->send fixtures remain red on this blocker.

* feat(mint): node-side asset_creators binding + mint key-rotation — close create->mint->send

Per the architecture decision, the per-asset creator binding moves
OUT of the commitment-key check and INTO a node-side mapping table
(MULTI_ASSET.md §5.3 'off-circuit verify'). The Plonky2 circuit is
unchanged.

- Migration 0018: asset_creators (asset_id PK -> creator_pubkey),
  octet_length CHECKs.
- prepare_mint rotates next_public_key to a fresh wallet key (via
  prove_initial_with_in_and_out_coins with all slots inactive), so the
  creator's first follow-up send commits under a fresh key — no more
  insert-only commitment-SMT collision. MintRequest gains
  next_public_key; MintingPrepared/StagedMint carry creator_pubkey.
- mint_flow rejects (409) a mint of an asset_id registered to a
  different creator before proving; mint_commit_flow requires
  commitment.public_key == staged.creator_pubkey and registers the
  asset_id -> creator_pubkey row on success.
- Removed the now-obsolete commitment_binds_account_state /
  proof_account_state_hash (the binding they enforced moved
  off-circuit) and their tests; added db_tests for the new functions.
- Test fixtures use the rotating mint; create->mint->send fixtures now
  pass.

Verified locally: full -p node -p shared --all-features heavy gate
green — 495/495 tests, 100% lines + 100% functions (the CI coverage
gate's --fail-under thresholds). fmt/clippy(3 scopes)/build clean.
MULTI_ASSET.md §17 updated. Migration 0017 trigger fix + the earlier
router/self_heal fixture heal are included on this branch.

* test(history): upsert TxDetail happy-path account under the 64-byte composite key

The staging TxDetail happy-path test (rebased in) mutated the account via
upsert_account_with_source with the raw 32-byte owner address, which trips
the migration-0017 accounts_address_length=64 CHECK. Build the same
owner‖asset_id composite key seed_account_history uses, so the send row
chains onto the mint row and list_account_history(&owner) still resolves.
… before promote

Resolves PR #222 promote-conflict introduced by the docs-cleanup on develop
(#216 removed root design markdowns; node #220 had concurrently touched
MULTI_ASSET.md) and the small program-plonky2/src/circuit/main.rs edit on
both branches.

- MULTI_ASSET.md: respect #216's policy (design docs live in zk-coins/research,
  the multi-asset spec from node #220 archived locally before delete)
- circuit/main.rs: auto-merged
- All other develop deletes (ARKADE/BITVM/BRIDGE/LIGHTNING/MIGRATION/ROADMAP/
  SPEC, program-plonky2 session notes) accepted; staging gets the slim
  CONTRIBUTING.md + README from #216
* feat(multi-asset): neutral permissionless minting — no native coin, no minting authority

zkCoins becomes a fully neutral protocol: there is no native/official coin and
no central minting authority. Anyone creates their own asset and mints unlimited
supply of THEIR OWN asset; nobody can forge or inflate a foreign one.

Circuit & types (program-plonky2, script-plonky2, shared):
- AccountState gains an `asset_id` field (per-(owner, asset) model); hash is now
  H(owner || balance || pubkey || asset_id).
- calculate_asset_id uses a fixed-width preimage
  Poseidon(genesis || creator_pubkey || name_hash || decimals), re-derivable
  in-circuit; add calculate_name_hash. Remove NATIVE_ASSET_ID and MINTING_ADDRESS;
  asset_id is now a required field.
- Issuer-gated mint exception: value creation is granted only when, in-circuit,
  asset_id == calculate_asset_id(creator_pubkey, name_hash, decimals)
  AND owner == H(creator_pubkey) AND creator_pubkey == account.public_key.
  Thread MintWitness through the prover.

Node:
- Remove the node-held minting account / MINTING_ADDRESS bootstrap.
- Accounts keyed per (owner, asset_id); receive routes by (recipient, asset_id);
  per-asset balance + GET /api/balance/:address aggregation. Migration 0017
  (composite owner||asset_id key, genesis reset; history stays owner-keyed).
- Mint is now two-phase like send: MintRequest{creator_pubkey, name, decimals,
  amount, signature, timestamp}, verify_mint_signature_pub, proof built on the
  creator's own (owner, asset_id) account.
- SOUNDNESS GATE: at mint commit ingest, enforce commitment.public_key ==
  account_state.public_key (commitment_binds_account_state). The circuit verifies
  no signature, so this off-circuit check ties the on-chain commitment to the
  asset's creator key and prevents forged inflation of a foreign asset.
- Capabilities.multi_asset = true.

* fix(migration-0017): redefine the live accounts_history_capture trigger fn

Migration 0010 renamed account_history_capture() to
accounts_history_capture() (plural) and re-pointed
accounts_history_trigger at the new name. 0017's owner-prefix redefinition
targeted the obsolete singular name, so the live trigger kept writing the
full 64-byte composite accounts.address into account_history and every
account upsert with a composite key failed the 32-byte
account_history_address_length CHECK (caught by
test_load_from_pg_rejects_corrupted_blob once the heavy suite ran).

Replace the plural (live) function instead, preserving 0010's full body —
the zkcoins.request_log_id GUC read and the triggering_request_log_id
column — and only swapping NEW.address for its 32-byte owner prefix.

* fix(mint): rotate next_public_key on issuer mint + heal the test suite

The issuer mint declared next_public_key == creator_pubkey ('no
rotation'). That broke the entire create->mint->send flow: the global
commitment SMT is keyed by sha256(public_key) and is insert-only
(idempotent only for identical values), while the recursive
AccountUpdate copy-constrains the inner proof's next_public_key to the
follow-up transition's current key. A post-mint send was therefore
forced to re-commit under the creator key and died at ingest with
'Key already exists in the tree with different value' (witnessed as
the 'Partition ... set twice' conflict when a fresh key was tried
instead). Never caught: draft PRs skip CI and every account_node test
funded senders via raw imported balances, which the new circuit
(correctly) rejects.

- MintRequest gains next_public_key (compressed secp256k1, hex); bound
  through the two-phase flow (the wallet countersigns the staged proof
  that committed to it), mirroring the send shape.
- prepare_mint threads it into prove_initial_with_in_and_out_coins
  (all slots inactive) — the Initial-branch entry point with an
  explicit rotation target. The commitment itself stays signed by the
  creator key (the issuer gate demands account.public_key ==
  creator_pubkey), so the soundness gate is unchanged.
- account_node_tests: replace every provenance-less
  'import_account { balance, proof: None }' fixture with a real
  issuer mint via the new mint_funded_asset helper (prove -> creator-
  signed commitment -> state advance -> commit_mint); test_asset_id()
  now returns the DERIVED asset id for the fixture creator key so the
  issuer gate admits it. New test pins the re-mint rejection arm.
- router_tests: creator-signed mint bodies (signed_mint_body /
  signed_mint_request) for the jobs admit suite; seed_account_history
  writes the 64-byte owner||asset_id composite key (0017 CHECK).
- self_heal_tests: composite-key seeding for the same reason.

Verified locally: account_node::tests 28/28, the previously failing
router/self_heal/jobs classes 50/50, cargo fmt --check, clippy clean in
all three CI scopes, node builds (MVP + --all-features).

* revert: unsound mint next_public_key rotation; document create->mint->send blocker

The previous commit rotated the issuer mint's next_public_key to a
fresh wallet key to dodge the commitment-SMT collision on the first
post-mint send. A dedicated soundness test
(commitment_binds_account_state_checks_creator_verifying_key) proves
that is UNSOUND: the circuit commits account_state_hash (ProofData
PI[0]) with the transition's next_public_key, so binding the commit-leg
gate to a rotated key lets a forger sign the mint commitment with their
own rotation key and inflate/forge a foreign asset. Revert to the
no-rotation mint (PI[0] == creator key), which the gate requires.

The create->mint->send loop is therefore blocked at the circuit layer:
no-rotation is mandatory for soundness, but it forces the creator's
first send to re-commit under sha256(creator_pubkey), which the
insert-only commitment SMT rejects. The two constraints are mutually
exclusive without a circuit change (a dedicated creator_pubkey public
input the gate binds against, decoupled from the rotated state key).
Documented in MULTI_ASSET.md §17.

Kept from the heal: migration 0017 trigger-name fix, the router/
self_heal 64-byte composite-key + creator-signed mint-body test fixes,
and the soundness + MintStore + zero_asset_id coverage tests. The
account_node create->mint->send fixtures remain red on this blocker.

* feat(mint): node-side asset_creators binding + mint key-rotation — close create->mint->send

Per the architecture decision, the per-asset creator binding moves
OUT of the commitment-key check and INTO a node-side mapping table
(MULTI_ASSET.md §5.3 'off-circuit verify'). The Plonky2 circuit is
unchanged.

- Migration 0018: asset_creators (asset_id PK -> creator_pubkey),
  octet_length CHECKs.
- prepare_mint rotates next_public_key to a fresh wallet key (via
  prove_initial_with_in_and_out_coins with all slots inactive), so the
  creator's first follow-up send commits under a fresh key — no more
  insert-only commitment-SMT collision. MintRequest gains
  next_public_key; MintingPrepared/StagedMint carry creator_pubkey.
- mint_flow rejects (409) a mint of an asset_id registered to a
  different creator before proving; mint_commit_flow requires
  commitment.public_key == staged.creator_pubkey and registers the
  asset_id -> creator_pubkey row on success.
- Removed the now-obsolete commitment_binds_account_state /
  proof_account_state_hash (the binding they enforced moved
  off-circuit) and their tests; added db_tests for the new functions.
- Test fixtures use the rotating mint; create->mint->send fixtures now
  pass.

Verified locally: full -p node -p shared --all-features heavy gate
green — 495/495 tests, 100% lines + 100% functions (the CI coverage
gate's --fail-under thresholds). fmt/clippy(3 scopes)/build clean.
MULTI_ASSET.md §17 updated. Migration 0017 trigger fix + the earlier
router/self_heal fixture heal are included on this branch.

* test(history): upsert TxDetail happy-path account under the 64-byte composite key

The staging TxDetail happy-path test (rebased in) mutated the account via
upsert_account_with_source with the raw 32-byte owner address, which trips
the migration-0017 accounts_address_length=64 CHECK. Build the same
owner‖asset_id composite key seed_account_history uses, so the send row
chains onto the mint row and list_account_history(&owner) still resolves.

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
@github-actions github-actions Bot added the ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra) label Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant