|
| 1 | +# Taproot Asset Supply Commitment Persistence |
| 2 | + |
| 3 | +This document details the database schema and Go implementation (`tapdb/supply_commit.go`) that provides persistent storage for the Taproot Asset Supply Commitment State Machine (`universe/supplycommit`). This layer ensures the state machine's progress is durable across restarts and provides mechanisms for tracking historical and pending supply commitments. |
| 4 | + |
| 5 | +## Purpose |
| 6 | + |
| 7 | +The primary function of this persistence layer is to reliably store the state and associated data for the `universe/supplycommit` state machine. This state machine manages the process of creating and anchoring cryptographic commitments to the supply changes (mints, burns, ignores) of a specific asset group onto the Bitcoin blockchain. |
| 8 | + |
| 9 | +This involves storing: |
| 10 | + |
| 11 | +1. The current operational state of the state machine for each asset group. |
| 12 | +2. Details of past, successfully confirmed supply commitments. |
| 13 | +3. Information about pending state transitions, including the specific supply update events being processed. |
| 14 | +4. Details of the Bitcoin transactions used to anchor commitments. |
| 15 | + |
| 16 | +## Schema Overview |
| 17 | + |
| 18 | +Migration `000036_asset_commit.up.sql` introduces several tables to manage the state machine's lifecycle and data. |
| 19 | + |
| 20 | +1. **`supply_commit_states`**: An enum-like table defining the possible states of the state machine (e.g., `DefaultState`, `UpdatesPendingState`, `CommitTxCreateState`). |
| 21 | + * `id` (INTEGER PK): Numeric ID for the state. |
| 22 | + * `state_name` (TEXT UNIQUE): Human-readable name of the state. |
| 23 | + |
| 24 | +2. **`supply_commit_update_types`**: An enum-like table defining the types of supply updates (e.g., `mint`, `burn`, `ignore`). |
| 25 | + * `id` (INTEGER PK): Numeric ID for the update type. |
| 26 | + * `update_type_name` (TEXT UNIQUE): Human-readable name of the update type. |
| 27 | + |
| 28 | +3. **`supply_commitments`**: Stores the details of a specific, potentially confirmed, supply commitment anchored on-chain. |
| 29 | + * `commit_id` (INTEGER PK): Unique identifier for this commitment instance. |
| 30 | + * `group_key` (BLOB): The tweaked group key identifying the asset group. |
| 31 | + * `chain_txn_id` (BIGINT FK -> `chain_txns.txn_id`): Reference to the Bitcoin transaction containing this commitment. |
| 32 | + * `output_index` (INTEGER): The output index within the `chain_txn_id` transaction. |
| 33 | + * `internal_key_id` (BIGINT FK -> `internal_keys.key_id`): The internal key used for the commitment output's Taproot derivation. |
| 34 | + * `output_key` (BLOB): The final tweaked Taproot output key. |
| 35 | + * `block_header` (BLOB): Header of the block confirming the commitment (NULL if unconfirmed). |
| 36 | + * `block_height` (INTEGER): Height of the confirming block (NULL if unconfirmed). |
| 37 | + * `merkle_proof` (BLOB): Merkle proof of the transaction's inclusion in the block (NULL if unconfirmed). |
| 38 | + * `supply_root_hash` (BLOB): The MS-SMT root hash of the supply tree at this commitment (NULL until finalized). |
| 39 | + * `supply_root_sum` (BIGINT): The MS-SMT root sum (total supply value) at this commitment (NULL until finalized). |
| 40 | + |
| 41 | +4. **`supply_commit_state_machines`**: Tracks the current state for each asset group's state machine instance. |
| 42 | + * `group_key` (BLOB PK): The tweaked group key, uniquely identifying the state machine instance. |
| 43 | + * `current_state_id` (INTEGER FK -> `supply_commit_states.id`): The current operational state. |
| 44 | + * `latest_commitment_id` (BIGINT FK -> `supply_commitments.commit_id`): Reference to the most recently *finalized* and confirmed commitment (NULL if none). |
| 45 | + |
| 46 | +5. **`supply_commit_transitions`**: Records an active attempt to transition the supply state. Acts like a Write-Ahead Log (WAL) entry for the state machine. |
| 47 | + * `transition_id` (INTEGER PK): Unique identifier for this transition attempt. |
| 48 | + * `state_machine_group_key` (BLOB FK -> `supply_commit_state_machines.group_key`): Links back to the state machine instance. |
| 49 | + * `old_commitment_id` (BIGINT FK -> `supply_commitments.commit_id`): The commitment being replaced (NULL for the first commitment). |
| 50 | + * `new_commitment_id` (BIGINT FK -> `supply_commitments.commit_id`): The new commitment being created by this transition (NULL initially). |
| 51 | + * `pending_commit_txn_id` (BIGINT FK -> `chain_txns.txn_id`): The Bitcoin transaction intended to confirm this transition (NULL until created/signed). |
| 52 | + * `finalized` (BOOLEAN): Indicates if the transition completed successfully and was applied. Defaults to `FALSE`. |
| 53 | + * `creation_time` (TIMESTAMP): When the transition was initiated. |
| 54 | + * `UNIQUE INDEX ... WHERE finalized = 0`: Crucially ensures only *one* non-finalized (pending) transition can exist per `state_machine_group_key` at any time. |
| 55 | + |
| 56 | +6. **`supply_update_events`**: Stores the individual mint, burn, or ignore events associated with a *pending* transition. |
| 57 | + * `event_id` (INTEGER PK): Unique identifier for the event record. |
| 58 | + * `transition_id` (BIGINT FK -> `supply_commit_transitions.transition_id` ON DELETE CASCADE): Links to the parent transition. Cascade delete ensures events are cleaned up if a transition is aborted/deleted. |
| 59 | + * `update_type_id` (INTEGER FK -> `supply_commit_update_types.id`): Type of the update event. |
| 60 | + * `event_data` (BLOB): Serialized data specific to the event type (e.g., `NewMintEvent`, `NewBurnEvent`). |
| 61 | + |
| 62 | +7. **`ALTER TABLE mint_anchor_uni_commitments`**: Adds a `spent_by` column (FK -> `supply_commitments.commit_id`) to track which supply commitment transaction spent a given minting pre-commitment output. |
| 63 | + |
| 64 | +### Schema Relationships (Mermaid Diagram) |
| 65 | + |
| 66 | +```mermaid |
| 67 | +erDiagram |
| 68 | + supply_commit_state_machines ||--o{ supply_commit_transitions : "has_pending" |
| 69 | + supply_commit_state_machines }|--|| supply_commit_states : "uses_state" |
| 70 | + supply_commit_state_machines }|--|| supply_commitments : "tracks_latest" |
| 71 | + supply_commit_transitions ||--o{ supply_update_events : "includes_events" |
| 72 | + supply_commit_transitions }|--|| supply_commitments : "replaces_old" |
| 73 | + supply_commit_transitions }|--|| supply_commitments : "creates_new" |
| 74 | + supply_commit_transitions }|--|| chain_txns : "uses_pending_tx" |
| 75 | + supply_update_events ||--|| supply_commit_update_types : "uses_type" |
| 76 | + supply_commitments ||--|| chain_txns : "included_in_tx" |
| 77 | + supply_commitments ||--|| internal_keys : "uses_internal_key" |
| 78 | + mint_anchor_uni_commitments }|--|| supply_commitments : "spent_by_commit" |
| 79 | + |
| 80 | + supply_commit_states { |
| 81 | + INTEGER id PK |
| 82 | + TEXT state_name UK |
| 83 | + } |
| 84 | + |
| 85 | + supply_commit_update_types { |
| 86 | + INTEGER id PK |
| 87 | + TEXT update_type_name UK |
| 88 | + } |
| 89 | + |
| 90 | + supply_commitments { |
| 91 | + INTEGER commit_id PK |
| 92 | + BLOB group_key "Idx" |
| 93 | + BIGINT chain_txn_id FK |
| 94 | + INTEGER output_index |
| 95 | + BIGINT internal_key_id FK |
| 96 | + BLOB output_key |
| 97 | + BLOB supply_root_hash |
| 98 | + BIGINT supply_root_sum |
| 99 | + } |
| 100 | + |
| 101 | + supply_commit_state_machines { |
| 102 | + BLOB group_key PK |
| 103 | + INTEGER current_state_id FK |
| 104 | + BIGINT latest_commitment_id FK |
| 105 | + } |
| 106 | + |
| 107 | + supply_commit_transitions { |
| 108 | + INTEGER transition_id PK |
| 109 | + BLOB state_machine_group_key FK |
| 110 | + BIGINT old_commitment_id FK |
| 111 | + BIGINT new_commitment_id FK |
| 112 | + BIGINT pending_commit_txn_id FK |
| 113 | + BOOLEAN finalized "Default=0" |
| 114 | + TIMESTAMP creation_time |
| 115 | + } |
| 116 | + |
| 117 | + supply_update_events { |
| 118 | + INTEGER event_id PK |
| 119 | + BIGINT transition_id FK |
| 120 | + INTEGER update_type_id FK |
| 121 | + BLOB event_data |
| 122 | + } |
| 123 | + |
| 124 | + chain_txns { |
| 125 | + INTEGER txn_id PK |
| 126 | + BLOB txid UK |
| 127 | + BLOB raw_tx |
| 128 | + INTEGER block_height |
| 129 | + BLOB block_hash |
| 130 | + INTEGER tx_index |
| 131 | + } |
| 132 | + |
| 133 | + internal_keys { |
| 134 | + INTEGER key_id PK |
| 135 | + BLOB raw_key UK |
| 136 | + } |
| 137 | + |
| 138 | + mint_anchor_uni_commitments { |
| 139 | + INTEGER id PK |
| 140 | + BIGINT batch_id FK |
| 141 | + INTEGER tx_output_index |
| 142 | + BLOB taproot_internal_key |
| 143 | + BLOB group_key |
| 144 | + BIGINT spent_by FK |
| 145 | + } |
| 146 | +``` |
| 147 | + |
| 148 | +## State Machine Persistence Logic |
| 149 | + |
| 150 | +The database tables work together to provide durable storage and a recovery mechanism for the supply commitment state machine. |
| 151 | + |
| 152 | +* **Current State:** The `supply_commit_state_machines` table acts as the primary indicator of the current operational state (`current_state_id`) for a given asset group (`group_key`). It also points to the `latest_commitment_id` that has been successfully processed and confirmed. |
| 153 | + |
| 154 | +* **Pending Transitions (WAL):** The `supply_commit_transitions` table is key to the persistence strategy. Due to the unique index `supply_commit_transitions_single_pending_idx` (on `state_machine_group_key` WHERE `finalized = 0`), only one *active* transition can exist per asset group. This record acts like a Write-Ahead Log (WAL) entry: |
| 155 | + * It captures the *intent* to move from `old_commitment_id` to `new_commitment_id`. |
| 156 | + * It aggregates all necessary information for the transition *before* it's fully finalized. |
| 157 | + * The `supply_update_events` associated with this transition (linked via `transition_id`) store the specific data (mints, burns, ignores) driving the change. |
| 158 | + * References to the `new_commitment_id` (in `supply_commitments`) and `pending_commit_txn_id` (in `chain_txns`) are added as the state machine progresses through transaction creation and signing. |
| 159 | + |
| 160 | +* **Commitment Data:** The `supply_commitments` table stores the immutable details of *each* commitment attempt once it reaches the stage of having a potential on-chain transaction. Initially, confirmation details (`block_height`, `merkle_proof`, etc.) and the final SMT root (`supply_root_hash`, `supply_root_sum`) are NULL. |
| 161 | + |
| 162 | +* **Lifecycle & Recovery:** |
| 163 | + 1. **Initiation (`InsertPendingUpdate`):** When the first `SupplyUpdateEvent` arrives for an idle state machine, a new row is inserted into `supply_commit_transitions` (`finalized=0`), and the event is stored in `supply_update_events`. The state machine's state in `supply_commit_state_machines` is set to `UpdatesPendingState`. Subsequent events for this group add more rows to `supply_update_events` linked to the *same* pending transition. |
| 164 | + 2. **Transaction Creation/Signing (`InsertSignedCommitTx`):** When the state machine creates and signs the commitment transaction, a new row is added to `chain_txns`, a new row is added to `supply_commitments` (with NULL confirmation/root details), and the *pending* `supply_commit_transitions` row is updated with `new_commitment_id` and `pending_commit_txn_id`. The state machine's state moves to `CommitBroadcastState`. |
| 165 | + 3. **Confirmation & Finalization (`ApplyStateTransition`):** Once the transaction (`pending_commit_txn_id`) confirms: |
| 166 | + * The corresponding `supply_commitments` row (`new_commitment_id`) is updated with block details, merkle proof, and the final calculated `supply_root_hash` and `supply_root_sum` (derived by applying the `supply_update_events` to the SMTs via `applySupplyUpdatesInternal`). |
| 167 | + * The corresponding `chain_txns` row is updated with block hash/height/index. |
| 168 | + * The `supply_commit_transitions` row is marked `finalized = 1`. |
| 169 | + * The `supply_commit_state_machines` row is updated: `current_state_id` becomes `DefaultState`, and `latest_commitment_id` is set to the `new_commitment_id` of the just-finalized transition. |
| 170 | + 4. **Restart:** Upon restart, the system queries `supply_commit_state_machines` for the current state. If it's not `DefaultState`, it queries `supply_commit_transitions` for the single pending (`finalized=0`) transition and reconstructs the `SupplyStateTransition` object (including fetching associated `supply_update_events` and commitment details) to resume operation from the correct point (e.g., re-broadcasting, waiting for confirmation, or finalizing). |
| 171 | + |
| 172 | +## Implementation (`tapdb/SupplyCommitMachine`) |
| 173 | + |
| 174 | +The `tapdb.SupplyCommitMachine` struct implements the `supplycommit.CommitmentTracker` and `supplycommit.StateMachineStore` interfaces, bridging the gap between the abstract state machine logic and the SQL database. |
| 175 | + |
| 176 | +* **Core:** It embeds a `BatchedSupplyCommitStore`, which provides access to the necessary SQLc queries and transaction management (`ExecTx`). |
| 177 | +* **Interface Mapping:** |
| 178 | + * `UnspentPrecommits`: Queries `mint_anchor_uni_commitments` filtering by `group_key` and `spent_by IS NULL`. |
| 179 | + * `SupplyCommit`: Queries `supply_commit_state_machines` to get `latest_commitment_id` for the group, then queries `supply_commitments` using that ID. |
| 180 | + * `InsertPendingUpdate`: Manages the logic described in "Lifecycle & Recovery - Initiation", using `UpsertSupplyCommitStateMachine`, `QueryExistingPendingTransition`, `InsertSupplyCommitTransition`, `InsertSupplyUpdateEvent`. |
| 181 | + * `InsertSignedCommitTx`: Implements the logic from "Lifecycle & Recovery - Transaction Creation/Signing", using `QueryPendingSupplyCommitTransition`, `UpsertChainTx`, `UpsertInternalKey`, `InsertSupplyCommitment`, `UpdateSupplyCommitTransitionCommitment`, `UpsertSupplyCommitStateMachine`. |
| 182 | + * `CommitState`: Updates `current_state_id` in `supply_commit_state_machines` via `UpsertSupplyCommitStateMachine`. |
| 183 | + * `FetchState`: Reconstructs the current state and the pending `SupplyStateTransition` (if any) by querying `supply_commit_state_machines`, `supply_commit_transitions`, `supply_update_events`, `supply_commitments`, `chain_txns`, and `internal_keys`. It handles deserializing event data and reconstructing commitment objects. |
| 184 | + * `ApplyStateTransition`: Executes the finalization logic ("Lifecycle & Recovery - Confirmation & Finalization"). It calls `applySupplyUpdatesInternal` (from `supply_tree.go`) to persist SMT changes, then uses `UpdateSupplyCommitmentRoot`, `UpdateSupplyCommitmentChainDetails`, `UpsertChainTx`, `FinalizeSupplyCommitTransition`, and `UpsertSupplyCommitStateMachine` to update the database records accordingly. |
| 185 | + |
| 186 | +## Atomicity and Recovery |
| 187 | + |
| 188 | +All multi-step database modifications within `SupplyCommitMachine` methods are wrapped in database transactions using `db.ExecTx`. This ensures that operations like inserting a transition and its first event, or updating commitment details and finalizing the transition, are atomic. |
| 189 | + |
| 190 | +The persistence of the current state in `supply_commit_state_machines` and the detailed logging of the single pending transition in `supply_commit_transitions` (acting as a WAL) allow the state machine to reliably recover and resume its operation after restarts, preventing duplicate commitments or loss of pending updates. |
0 commit comments