Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions node/migrations/0017_neutral_multi_asset_account_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
-- Neutral, permissionless multi-asset account keys (Milestone 2).
--
-- ## What changes
--
-- The protocol becomes fully neutral and permissionless: there is no
-- native/official coin and no central minting authority. Accounts are
-- now keyed per `(owner_address, asset_id)` (Model B) instead of per
-- owner address alone. The in-memory `AccountNode` keys by the tuple;
-- on disk the `accounts.address` BYTEA column stores the 64-byte
-- composite key `owner(32) || asset_id(32)` (see
-- `account_node::account_key_bytes`).
--
-- ## Why a genesis reset
--
-- Same rationale as migrations 0015 / 0016: the Milestone 1 circuit
-- change (new `AccountState` layout with `asset_id`, new asset-id
-- derivation, issuer-mint gate) invalidates EVERY persisted proof at
-- once — each `account.proof`, every queued source proof — and the
-- global SMT/MMR are append-only and shared across accounts. They
-- cannot be partially unwound per account without the global-vs-account
-- mismatch that breaks soundness. The old single-balance account rows
-- are also keyed by 32-byte owner addresses, incompatible with the new
-- 64-byte composite key. A coordinated reset to genesis is the only
-- provably-consistent recovery.
--
-- ## Scope: DEV *and* PRD
--
-- Both are closed test environments (CONTRIBUTING § "Closed test
-- environment"); there is no data to preserve. sqlx applies a migration
-- once per database, so the reset fires exactly once per environment on
-- the first deploy that carries it.
--
-- ## Table set (mirrors `0016` / `reset_proof_dependent_state_tx`)

DELETE FROM accounts;
DELETE FROM smt_state;
DELETE FROM mmr_state;
DELETE FROM mmr_root_index;
DELETE FROM latest_block;
DELETE FROM circuit_digest_meta;

-- The `accounts.address` column now stores the 64-byte composite key
-- `owner(32) || asset_id(32)`. Relax the 0010 length CHECK from 32 to
-- 64. (Idempotent guards via IF EXISTS so a re-run after a manual fix
-- does not error.)
ALTER TABLE accounts DROP CONSTRAINT IF EXISTS accounts_address_length;
ALTER TABLE accounts
ADD CONSTRAINT accounts_address_length CHECK (octet_length(address) = 64);

-- The `account_history` ledger stays keyed by the 32-byte OWNER address
-- (the human-facing handle the `/api/history` endpoint queries by), NOT
-- the 64-byte composite. Redefine the capture trigger to write only the
-- owner prefix of the composite `accounts.address` so the existing
-- 32-byte `account_history_address_length` CHECK still holds and the
-- history endpoint continues to resolve by owner.
--
-- NOTE on the function name: migration 0010 renamed this function
-- `account_history_capture()` → `accounts_history_capture()` (plural,
-- matching the table noun) and re-pointed the `accounts_history_trigger`
-- at the new name. The live trigger therefore executes
-- `accounts_history_capture()`; replacing the obsolete singular name
-- here would leave the live trigger writing `NEW.address` (the 64-byte
-- composite), which violates the 32-byte `account_history_address_length`
-- CHECK on every account upsert. We CREATE OR REPLACE the *plural*
-- function so the owner-prefix change actually takes effect, preserving
-- 0010's full body (the `zkcoins.request_log_id` GUC read +
-- `triggering_request_log_id` column) and only swapping `NEW.address`
-- for its 32-byte owner prefix.
CREATE OR REPLACE FUNCTION accounts_history_capture() RETURNS TRIGGER AS $$
DECLARE
src TEXT := COALESCE(NULLIF(current_setting('zkcoins.account_source', TRUE), ''), 'scanner');
commit_txid_hex TEXT := NULLIF(current_setting('zkcoins.account_commit_txid', TRUE), '');
commit_txid_bytes BYTEA := NULL;
req_log_id_text TEXT := NULLIF(current_setting('zkcoins.request_log_id', TRUE), '');
req_log_id BIGINT := NULL;
owner_address BYTEA := substring(NEW.address FROM 1 FOR 32);
BEGIN
-- Skip when row content didn't change (UPDATEs that touch only
-- `updated_at` should not generate history noise).
IF TG_OP = 'UPDATE' AND OLD.data = NEW.data THEN
RETURN NEW;
END IF;

IF commit_txid_hex IS NOT NULL THEN
BEGIN
commit_txid_bytes := decode(commit_txid_hex, 'hex');
EXCEPTION WHEN OTHERS THEN
commit_txid_bytes := NULL;
END;
END IF;

IF req_log_id_text IS NOT NULL THEN
BEGIN
req_log_id := req_log_id_text::BIGINT;
EXCEPTION WHEN OTHERS THEN
req_log_id := NULL;
END;
END IF;

INSERT INTO account_history
(address, prev_data, new_data, source, triggering_commit_txid, triggering_request_log_id)
VALUES
(owner_address,
CASE WHEN TG_OP = 'UPDATE' THEN OLD.data ELSE NULL END,
NEW.data,
src,
commit_txid_bytes,
req_log_id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
35 changes: 35 additions & 0 deletions node/migrations/0018_asset_creators.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Per-asset creator binding table (node-side, off-circuit).
--
-- ## What changes
--
-- The neutral, permissionless multi-asset model binds each `asset_id`
-- to the public key that first minted it. v1 of MULTI_ASSET.md §5.3
-- ("off-circuit verify") keeps this binding OUT of the Plonky2 circuit
-- and OUT of the insert-only commitment SMT: instead the node records
-- `asset_id -> creator_pubkey` here and, at mint-commit time, requires
-- the wallet-signed `commitment.public_key` to equal the registered
-- creator key.
--
-- ## Why this table (and why the SMT key check moved out)
--
-- The mint previously set `next_public_key == creator_pubkey` so the
-- on-chain commitment committed under `sha256(creator_pubkey)`. That
-- doubled as the creator binding but also made the creator's FIRST
-- follow-up send re-commit under the same map key, which the
-- insert-only commitment SMT rejects ("Key already exists in the tree
-- with different value"). The mint now rotates `next_public_key` to a
-- fresh wallet key (like a normal send), so the binding can no longer
-- ride on the commitment key. This table carries it instead: a first
-- mint inserts the row, and any later mint of the same `asset_id` whose
-- creator key differs is rejected with 409 CONFLICT.
--
-- The `asset_id` is 32 bytes; the compressed secp256k1 `creator_pubkey`
-- is 33 bytes — the same CHECK shapes the other key columns use.

CREATE TABLE asset_creators (
asset_id BYTEA PRIMARY KEY,
creator_pubkey BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (octet_length(asset_id) = 32),
CHECK (octet_length(creator_pubkey) = 33)
);
Loading
Loading