Release: develop -> main - #223
Draft
github-actions[bot] wants to merge 3 commits into
Draft
Conversation
* 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>
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.
Automatic Release PR
Commits: 3 new commit(s)