diff --git a/docs/release-notes/release-notes-0.7.0.md b/docs/release-notes/release-notes-0.7.0.md index aa5d59c12..cd0b35b9b 100644 --- a/docs/release-notes/release-notes-0.7.0.md +++ b/docs/release-notes/release-notes-0.7.0.md @@ -72,6 +72,7 @@ - https://github.com/lightninglabs/taproot-assets/pull/1716 - https://github.com/lightninglabs/taproot-assets/pull/1675 - https://github.com/lightninglabs/taproot-assets/pull/1674 + - https://github.com/lightninglabs/taproot-assets/pull/1784 - A new [address version 2 was introduced that supports grouped assets and custom (sender-defined) diff --git a/tapdb/asset_minting.go b/tapdb/asset_minting.go index 16d9a97c1..53e3e83fb 100644 --- a/tapdb/asset_minting.go +++ b/tapdb/asset_minting.go @@ -116,9 +116,9 @@ type ( // database ID. ProofUpdateByID = sqlc.UpsertAssetProofByIDParams - // FetchPreCommitParams is a type alias for the params used to fetch - // mint anchor pre-commitments. - FetchPreCommitParams = sqlc.FetchMintAnchorUniCommitmentParams + // FetchMintPreCommitsParams is a type alias for the params used to + // fetch mint anchor supply pre-commitments. + FetchMintPreCommitsParams = sqlc.FetchMintSupplyPreCommitsParams // FetchAssetID is used to fetch the primary key ID of an asset, by // outpoint and tweaked script key. @@ -132,9 +132,9 @@ type ( // disk. NewAssetMeta = sqlc.UpsertAssetMetaParams - // MintAnchorUniCommitParams wraps the params needed to insert a new - // mint anchor uni commitment on disk. - MintAnchorUniCommitParams = sqlc.UpsertMintAnchorUniCommitmentParams + // UpsertBatchPreCommitParams wraps the params needed to insert a new + // mint batch supply pre-commit on disk. + UpsertBatchPreCommitParams = sqlc.UpsertMintSupplyPreCommitParams ) // PendingAssetStore is a sub-set of the main sqlc.Querier interface that @@ -248,15 +248,15 @@ type PendingAssetStore interface { FetchAssetMetaForAsset(ctx context.Context, assetID []byte) (sqlc.FetchAssetMetaForAssetRow, error) - // FetchMintAnchorUniCommitment fetches mint anchor pre-commitments. - FetchMintAnchorUniCommitment(ctx context.Context, - arg FetchPreCommitParams) ( - []sqlc.FetchMintAnchorUniCommitmentRow, error) + // FetchMintSupplyPreCommits fetches mint anchor pre-commitments. + FetchMintSupplyPreCommits(ctx context.Context, + arg FetchMintPreCommitsParams) ( + []sqlc.FetchMintSupplyPreCommitsRow, error) - // UpsertMintAnchorUniCommitment inserts a new or updates an existing - // mint anchor uni commitment on disk. - UpsertMintAnchorUniCommitment(ctx context.Context, - arg MintAnchorUniCommitParams) (int64, error) + // UpsertMintSupplyPreCommit inserts a new or updates an existing + // mint batch supply commit on disk. + UpsertMintSupplyPreCommit(ctx context.Context, + arg UpsertBatchPreCommitParams) (int64, error) } var ( @@ -448,8 +448,8 @@ func insertMintAnchorTx(ctx context.Context, q PendingAssetStore, return fmt.Errorf("unable to encode outpoint: %w", err) } - _, err = q.UpsertMintAnchorUniCommitment( - ctx, MintAnchorUniCommitParams{ + _, err = q.UpsertMintSupplyPreCommit( + ctx, UpsertBatchPreCommitParams{ BatchKey: rawBatchKey, TxOutputIndex: int32(preCommitOut.OutIdx), TaprootInternalKeyID: internalKeyID, @@ -1347,8 +1347,8 @@ func marshalMintingBatch(ctx context.Context, q PendingAssetStore, // the pre-commitment output index from the database. var preCommitOut fn.Option[tapgarden.PreCommitmentOutput] if dbBatch.UniverseCommitments { - fetchRes, err := q.FetchMintAnchorUniCommitment( - ctx, FetchPreCommitParams{ + fetchRes, err := q.FetchMintSupplyPreCommits( + ctx, FetchMintPreCommitsParams{ BatchKey: dbBatch.RawKey, }, ) @@ -1545,8 +1545,8 @@ func (a *AssetMintingStore) FetchDelegationKey(ctx context.Context, readOpts := NewAssetStoreReadTx() dbErr := a.db.ExecTx(ctx, &readOpts, func(q PendingAssetStore) error { - fetchRow, err := q.FetchMintAnchorUniCommitment( - ctx, FetchPreCommitParams{ + fetchRow, err := q.FetchMintSupplyPreCommits( + ctx, FetchMintPreCommitsParams{ GroupKey: groupKeyBytes, }, ) @@ -1644,8 +1644,8 @@ func upsertPreCommit(ctx context.Context, q PendingAssetStore, return fmt.Errorf("unable to encode outpoint: %w", err) } - _, err = q.UpsertMintAnchorUniCommitment( - ctx, MintAnchorUniCommitParams{ + _, err = q.UpsertMintSupplyPreCommit( + ctx, UpsertBatchPreCommitParams{ BatchKey: batchKey, TxOutputIndex: int32(preCommit.OutIdx), TaprootInternalKeyID: internalKeyID, diff --git a/tapdb/asset_minting_test.go b/tapdb/asset_minting_test.go index 9093d309a..a61f884e4 100644 --- a/tapdb/asset_minting_test.go +++ b/tapdb/asset_minting_test.go @@ -1835,10 +1835,12 @@ func TestTapscriptTreeManager(t *testing.T) { loadTapscriptTreeChecked(t, ctx, assetStore, tree5, tree5Hash) } -// storeMintAnchorUniCommitment stores a mint anchor commitment in the DB. -func storeMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore, +// storeMintSupplyPreCommit stores a mint anchor supply pre-commitment in the +// DB. +func storeMintSupplyPreCommit(t *testing.T, assetStore AssetMintingStore, batchKey []byte, txOutputIndex int32, - taprootInternalKey keychain.KeyDescriptor, groupKey []byte) { + taprootInternalKey keychain.KeyDescriptor, groupKey []byte, + outpoint wire.OutPoint) { ctx := context.Background() @@ -1854,12 +1856,16 @@ func storeMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore, }) require.NoError(t, err) - _, err = q.UpsertMintAnchorUniCommitment( - ctx, sqlc.UpsertMintAnchorUniCommitmentParams{ + opBytes, err := encodeOutpoint(outpoint) + require.NoError(t, err) + + _, err = q.UpsertMintSupplyPreCommit( + ctx, UpsertBatchPreCommitParams{ BatchKey: batchKey, TxOutputIndex: txOutputIndex, TaprootInternalKeyID: internalKeyID, GroupKey: groupKey, + Outpoint: opBytes, }, ) require.NoError(t, err) @@ -1869,19 +1875,21 @@ func storeMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore, _ = assetStore.db.ExecTx(ctx, &writeTxOpts, upsertMintAnchorPreCommit) } -// assertMintAnchorUniCommitment is a helper function that reads a mint anchor -// commitment from the DB and asserts that it matches the expected values. -func assertMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore, +// assertMintSupplyPreCommit is a helper function that reads a mint anchor +// supply pre-commitment from the DB and asserts that it matches the expected +// values. +func assertMintSupplyPreCommit(t *testing.T, assetStore AssetMintingStore, batchKey []byte, txOutputIndex int32, - preCommitInternalKey keychain.KeyDescriptor, groupPubKeyBytes []byte) { + preCommitInternalKey keychain.KeyDescriptor, groupPubKeyBytes []byte, + outpoint wire.OutPoint) { ctx := context.Background() readOpts := NewAssetStoreReadTx() - var preCommit *sqlc.FetchMintAnchorUniCommitmentRow + var preCommit *sqlc.FetchMintSupplyPreCommitsRow readMintAnchorCommitment := func(q PendingAssetStore) error { - fetchRes, err := q.FetchMintAnchorUniCommitment( - ctx, FetchPreCommitParams{ + fetchRes, err := q.FetchMintSupplyPreCommits( + ctx, FetchMintPreCommitsParams{ BatchKey: batchKey, }, ) @@ -1912,12 +1920,16 @@ func assertMintAnchorUniCommitment(t *testing.T, assetStore AssetMintingStore, preCommit.InternalKey.KeyFamily, ) require.Equal(t, groupPubKeyBytes, preCommit.GroupKey) + + opBytes, err := encodeOutpoint(outpoint) + require.NoError(t, err) + require.Equal(t, opBytes, preCommit.Outpoint) } -// TestUpsertMintAnchorUniCommitment tests the UpsertMintAnchorUniCommitment -// FetchMintAnchorUniCommitment and SQL queries. In particular, it tests that -// upsert works correctly. -func TestUpsertMintAnchorUniCommitment(t *testing.T) { +// TestUpsertMintSupplyPreCommit tests the UpsertMintSupplyPreCommit and +// FetchSupplyPreCommits SQL queries. In particular, it tests that upsert works +// correctly. +func TestUpsertMintSupplyPreCommit(t *testing.T) { t.Parallel() ctx := context.Background() @@ -1948,51 +1960,62 @@ func TestUpsertMintAnchorUniCommitment(t *testing.T) { }, ) + // Define pre-commit outpoint for the batch mint anchor tx. + txOutputIndex := int32(2) + txidStr := mintingBatch.GenesisPacket.FundedPsbt.Pkt.UnsignedTx.TxID() + + txid, err := chainhash.NewHashFromStr(txidStr) + require.NoError(t, err) + + preCommitOutpoint := wire.OutPoint{ + Hash: *txid, + Index: uint32(txOutputIndex), + } + // Serialize keys into bytes for easier handling. preCommitInternalKey, _ := test.RandKeyDesc(t) groupPubKeyBytes := group.GroupPubKey.SerializeCompressed() // Upsert a mint anchor commitment for the batch. - txOutputIndex := int32(2) - storeMintAnchorUniCommitment( + storeMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, - preCommitInternalKey, groupPubKeyBytes, + preCommitInternalKey, groupPubKeyBytes, preCommitOutpoint, ) // Retrieve and inspect the mint anchor commitment we just inserted. - assertMintAnchorUniCommitment( + assertMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, - preCommitInternalKey, groupPubKeyBytes, + preCommitInternalKey, groupPubKeyBytes, preCommitOutpoint, ) - // Upsert-ing a new taproot internal key for the same batch should - // overwrite the existing one. + // Upsert-ing a new taproot internal key for the same pre-commit + // outpoint should overwrite the existing one. internalKey2, _ := test.RandKeyDesc(t) - storeMintAnchorUniCommitment( + storeMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, internalKey2, - groupPubKeyBytes, + groupPubKeyBytes, preCommitOutpoint, ) - assertMintAnchorUniCommitment( + assertMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, internalKey2, - groupPubKeyBytes, + groupPubKeyBytes, preCommitOutpoint, ) - // Upsert-ing a new group key for the same batch should overwrite the - // existing one. + // Upsert-ing a new group key for the same pre-commit outpoint should + // overwrite the existing one. groupPubKey2 := test.RandPubKey(t) groupPubKey2Bytes := groupPubKey2.SerializeCompressed() - storeMintAnchorUniCommitment( + storeMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, internalKey2, - groupPubKey2Bytes, + groupPubKey2Bytes, preCommitOutpoint, ) - assertMintAnchorUniCommitment( + assertMintSupplyPreCommit( t, *assetStore, batchKey, txOutputIndex, internalKey2, - groupPubKey2Bytes, + groupPubKey2Bytes, preCommitOutpoint, ) } diff --git a/tapdb/assets_common.go b/tapdb/assets_common.go index 4f3b4ba39..b3d6fef20 100644 --- a/tapdb/assets_common.go +++ b/tapdb/assets_common.go @@ -37,6 +37,9 @@ type UpsertAssetStore interface { // DB. UpsertChainTx(ctx context.Context, arg ChainTxParams) (int64, error) + // FetchChainTx fetches a chain tx from the DB. + FetchChainTx(ctx context.Context, txid []byte) (ChainTx, error) + // UpsertGenesisAsset inserts a new or updates an existing genesis asset // (the base asset info) in the DB, and returns the primary key. // @@ -97,6 +100,10 @@ type UpsertAssetStore interface { // UpsertTapscriptTreeRootHash inserts a new tapscript tree root hash. UpsertTapscriptTreeRootHash(ctx context.Context, arg TapscriptTreeRootHash) (int64, error) + + // UpsertSupplyPreCommit inserts a new supply pre-commit record. + UpsertSupplyPreCommit(ctx context.Context, + arg sqlc.UpsertSupplyPreCommitParams) (int64, error) } var ( diff --git a/tapdb/burn_tree.go b/tapdb/burn_tree.go index ed19fca9e..fede10d27 100644 --- a/tapdb/burn_tree.go +++ b/tapdb/burn_tree.go @@ -125,9 +125,16 @@ func insertBurnsInternal(ctx context.Context, db BaseUniverseStore, // Call the generic upsert function for the burn sub-tree to // update DB records. MetaReveal is nil for burns. + uniProofType, err := + supplycommit.BurnTreeType.ToUniverseProofType() + if err != nil { + return nil, fmt.Errorf("unable to map burn supply "+ + "tree type to universe proof type: %w", err) + } + _, err = universeUpsertProofLeaf( - ctx, db, subNs, supplycommit.BurnTreeType.String(), - groupKey, leafKey, leaf, nil, blockHeight, + ctx, db, subNs, uniProofType, groupKey, leafKey, leaf, + nil, blockHeight, ) if err != nil { return nil, fmt.Errorf("unable to upsert burn "+ diff --git a/tapdb/migrations.go b/tapdb/migrations.go index 4666eb5c5..6c3f948b6 100644 --- a/tapdb/migrations.go +++ b/tapdb/migrations.go @@ -24,7 +24,7 @@ const ( // daemon. // // NOTE: This MUST be updated when a new migration is added. - LatestMigrationVersion = 45 + LatestMigrationVersion = 46 ) // DatabaseBackend is an interface that contains all methods our different diff --git a/tapdb/multiverse.go b/tapdb/multiverse.go index cb7c2418d..fae413ae5 100644 --- a/tapdb/multiverse.go +++ b/tapdb/multiverse.go @@ -802,7 +802,7 @@ func (b *MultiverseStore) UpsertProofLeaf(ctx context.Context, } uniProof, err = universeUpsertProofLeaf( - ctx, dbTx, id.String(), id.ProofType.String(), + ctx, dbTx, id.String(), id.ProofType, id.GroupKey, key, leaf, metaReveal, blockHeight, ) if err != nil { @@ -883,7 +883,7 @@ func (b *MultiverseStore) UpsertProofLeafBatch(ctx context.Context, // start with. uniProof, err := universeUpsertProofLeaf( ctx, store, item.ID.String(), - item.ID.ProofType.String(), + item.ID.ProofType, item.ID.GroupKey, item.Key, item.Leaf, item.MetaReveal, blockHeight, ) diff --git a/tapdb/sqlc/assets.sql.go b/tapdb/sqlc/assets.sql.go index 10026b20a..6db3205eb 100644 --- a/tapdb/sqlc/assets.sql.go +++ b/tapdb/sqlc/assets.sql.go @@ -1600,37 +1600,38 @@ func (q *Queries) FetchManagedUTXOs(ctx context.Context) ([]FetchManagedUTXOsRow return items, nil } -const FetchMintAnchorUniCommitment = `-- name: FetchMintAnchorUniCommitment :many +const FetchMintSupplyPreCommits = `-- name: FetchMintSupplyPreCommits :many SELECT - mint_anchor_uni_commitments.id, - mint_anchor_uni_commitments.batch_id, - mint_anchor_uni_commitments.tx_output_index, - mint_anchor_uni_commitments.group_key, - mint_anchor_uni_commitments.spent_by, + precommits.id, + precommits.batch_id, + precommits.tx_output_index, + precommits.group_key, + precommits.spent_by, batch_internal_keys.raw_key AS batch_key, - mint_anchor_uni_commitments.taproot_internal_key_id, + precommits.taproot_internal_key_id, + precommits.outpoint, taproot_internal_keys.key_id, taproot_internal_keys.raw_key, taproot_internal_keys.key_family, taproot_internal_keys.key_index -FROM mint_anchor_uni_commitments +FROM mint_supply_pre_commits AS precommits JOIN internal_keys taproot_internal_keys - ON mint_anchor_uni_commitments.taproot_internal_key_id = taproot_internal_keys.key_id + ON precommits.taproot_internal_key_id = taproot_internal_keys.key_id LEFT JOIN asset_minting_batches batches - ON mint_anchor_uni_commitments.batch_id = batches.batch_id + ON precommits.batch_id = batches.batch_id LEFT JOIN internal_keys batch_internal_keys ON batches.batch_id = batch_internal_keys.key_id WHERE ( (batch_internal_keys.raw_key = $1 OR $1 IS NULL) AND - (mint_anchor_uni_commitments.group_key = $2 OR $2 IS NULL) AND + (precommits.group_key = $2 OR $2 IS NULL) AND (taproot_internal_keys.raw_key = $3 OR $3 IS NULL) ) ` -type FetchMintAnchorUniCommitmentParams struct { +type FetchMintSupplyPreCommitsParams struct { BatchKey []byte GroupKey []byte TaprootInternalKeyRaw []byte } -type FetchMintAnchorUniCommitmentRow struct { +type FetchMintSupplyPreCommitsRow struct { ID int64 BatchID int32 TxOutputIndex int32 @@ -1638,20 +1639,21 @@ type FetchMintAnchorUniCommitmentRow struct { SpentBy sql.NullInt64 BatchKey []byte TaprootInternalKeyID int64 + Outpoint []byte InternalKey InternalKey } -// Fetch records from the mint_anchor_uni_commitments table with optional +// Fetch records from the supply_pre_commits table with optional // filtering. -func (q *Queries) FetchMintAnchorUniCommitment(ctx context.Context, arg FetchMintAnchorUniCommitmentParams) ([]FetchMintAnchorUniCommitmentRow, error) { - rows, err := q.db.QueryContext(ctx, FetchMintAnchorUniCommitment, arg.BatchKey, arg.GroupKey, arg.TaprootInternalKeyRaw) +func (q *Queries) FetchMintSupplyPreCommits(ctx context.Context, arg FetchMintSupplyPreCommitsParams) ([]FetchMintSupplyPreCommitsRow, error) { + rows, err := q.db.QueryContext(ctx, FetchMintSupplyPreCommits, arg.BatchKey, arg.GroupKey, arg.TaprootInternalKeyRaw) if err != nil { return nil, err } defer rows.Close() - var items []FetchMintAnchorUniCommitmentRow + var items []FetchMintSupplyPreCommitsRow for rows.Next() { - var i FetchMintAnchorUniCommitmentRow + var i FetchMintSupplyPreCommitsRow if err := rows.Scan( &i.ID, &i.BatchID, @@ -1660,6 +1662,7 @@ func (q *Queries) FetchMintAnchorUniCommitment(ctx context.Context, arg FetchMin &i.SpentBy, &i.BatchKey, &i.TaprootInternalKeyID, + &i.Outpoint, &i.InternalKey.KeyID, &i.InternalKey.RawKey, &i.InternalKey.KeyFamily, @@ -3237,20 +3240,24 @@ func (q *Queries) UpsertManagedUTXO(ctx context.Context, arg UpsertManagedUTXOPa return utxo_id, err } -const UpsertMintAnchorUniCommitment = `-- name: UpsertMintAnchorUniCommitment :one +const UpsertMintSupplyPreCommit = `-- name: UpsertMintSupplyPreCommit :one WITH target_batch AS ( -- This CTE is used to fetch the ID of a batch, based on the serialized -- internal key associated with the batch. - SELECT keys.key_id AS batch_id - FROM internal_keys keys - WHERE keys.raw_key = $6 + -- Only yield a batch_id when the batch actually exists. + SELECT b.batch_id + FROM asset_minting_batches AS b + JOIN internal_keys AS k ON k.key_id = b.batch_id + WHERE k.raw_key = $6 ) -INSERT INTO mint_anchor_uni_commitments ( - batch_id, tx_output_index, taproot_internal_key_id, group_key, spent_by, outpoint +INSERT INTO mint_supply_pre_commits ( + batch_id, tx_output_index, taproot_internal_key_id, group_key, spent_by, + outpoint ) VALUES ( - (SELECT batch_id FROM target_batch), $1, - $2, $3, $4, $5 + (SELECT batch_id FROM target_batch), $1, + $2, $3, $4, + $5 ) ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET -- The following fields are updated if a conflict occurs. @@ -3260,7 +3267,7 @@ ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET RETURNING id ` -type UpsertMintAnchorUniCommitmentParams struct { +type UpsertMintSupplyPreCommitParams struct { TxOutputIndex int32 TaprootInternalKeyID int64 GroupKey []byte @@ -3269,11 +3276,15 @@ type UpsertMintAnchorUniCommitmentParams struct { BatchKey []byte } -// Upsert a record into the mint_anchor_uni_commitments table. -// If a record with the same batch ID and tx output index already exists, update -// the existing record. Otherwise, insert a new record. -func (q *Queries) UpsertMintAnchorUniCommitment(ctx context.Context, arg UpsertMintAnchorUniCommitmentParams) (int64, error) { - row := q.db.QueryRowContext(ctx, UpsertMintAnchorUniCommitment, +// Upsert a supply pre-commit that is tied to a minting batch. +// The batch is resolved from @batch_key +// (internal_keys -> asset_minting_batches). +// The key is (batch_id, tx_output_index), where tx_output_index is the +// pre-commit output index in the batch’s mint anchor transaction. +// If a row exists for the same batch and index, update non-key fields only; +// the batch association is not changed. +func (q *Queries) UpsertMintSupplyPreCommit(ctx context.Context, arg UpsertMintSupplyPreCommitParams) (int64, error) { + row := q.db.QueryRowContext(ctx, UpsertMintSupplyPreCommit, arg.TxOutputIndex, arg.TaprootInternalKeyID, arg.GroupKey, @@ -3333,6 +3344,52 @@ func (q *Queries) UpsertScriptKey(ctx context.Context, arg UpsertScriptKeyParams return script_key_id, err } +const UpsertSupplyPreCommit = `-- name: UpsertSupplyPreCommit :one +INSERT INTO supply_pre_commits ( + group_key, + taproot_internal_key, + outpoint, + chain_txn_db_id, + spent_by +) +VALUES ( + $1, + $2, + $3, + $4, + $5 +) +ON CONFLICT(outpoint) DO UPDATE SET + group_key = EXCLUDED.group_key, + taproot_internal_key = EXCLUDED.taproot_internal_key, + outpoint = EXCLUDED.outpoint, + chain_txn_db_id = EXCLUDED.chain_txn_db_id, + spent_by = EXCLUDED.spent_by +RETURNING id +` + +type UpsertSupplyPreCommitParams struct { + GroupKey []byte + TaprootInternalKey []byte + Outpoint []byte + ChainTxnDbID int64 + SpentBy sql.NullInt64 +} + +// Upsert a supply pre-commit output that is not tied to a minting batch. +func (q *Queries) UpsertSupplyPreCommit(ctx context.Context, arg UpsertSupplyPreCommitParams) (int64, error) { + row := q.db.QueryRowContext(ctx, UpsertSupplyPreCommit, + arg.GroupKey, + arg.TaprootInternalKey, + arg.Outpoint, + arg.ChainTxnDbID, + arg.SpentBy, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + const UpsertTapscriptTreeEdge = `-- name: UpsertTapscriptTreeEdge :one INSERT INTO tapscript_edges ( root_hash_id, node_index, raw_node_id diff --git a/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.down.sql b/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.down.sql new file mode 100644 index 000000000..d23075d48 --- /dev/null +++ b/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.down.sql @@ -0,0 +1,8 @@ +-- Undo table rename. +ALTER TABLE mint_supply_pre_commits + RENAME TO mint_anchor_uni_commitments; + +-- Drop the new supply_pre_commits table. +DROP INDEX IF EXISTS supply_pre_commits_idx_group_key; +DROP INDEX IF EXISTS supply_pre_commits_unique_outpoint; +DROP TABLE IF EXISTS supply_pre_commits; \ No newline at end of file diff --git a/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.up.sql b/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.up.sql new file mode 100644 index 000000000..64e4a4376 --- /dev/null +++ b/tapdb/sqlc/migrations/000046_supply_pre_commit_tables.up.sql @@ -0,0 +1,33 @@ +-- Rename the table to align with "supply" terminology. +ALTER TABLE mint_anchor_uni_commitments + RENAME TO mint_supply_pre_commits; + +-- Add a new table to track supply pre-commitments generated by +CREATE TABLE supply_pre_commits ( + id INTEGER PRIMARY KEY, + + -- The asset group key for this supply pre-commitment. + -- Stored in canonical 32-byte x-only form as defined in BIP340 + -- (schnorr.SerializePubKey). + group_key BLOB NOT NULL CHECK(length(group_key) = 32), + + -- The taproot internal key of the pre-commitment transaction output. + taproot_internal_key BLOB NOT NULL CHECK(length(taproot_internal_key) = 33), + + -- The pre-commit outpoint from the mint anchor transaction. + outpoint BLOB NOT NULL CHECK(length(outpoint) > 0), + + -- The chain transaction that included this pre-commitment output. + chain_txn_db_id BIGINT NOT NULL REFERENCES chain_txns(txn_id), + + -- Reference to supply commitment which spends this pre-commitment. + spent_by BIGINT REFERENCES supply_commitments(commit_id) +); + +-- Create a unique index on outpoint. +CREATE UNIQUE INDEX supply_pre_commits_unique_outpoint + ON supply_pre_commits(outpoint); + +-- Create an index on group_key for faster lookups. +CREATE INDEX supply_pre_commits_idx_group_key + ON supply_pre_commits(group_key); diff --git a/tapdb/sqlc/models.go b/tapdb/sqlc/models.go index fb1e3aeb2..8c6f374d8 100644 --- a/tapdb/sqlc/models.go +++ b/tapdb/sqlc/models.go @@ -304,7 +304,7 @@ type ManagedUtxo struct { RootVersion sql.NullInt16 } -type MintAnchorUniCommitment struct { +type MintSupplyPreCommit struct { ID int64 BatchID int32 TxOutputIndex int32 @@ -415,6 +415,15 @@ type SupplyCommitment struct { SpentCommitment sql.NullInt64 } +type SupplyPreCommit struct { + ID int64 + GroupKey []byte + TaprootInternalKey []byte + Outpoint []byte + ChainTxnDbID int64 + SpentBy sql.NullInt64 +} + type SupplySyncerPushLog struct { ID int64 GroupKey []byte diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go index c9cc85d3b..f74c59523 100644 --- a/tapdb/sqlc/querier.go +++ b/tapdb/sqlc/querier.go @@ -90,9 +90,9 @@ type Querier interface { FetchInternalKeyLocator(ctx context.Context, rawKey []byte) (FetchInternalKeyLocatorRow, error) FetchManagedUTXO(ctx context.Context, arg FetchManagedUTXOParams) (FetchManagedUTXORow, error) FetchManagedUTXOs(ctx context.Context) ([]FetchManagedUTXOsRow, error) - // Fetch records from the mint_anchor_uni_commitments table with optional + // Fetch records from the supply_pre_commits table with optional // filtering. - FetchMintAnchorUniCommitment(ctx context.Context, arg FetchMintAnchorUniCommitmentParams) ([]FetchMintAnchorUniCommitmentRow, error) + FetchMintSupplyPreCommits(ctx context.Context, arg FetchMintSupplyPreCommitsParams) ([]FetchMintSupplyPreCommitsRow, error) FetchMintingBatch(ctx context.Context, rawKey []byte) (FetchMintingBatchRow, error) FetchMintingBatchesByInverseState(ctx context.Context, batchState int16) ([]FetchMintingBatchesByInverseStateRow, error) FetchMultiverseRoot(ctx context.Context, namespaceRoot string) (FetchMultiverseRootRow, error) @@ -114,9 +114,15 @@ type Querier interface { FetchUniverseRoot(ctx context.Context, namespace string) (FetchUniverseRootRow, error) FetchUniverseSupplyRoot(ctx context.Context, namespaceRoot string) (FetchUniverseSupplyRootRow, error) FetchUnknownTypeScriptKeys(ctx context.Context) ([]FetchUnknownTypeScriptKeysRow, error) - // Fetch unspent pre-commitment outputs. A pre-commitment output is a mint - // anchor transaction output which relates to the supply commitment feature. - FetchUnspentPrecommits(ctx context.Context, groupKey []byte) ([]FetchUnspentPrecommitsRow, error) + // Fetch unspent supply pre-commitment outputs. Each pre-commitment output + // comes from a mint anchor transaction and relates to an asset issuance + // where the local node acted as the issuer. + FetchUnspentMintSupplyPreCommits(ctx context.Context, groupKey []byte) ([]FetchUnspentMintSupplyPreCommitsRow, error) + // Fetch unspent supply pre-commitment outputs. Each pre-commitment output + // comes from a mint anchor transaction and relates to an asset issuance + // where a peer node acted as the issuer. Rows in this table do not relate to an + // issuance where the local node acted as the issuer. + FetchUnspentSupplyPreCommits(ctx context.Context, groupKey []byte) ([]FetchUnspentSupplyPreCommitsRow, error) FinalizeSupplyCommitTransition(ctx context.Context, transitionID int64) error FreezePendingTransition(ctx context.Context, groupKey []byte) error GenesisAssets(ctx context.Context) ([]GenesisAsset, error) @@ -149,8 +155,10 @@ type Querier interface { LinkDanglingSupplyUpdateEvents(ctx context.Context, arg LinkDanglingSupplyUpdateEventsParams) error LogProofTransferAttempt(ctx context.Context, arg LogProofTransferAttemptParams) error LogServerSync(ctx context.Context, arg LogServerSyncParams) error - // Mark a specific pre-commitment output as spent by its outpoint. - MarkPreCommitmentSpentByOutpoint(ctx context.Context, arg MarkPreCommitmentSpentByOutpointParams) error + // Mark a supply pre-commitment output as spent by its outpoint. The + // pre-commitment corresponds to an asset issuance where the local node acted as + // the issuer. + MarkMintPreCommitSpentByOutpoint(ctx context.Context, arg MarkMintPreCommitSpentByOutpointParams) error NewMintingBatch(ctx context.Context, arg NewMintingBatchParams) error QueryAddr(ctx context.Context, arg QueryAddrParams) (QueryAddrRow, error) // We use a LEFT JOIN here as not every asset has a group key, so this'll @@ -233,10 +241,14 @@ type Querier interface { UpsertGenesisPoint(ctx context.Context, prevOut []byte) (int64, error) UpsertInternalKey(ctx context.Context, arg UpsertInternalKeyParams) (int64, error) UpsertManagedUTXO(ctx context.Context, arg UpsertManagedUTXOParams) (int64, error) - // Upsert a record into the mint_anchor_uni_commitments table. - // If a record with the same batch ID and tx output index already exists, update - // the existing record. Otherwise, insert a new record. - UpsertMintAnchorUniCommitment(ctx context.Context, arg UpsertMintAnchorUniCommitmentParams) (int64, error) + // Upsert a supply pre-commit that is tied to a minting batch. + // The batch is resolved from @batch_key + // (internal_keys -> asset_minting_batches). + // The key is (batch_id, tx_output_index), where tx_output_index is the + // pre-commit output index in the batch’s mint anchor transaction. + // If a row exists for the same batch and index, update non-key fields only; + // the batch association is not changed. + UpsertMintSupplyPreCommit(ctx context.Context, arg UpsertMintSupplyPreCommitParams) (int64, error) UpsertMultiverseLeaf(ctx context.Context, arg UpsertMultiverseLeafParams) (int64, error) UpsertMultiverseRoot(ctx context.Context, arg UpsertMultiverseRootParams) (int64, error) UpsertRootNode(ctx context.Context, arg UpsertRootNodeParams) error @@ -244,6 +256,8 @@ type Querier interface { // Return the ID of the state that was actually set (either inserted or updated), // and the latest commitment ID that was set. UpsertSupplyCommitStateMachine(ctx context.Context, arg UpsertSupplyCommitStateMachineParams) (UpsertSupplyCommitStateMachineRow, error) + // Upsert a supply pre-commit output that is not tied to a minting batch. + UpsertSupplyPreCommit(ctx context.Context, arg UpsertSupplyPreCommitParams) (int64, error) UpsertTapscriptTreeEdge(ctx context.Context, arg UpsertTapscriptTreeEdgeParams) (int64, error) UpsertTapscriptTreeNode(ctx context.Context, rawNode []byte) (int64, error) UpsertTapscriptTreeRootHash(ctx context.Context, arg UpsertTapscriptTreeRootHashParams) (int64, error) diff --git a/tapdb/sqlc/queries/assets.sql b/tapdb/sqlc/queries/assets.sql index 9628f3032..dfce2d3ac 100644 --- a/tapdb/sqlc/queries/assets.sql +++ b/tapdb/sqlc/queries/assets.sql @@ -1062,23 +1062,55 @@ JOIN genesis_assets ON genesis_assets.meta_data_id = assets_meta.meta_id ORDER BY assets_meta.meta_id; --- name: UpsertMintAnchorUniCommitment :one --- Upsert a record into the mint_anchor_uni_commitments table. --- If a record with the same batch ID and tx output index already exists, update --- the existing record. Otherwise, insert a new record. +-- name: UpsertSupplyPreCommit :one +-- Upsert a supply pre-commit output that is not tied to a minting batch. +INSERT INTO supply_pre_commits ( + group_key, + taproot_internal_key, + outpoint, + chain_txn_db_id, + spent_by +) +VALUES ( + @group_key, + @taproot_internal_key, + @outpoint, + @chain_txn_db_id, + sqlc.narg('spent_by') +) +ON CONFLICT(outpoint) DO UPDATE SET + group_key = EXCLUDED.group_key, + taproot_internal_key = EXCLUDED.taproot_internal_key, + outpoint = EXCLUDED.outpoint, + chain_txn_db_id = EXCLUDED.chain_txn_db_id, + spent_by = EXCLUDED.spent_by +RETURNING id; + +-- name: UpsertMintSupplyPreCommit :one +-- Upsert a supply pre-commit that is tied to a minting batch. +-- The batch is resolved from @batch_key +-- (internal_keys -> asset_minting_batches). +-- The key is (batch_id, tx_output_index), where tx_output_index is the +-- pre-commit output index in the batch’s mint anchor transaction. +-- If a row exists for the same batch and index, update non-key fields only; +-- the batch association is not changed. WITH target_batch AS ( -- This CTE is used to fetch the ID of a batch, based on the serialized -- internal key associated with the batch. - SELECT keys.key_id AS batch_id - FROM internal_keys keys - WHERE keys.raw_key = @batch_key + -- Only yield a batch_id when the batch actually exists. + SELECT b.batch_id + FROM asset_minting_batches AS b + JOIN internal_keys AS k ON k.key_id = b.batch_id + WHERE k.raw_key = @batch_key ) -INSERT INTO mint_anchor_uni_commitments ( - batch_id, tx_output_index, taproot_internal_key_id, group_key, spent_by, outpoint +INSERT INTO mint_supply_pre_commits ( + batch_id, tx_output_index, taproot_internal_key_id, group_key, spent_by, + outpoint ) VALUES ( - (SELECT batch_id FROM target_batch), @tx_output_index, - @taproot_internal_key_id, @group_key, sqlc.narg('spent_by'), sqlc.narg('outpoint') + (SELECT batch_id FROM target_batch), @tx_output_index, + @taproot_internal_key_id, @group_key, sqlc.narg('spent_by'), + @outpoint ) ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET -- The following fields are updated if a conflict occurs. @@ -1087,27 +1119,28 @@ ON CONFLICT(batch_id, tx_output_index) DO UPDATE SET outpoint = EXCLUDED.outpoint RETURNING id; --- name: FetchMintAnchorUniCommitment :many --- Fetch records from the mint_anchor_uni_commitments table with optional +-- name: FetchMintSupplyPreCommits :many +-- Fetch records from the supply_pre_commits table with optional -- filtering. SELECT - mint_anchor_uni_commitments.id, - mint_anchor_uni_commitments.batch_id, - mint_anchor_uni_commitments.tx_output_index, - mint_anchor_uni_commitments.group_key, - mint_anchor_uni_commitments.spent_by, + precommits.id, + precommits.batch_id, + precommits.tx_output_index, + precommits.group_key, + precommits.spent_by, batch_internal_keys.raw_key AS batch_key, - mint_anchor_uni_commitments.taproot_internal_key_id, + precommits.taproot_internal_key_id, + precommits.outpoint, sqlc.embed(taproot_internal_keys) -FROM mint_anchor_uni_commitments +FROM mint_supply_pre_commits AS precommits JOIN internal_keys taproot_internal_keys - ON mint_anchor_uni_commitments.taproot_internal_key_id = taproot_internal_keys.key_id + ON precommits.taproot_internal_key_id = taproot_internal_keys.key_id LEFT JOIN asset_minting_batches batches - ON mint_anchor_uni_commitments.batch_id = batches.batch_id + ON precommits.batch_id = batches.batch_id LEFT JOIN internal_keys batch_internal_keys ON batches.batch_id = batch_internal_keys.key_id WHERE ( (batch_internal_keys.raw_key = sqlc.narg('batch_key') OR sqlc.narg('batch_key') IS NULL) AND - (mint_anchor_uni_commitments.group_key = sqlc.narg('group_key') OR sqlc.narg('group_key') IS NULL) AND + (precommits.group_key = sqlc.narg('group_key') OR sqlc.narg('group_key') IS NULL) AND (taproot_internal_keys.raw_key = sqlc.narg('taproot_internal_key_raw') OR sqlc.narg('taproot_internal_key_raw') IS NULL) ); diff --git a/tapdb/sqlc/queries/supply_commit.sql b/tapdb/sqlc/queries/supply_commit.sql index 8cccddc73..b5c0eec8b 100644 --- a/tapdb/sqlc/queries/supply_commit.sql +++ b/tapdb/sqlc/queries/supply_commit.sql @@ -194,16 +194,34 @@ WHERE transition_id = @transition_id; DELETE FROM supply_update_events WHERE transition_id = @transition_id; --- name: FetchUnspentPrecommits :many --- Fetch unspent pre-commitment outputs. A pre-commitment output is a mint --- anchor transaction output which relates to the supply commitment feature. +-- name: FetchUnspentSupplyPreCommits :many +-- Fetch unspent supply pre-commitment outputs. Each pre-commitment output +-- comes from a mint anchor transaction and relates to an asset issuance +-- where a peer node acted as the issuer. Rows in this table do not relate to an +-- issuance where the local node acted as the issuer. +SELECT + chain_txns.block_height, + chain_txns.raw_tx, + pre_commit.outpoint, + pre_commit.taproot_internal_key, + pre_commit.group_key +FROM supply_pre_commits pre_commit + JOIN chain_txns ON pre_commit.chain_txn_db_id = chain_txns.txn_id +WHERE + pre_commit.group_key = @group_key AND + pre_commit.spent_by IS NULL; + +-- name: FetchUnspentMintSupplyPreCommits :many +-- Fetch unspent supply pre-commitment outputs. Each pre-commitment output +-- comes from a mint anchor transaction and relates to an asset issuance +-- where the local node acted as the issuer. SELECT mac.tx_output_index, sqlc.embed(ik), mac.group_key, mint_txn.block_height, mint_txn.raw_tx -FROM mint_anchor_uni_commitments mac +FROM mint_supply_pre_commits mac JOIN asset_minting_batches amb ON mac.batch_id = amb.batch_id JOIN genesis_points gp ON amb.genesis_id = gp.genesis_id JOIN chain_txns mint_txn ON gp.anchor_tx_id = mint_txn.txn_id @@ -214,9 +232,11 @@ WHERE mac.group_key = @group_key AND (mac.spent_by IS NULL OR commit_txn.block_hash IS NULL); --- name: MarkPreCommitmentSpentByOutpoint :exec --- Mark a specific pre-commitment output as spent by its outpoint. -UPDATE mint_anchor_uni_commitments +-- name: MarkMintPreCommitSpentByOutpoint :exec +-- Mark a supply pre-commitment output as spent by its outpoint. The +-- pre-commitment corresponds to an asset issuance where the local node acted as +-- the issuer. +UPDATE mint_supply_pre_commits SET spent_by = @spent_by_commit_id WHERE outpoint = @outpoint AND spent_by IS NULL; diff --git a/tapdb/sqlc/schemas/generated_schema.sql b/tapdb/sqlc/schemas/generated_schema.sql index 0d686d16e..acb519e8b 100644 --- a/tapdb/sqlc/schemas/generated_schema.sql +++ b/tapdb/sqlc/schemas/generated_schema.sql @@ -624,7 +624,14 @@ CREATE TABLE managed_utxos ( lease_expiry TIMESTAMP , root_version SMALLINT); -CREATE TABLE mint_anchor_uni_commitments ( +CREATE INDEX mint_anchor_uni_commitments_outpoint_idx + ON "mint_supply_pre_commits"(outpoint) + WHERE outpoint IS NOT NULL; + +CREATE UNIQUE INDEX mint_anchor_uni_commitments_unique + ON "mint_supply_pre_commits" (batch_id, tx_output_index); + +CREATE TABLE "mint_supply_pre_commits" ( id INTEGER PRIMARY KEY, -- The ID of the minting batch this universe commitment relates to. @@ -639,13 +646,6 @@ CREATE TABLE mint_anchor_uni_commitments ( BIGINT REFERENCES internal_keys(key_id) NOT NULL, spent_by BIGINT REFERENCES supply_commitments(commit_id), outpoint BLOB); -CREATE INDEX mint_anchor_uni_commitments_outpoint_idx - ON mint_anchor_uni_commitments(outpoint) - WHERE outpoint IS NOT NULL; - -CREATE UNIQUE INDEX mint_anchor_uni_commitments_unique - ON mint_anchor_uni_commitments (batch_id, tx_output_index); - CREATE TABLE mssmt_nodes ( -- hash_key is the hash key by which we reference all nodes. hash_key BLOB NOT NULL, @@ -892,6 +892,33 @@ CREATE UNIQUE INDEX supply_commitments_outpoint_uk CREATE INDEX supply_commitments_spent_commitment_idx ON supply_commitments(spent_commitment); +CREATE TABLE supply_pre_commits ( + id INTEGER PRIMARY KEY, + + -- The asset group key for this supply pre-commitment. + -- Stored in canonical 32-byte x-only form as defined in BIP340 + -- (schnorr.SerializePubKey). + group_key BLOB NOT NULL CHECK(length(group_key) = 32), + + -- The taproot internal key of the pre-commitment transaction output. + taproot_internal_key BLOB NOT NULL CHECK(length(taproot_internal_key) = 33), + + -- The pre-commit outpoint from the mint anchor transaction. + outpoint BLOB NOT NULL CHECK(length(outpoint) > 0), + + -- The chain transaction that included this pre-commitment output. + chain_txn_db_id BIGINT NOT NULL REFERENCES chain_txns(txn_id), + + -- Reference to supply commitment which spends this pre-commitment. + spent_by BIGINT REFERENCES supply_commitments(commit_id) +); + +CREATE INDEX supply_pre_commits_idx_group_key + ON supply_pre_commits(group_key); + +CREATE UNIQUE INDEX supply_pre_commits_unique_outpoint + ON supply_pre_commits(outpoint); + CREATE TABLE supply_syncer_push_log ( id INTEGER PRIMARY KEY, diff --git a/tapdb/sqlc/supply_commit.sql.go b/tapdb/sqlc/supply_commit.sql.go index d33ae0104..960aa28ff 100644 --- a/tapdb/sqlc/supply_commit.sql.go +++ b/tapdb/sqlc/supply_commit.sql.go @@ -108,14 +108,14 @@ func (q *Queries) FetchSupplyCommit(ctx context.Context, groupKey []byte) (Fetch return i, err } -const FetchUnspentPrecommits = `-- name: FetchUnspentPrecommits :many +const FetchUnspentMintSupplyPreCommits = `-- name: FetchUnspentMintSupplyPreCommits :many SELECT mac.tx_output_index, ik.key_id, ik.raw_key, ik.key_family, ik.key_index, mac.group_key, mint_txn.block_height, mint_txn.raw_tx -FROM mint_anchor_uni_commitments mac +FROM mint_supply_pre_commits mac JOIN asset_minting_batches amb ON mac.batch_id = amb.batch_id JOIN genesis_points gp ON amb.genesis_id = gp.genesis_id JOIN chain_txns mint_txn ON gp.anchor_tx_id = mint_txn.txn_id @@ -127,7 +127,7 @@ WHERE (mac.spent_by IS NULL OR commit_txn.block_hash IS NULL) ` -type FetchUnspentPrecommitsRow struct { +type FetchUnspentMintSupplyPreCommitsRow struct { TxOutputIndex int32 InternalKey InternalKey GroupKey []byte @@ -135,17 +135,18 @@ type FetchUnspentPrecommitsRow struct { RawTx []byte } -// Fetch unspent pre-commitment outputs. A pre-commitment output is a mint -// anchor transaction output which relates to the supply commitment feature. -func (q *Queries) FetchUnspentPrecommits(ctx context.Context, groupKey []byte) ([]FetchUnspentPrecommitsRow, error) { - rows, err := q.db.QueryContext(ctx, FetchUnspentPrecommits, groupKey) +// Fetch unspent supply pre-commitment outputs. Each pre-commitment output +// comes from a mint anchor transaction and relates to an asset issuance +// where the local node acted as the issuer. +func (q *Queries) FetchUnspentMintSupplyPreCommits(ctx context.Context, groupKey []byte) ([]FetchUnspentMintSupplyPreCommitsRow, error) { + rows, err := q.db.QueryContext(ctx, FetchUnspentMintSupplyPreCommits, groupKey) if err != nil { return nil, err } defer rows.Close() - var items []FetchUnspentPrecommitsRow + var items []FetchUnspentMintSupplyPreCommitsRow for rows.Next() { - var i FetchUnspentPrecommitsRow + var i FetchUnspentMintSupplyPreCommitsRow if err := rows.Scan( &i.TxOutputIndex, &i.InternalKey.KeyID, @@ -169,6 +170,61 @@ func (q *Queries) FetchUnspentPrecommits(ctx context.Context, groupKey []byte) ( return items, nil } +const FetchUnspentSupplyPreCommits = `-- name: FetchUnspentSupplyPreCommits :many +SELECT + chain_txns.block_height, + chain_txns.raw_tx, + pre_commit.outpoint, + pre_commit.taproot_internal_key, + pre_commit.group_key +FROM supply_pre_commits pre_commit + JOIN chain_txns ON pre_commit.chain_txn_db_id = chain_txns.txn_id +WHERE + pre_commit.group_key = $1 AND + pre_commit.spent_by IS NULL +` + +type FetchUnspentSupplyPreCommitsRow struct { + BlockHeight sql.NullInt32 + RawTx []byte + Outpoint []byte + TaprootInternalKey []byte + GroupKey []byte +} + +// Fetch unspent supply pre-commitment outputs. Each pre-commitment output +// comes from a mint anchor transaction and relates to an asset issuance +// where a peer node acted as the issuer. Rows in this table do not relate to an +// issuance where the local node acted as the issuer. +func (q *Queries) FetchUnspentSupplyPreCommits(ctx context.Context, groupKey []byte) ([]FetchUnspentSupplyPreCommitsRow, error) { + rows, err := q.db.QueryContext(ctx, FetchUnspentSupplyPreCommits, groupKey) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchUnspentSupplyPreCommitsRow + for rows.Next() { + var i FetchUnspentSupplyPreCommitsRow + if err := rows.Scan( + &i.BlockHeight, + &i.RawTx, + &i.Outpoint, + &i.TaprootInternalKey, + &i.GroupKey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const FinalizeSupplyCommitTransition = `-- name: FinalizeSupplyCommitTransition :exec UPDATE supply_commit_transitions SET finalized = TRUE @@ -310,21 +366,23 @@ func (q *Queries) LinkDanglingSupplyUpdateEvents(ctx context.Context, arg LinkDa return err } -const MarkPreCommitmentSpentByOutpoint = `-- name: MarkPreCommitmentSpentByOutpoint :exec -UPDATE mint_anchor_uni_commitments +const MarkMintPreCommitSpentByOutpoint = `-- name: MarkMintPreCommitSpentByOutpoint :exec +UPDATE mint_supply_pre_commits SET spent_by = $1 WHERE outpoint = $2 AND spent_by IS NULL ` -type MarkPreCommitmentSpentByOutpointParams struct { +type MarkMintPreCommitSpentByOutpointParams struct { SpentByCommitID sql.NullInt64 Outpoint []byte } -// Mark a specific pre-commitment output as spent by its outpoint. -func (q *Queries) MarkPreCommitmentSpentByOutpoint(ctx context.Context, arg MarkPreCommitmentSpentByOutpointParams) error { - _, err := q.db.ExecContext(ctx, MarkPreCommitmentSpentByOutpoint, arg.SpentByCommitID, arg.Outpoint) +// Mark a supply pre-commitment output as spent by its outpoint. The +// pre-commitment corresponds to an asset issuance where the local node acted as +// the issuer. +func (q *Queries) MarkMintPreCommitSpentByOutpoint(ctx context.Context, arg MarkMintPreCommitSpentByOutpointParams) error { + _, err := q.db.ExecContext(ctx, MarkMintPreCommitSpentByOutpoint, arg.SpentByCommitID, arg.Outpoint) return err } diff --git a/tapdb/supply_commit.go b/tapdb/supply_commit.go index 033bb8788..3603f5b62 100644 --- a/tapdb/supply_commit.go +++ b/tapdb/supply_commit.go @@ -10,6 +10,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/asset" @@ -20,13 +21,20 @@ import ( "github.com/lightninglabs/taproot-assets/universe/supplycommit" "github.com/lightninglabs/taproot-assets/universe/supplyverifier" lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnutils" ) type ( - // UnspentPrecommits is an alias for the sqlc type representing an - // unspent pre-commitment row. - UnspentPrecommits = sqlc.FetchUnspentPrecommitsRow + // UnspentMintPreCommits is an alias for the sqlc type representing an + // unspent supply pre-commitment row where the local node was the + // issuer. + UnspentMintPreCommits = sqlc.FetchUnspentMintSupplyPreCommitsRow + + // UnspentPreCommits is an alias for the sqlc type representing an + // unspent supply pre-commitment row where a remote node was the + // issuer. + UnspentPreCommits = sqlc.FetchUnspentSupplyPreCommitsRow // SupplyCommit is an alias for the sqlc type. SupplyCommit = sqlc.FetchSupplyCommitRow @@ -103,10 +111,17 @@ type SupplyCommitStore interface { TreeStore BaseUniverseStore - // FetchUnspentPrecommits fetches all unspent pre-commitments for a - // given group key. - FetchUnspentPrecommits(ctx context.Context, - groupKey []byte) ([]UnspentPrecommits, error) + // FetchUnspentMintSupplyPreCommits fetches all unspent supply + // pre-commitments for the specified asset group key where the local + // node was the issuer. + FetchUnspentMintSupplyPreCommits(ctx context.Context, + groupKey []byte) ([]UnspentMintPreCommits, error) + + // FetchUnspentSupplyPreCommits fetches all unspent supply + // pre-commitments for the specified asset group key where a remote + // node was the issuer. + FetchUnspentSupplyPreCommits(ctx context.Context, + groupKey []byte) ([]UnspentPreCommits, error) // FetchSupplyCommit fetches the latest confirmed supply commitment for // a given group key. @@ -212,10 +227,11 @@ type SupplyCommitStore interface { FinalizeSupplyCommitTransition(ctx context.Context, transitionID int64) error - // MarkPreCommitmentSpentByOutpoint marks a pre-commitment as spent - // by its outpoint. - MarkPreCommitmentSpentByOutpoint(ctx context.Context, - arg sqlc.MarkPreCommitmentSpentByOutpointParams) error + // MarkMintPreCommitSpentByOutpoint marks a supply pre-commitment as + // spent by its outpoint. The pre-commitment corresponds to an asset + // issuance where the local node acted as the issuer. + MarkMintPreCommitSpentByOutpoint(ctx context.Context, + arg sqlc.MarkMintPreCommitSpentByOutpointParams) error // QueryExistingPendingTransition fetches the ID of an existing // non-finalized transition for a group key. Returns sql.ErrNoRows if @@ -265,7 +281,8 @@ func NewSupplyCommitMachine(db BatchedSupplyCommitStore) *SupplyCommitMachine { // asset spec. The asset spec will only specify a group key, and not also an // asset ID. func (s *SupplyCommitMachine) UnspentPrecommits(ctx context.Context, - assetSpec asset.Specifier) lfn.Result[supplycommit.PreCommits] { + assetSpec asset.Specifier, + localIssuerOnly bool) lfn.Result[supplycommit.PreCommits] { groupKey := assetSpec.UnwrapGroupKeyToPtr() if groupKey == nil { @@ -276,21 +293,25 @@ func (s *SupplyCommitMachine) UnspentPrecommits(ctx context.Context, var preCommits supplycommit.PreCommits readTx := ReadTxOption() dbErr := s.db.ExecTx(ctx, readTx, func(db SupplyCommitStore) error { - rows, err := db.FetchUnspentPrecommits(ctx, groupKeyBytes) - if err != nil { - // It's okay if there are no unspent pre-commits. - if errors.Is(err, sql.ErrNoRows) { - return nil - } - return fmt.Errorf("error fetching unspent "+ - "precommits: %w", err) + mintRows, err := db.FetchUnspentMintSupplyPreCommits( + ctx, groupKeyBytes, + ) + switch { + case errors.Is(err, sql.ErrNoRows): + // No unspent pre-commits minted by this local node + // exist for this group key. Proceed to query for + // pre-commits from other issuers. + + case err != nil: + return fmt.Errorf("failed to fetch unspent local node "+ + "issued pre-commit outputs: %w", err) } // For each pre-commitment, parse the internal key and group // key, and assemble the final struct as needed by the // interface. - preCommits = make(supplycommit.PreCommits, 0, len(rows)) - for _, row := range rows { + preCommits = make(supplycommit.PreCommits, 0, len(mintRows)) + for _, row := range mintRows { internalKey, err := parseInternalKey(row.InternalKey) if err != nil { return fmt.Errorf("failed to parse "+ @@ -322,6 +343,76 @@ func (s *SupplyCommitMachine) UnspentPrecommits(ctx context.Context, preCommits = append(preCommits, preCommit) } + // If any pre-commits were found where we acted as the issuer, + // return early and skip querying for pre-commits from other + // issuers. Also return early if the caller explicitly requested + // only pre-commits issued by the local node. + if len(preCommits) > 0 || localIssuerOnly { + return nil + } + + // No pre-commits found where we were the issuer. So now + // we'll query for pre-commits from other issuers. + rows, err := db.FetchUnspentSupplyPreCommits( + ctx, schnorr.SerializePubKey(groupKey), + ) + switch { + case errors.Is(err, sql.ErrNoRows): + // No unspent pre-commits minted by peer issuer nodes + // exist for this group key. Return early. + return nil + + case err != nil: + return fmt.Errorf("failed to fetch unspent remote "+ + "node issued pre-commit outputs: %w", err) + } + + // Parse rows into pre-commitment structs. + for _, row := range rows { + pubKey, err := btcec.ParsePubKey(row.TaprootInternalKey) + if err != nil { + return fmt.Errorf("failed to parse internal "+ + "raw key: %w", err) + } + + internalKey := keychain.KeyDescriptor{ + PubKey: pubKey, + } + + groupPubKey, err := schnorr.ParsePubKey(row.GroupKey) + if err != nil { + return fmt.Errorf("error parsing group key: %w", + err) + } + + var mintingTx wire.MsgTx + err = mintingTx.Deserialize(bytes.NewReader(row.RawTx)) + if err != nil { + return fmt.Errorf("error deserializing "+ + "minting tx: %w", err) + } + + var outpoint wire.OutPoint + err = readOutPoint( + bytes.NewReader(row.Outpoint), 0, 0, &outpoint, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrReadOutpoint, + err) + } + + preCommit := supplycommit.PreCommitment{ + BlockHeight: uint32( + row.BlockHeight.Int32, + ), + MintingTxn: &mintingTx, + OutIdx: outpoint.Index, + InternalKey: internalKey, + GroupPubKey: *groupPubKey, + } + preCommits = append(preCommits, preCommit) + } + return nil }) if dbErr != nil { @@ -1789,8 +1880,8 @@ func (s *SupplyCommitMachine) ApplyStateTransition( txIn.PreviousOutPoint.Index) // Mark this specific pre-commitment as spent. - err = db.MarkPreCommitmentSpentByOutpoint(ctx, - sqlc.MarkPreCommitmentSpentByOutpointParams{ + err = db.MarkMintPreCommitSpentByOutpoint(ctx, + sqlc.MarkMintPreCommitSpentByOutpointParams{ SpentByCommitID: sqlInt64( newCommitmentID, ), diff --git a/tapdb/supply_commit_test.go b/tapdb/supply_commit_test.go index 095803451..b03c457ac 100644 --- a/tapdb/supply_commit_test.go +++ b/tapdb/supply_commit_test.go @@ -242,8 +242,8 @@ func (h *supplyCommitTestHarness) addTestMintAnchorUniCommitment( err = wire.WriteOutPoint(&outpointBuf, 0, 0, &outpoint) require.NoError(h.t, err) - anchorCommitID, err := h.db.UpsertMintAnchorUniCommitment( - h.ctx, sqlc.UpsertMintAnchorUniCommitmentParams{ + anchorCommitID, err := h.db.UpsertMintSupplyPreCommit( + h.ctx, UpsertBatchPreCommitParams{ BatchKey: batchKeyBytes, TxOutputIndex: txOutputIndex, TaprootInternalKeyID: internalKeyID, @@ -1705,7 +1705,9 @@ func TestSupplyCommitApplyStateTransition(t *testing.T) { // Verify we have all three unspent pre-commitments before the // transition. - precommitsRes := h.commitMachine.UnspentPrecommits(h.ctx, h.assetSpec) + precommitsRes := h.commitMachine.UnspentPrecommits( + h.ctx, h.assetSpec, true, + ) precommits, err := precommitsRes.Unpack() require.NoError(t, err) require.Len( @@ -1730,7 +1732,9 @@ func TestSupplyCommitApplyStateTransition(t *testing.T) { // After the first transition, only the two pre-commitments that were // included in the transaction inputs should be marked as spent. // The extra pre-commitment should remain unspent. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, h.assetSpec) + precommitsRes = h.commitMachine.UnspentPrecommits( + h.ctx, h.assetSpec, true, + ) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Len( @@ -1760,7 +1764,9 @@ func TestSupplyCommitApplyStateTransition(t *testing.T) { ) // Verify we have the extra one from before plus the new one. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, h.assetSpec) + precommitsRes = h.commitMachine.UnspentPrecommits( + h.ctx, h.assetSpec, true, + ) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Len( @@ -1783,7 +1789,9 @@ func TestSupplyCommitApplyStateTransition(t *testing.T) { // After the second transition, the new pre-commitment should also be // spent. Finally, verify that no unspent pre-commitments remain. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, h.assetSpec) + precommitsRes = h.commitMachine.UnspentPrecommits( + h.ctx, h.assetSpec, true, + ) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Empty( @@ -1805,7 +1813,7 @@ func TestSupplyCommitUnspentPrecommits(t *testing.T) { ) // To start with, we shouldn't have any precommits. - precommitsRes := h.commitMachine.UnspentPrecommits(h.ctx, spec) + precommitsRes := h.commitMachine.UnspentPrecommits(h.ctx, spec, true) precommits, err := precommitsRes.Unpack() require.NoError(t, err) require.Empty(t, precommits) @@ -1819,7 +1827,7 @@ func TestSupplyCommitUnspentPrecommits(t *testing.T) { ) // At this point, we should find a single pre commitment on disk. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec) + precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec, true) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Len(t, precommits, 1) @@ -1839,7 +1847,7 @@ func TestSupplyCommitUnspentPrecommits(t *testing.T) { ) // We should now find two pre-commitments. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec) + precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec, true) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Len(t, precommits, 2) @@ -1861,7 +1869,7 @@ func TestSupplyCommitUnspentPrecommits(t *testing.T) { // As the transaction was confirmed above, we should now only have a // single pre commitment on disk. - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec) + precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, spec, true) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Len(t, precommits, 1) @@ -1872,14 +1880,18 @@ func TestSupplyCommitUnspentPrecommits(t *testing.T) { otherSpec := asset.NewSpecifierOptionalGroupPubKey( asset.RandID(t), otherGroupKey, ) - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, otherSpec) + precommitsRes = h.commitMachine.UnspentPrecommits( + h.ctx, otherSpec, true, + ) precommits, err = precommitsRes.Unpack() require.NoError(t, err) require.Empty(t, precommits) // Finally, trying with a missing group key should yield an error. emptySpec := asset.NewSpecifierOptionalGroupKey(asset.RandID(t), nil) - precommitsRes = h.commitMachine.UnspentPrecommits(h.ctx, emptySpec) + precommitsRes = h.commitMachine.UnspentPrecommits( + h.ctx, emptySpec, true, + ) require.ErrorIs(t, precommitsRes.Err(), ErrMissingGroupKey) } diff --git a/tapdb/supply_tree.go b/tapdb/supply_tree.go index 3134aa976..4644af4ef 100644 --- a/tapdb/supply_tree.go +++ b/tapdb/supply_tree.go @@ -462,8 +462,14 @@ func registerMintSupplyInternal(ctx context.Context, dbTx BaseUniverseStore, subNs := subTreeNamespace(groupKey, supplycommit.MintTreeType) // Upsert the leaf into the mint supply sub-tree SMT and DB. + uniProofType, err := supplycommit.MintTreeType.ToUniverseProofType() + if err != nil { + return nil, fmt.Errorf("unable to map mint supply tree type "+ + "to universe proof type: %w", err) + } + mintSupplyProof, err := universeUpsertProofLeaf( - ctx, dbTx, subNs, supplycommit.MintTreeType.String(), groupKey, + ctx, dbTx, subNs, uniProofType, groupKey, key, leaf, metaReveal, blockHeight, ) if err != nil { diff --git a/tapdb/universe.go b/tapdb/universe.go index 54678177a..e69af4771 100644 --- a/tapdb/universe.go +++ b/tapdb/universe.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapdb/sqlc" "github.com/lightninglabs/taproot-assets/universe" + "github.com/lightninglabs/taproot-assets/universe/supplyverifier" lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/keychain" ) @@ -577,6 +578,146 @@ func upsertAssetGen(ctx context.Context, db UpsertAssetStore, return genAssetID, nil } +// shouldInsertPreCommit determines whether a supply pre-commitment +// output should be inserted for a given issuance proof. +func shouldInsertPreCommit(proofType universe.ProofType, + issuanceProof proof.Proof, metaReveal *proof.MetaReveal) bool { + + // Only issuance proofs can carry supply pre-commitment outputs. + if proofType != universe.ProofTypeIssuance { + return false + } + + // Supply pre-commitment outputs apply only to asset groups. + if issuanceProof.Asset.GroupKey == nil { + return false + } + + // Without metadata, we can't determine pre-commitment support. + if metaReveal == nil { + return false + } + + // If the metadata indicates no supply commitment support, stop here. + if !metaReveal.UniverseCommitments { + return false + } + + // A delegation key is mandatory for supply pre-commitment outputs. + if metaReveal.DelegationKey.IsNone() { + return false + } + + return true +} + +// maybeUpsertSupplyPreCommit inserts a supply pre-commitment output if the +// asset group supports supply commitments and this is an issuance proof. +func maybeUpsertSupplyPreCommit(ctx context.Context, dbTx UpsertAssetStore, + proofType universe.ProofType, issuanceProof proof.Proof, + metaReveal *proof.MetaReveal) error { + + if !shouldInsertPreCommit(proofType, issuanceProof, metaReveal) { + return nil + } + + delegationKey, err := metaReveal.DelegationKey.UnwrapOrErr( + errors.New("missing delegation key"), + ) + if err != nil { + return err + } + + preCommitOutput, err := supplyverifier.ExtractPreCommitOutput( + issuanceProof, delegationKey, + ) + if err != nil { + return fmt.Errorf("unable to extract pre-commit "+ + "output: %w", err) + } + + outPointBytes, err := encodeOutpoint(preCommitOutput.OutPoint()) + if err != nil { + return fmt.Errorf("unable to encode supply pre-commit "+ + "outpoint: %w", err) + } + + // Upsert the supply pre-commitment output. + // + // Encode the group key and taproot internal key. + groupKeyBytes := schnorr.SerializePubKey( + &issuanceProof.Asset.GroupKey.GroupPubKey, + ) + taprootInternalKeyBytes := + preCommitOutput.InternalKey.PubKey.SerializeCompressed() + + // Try to fetch an existing chain tx row from the database. We fetch + // first instead of blindly upserting to avoid overwriting existing data + // with null values. + var chainTxDbID fn.Option[int64] + + txIDBytes := fn.ByteSlice(issuanceProof.AnchorTx.TxHash()) + chainTxn, err := dbTx.FetchChainTx(ctx, txIDBytes) + switch { + case errors.Is(err, sql.ErrNoRows): + // No existing chain tx, we'll insert a new one below. + + case err != nil: + return fmt.Errorf("unable to fetch chain tx: %w", err) + + default: + // We found an existing chain tx. If it has a valid block + // height, then we'll use it. Otherwise, we'll insert a new one + // below. + if chainTxn.BlockHeight.Valid { + chainTxDbID = fn.Some(chainTxn.TxnID) + } + } + + // If we didn't find an existing chain tx, then we'll insert a new + // one now. + if chainTxDbID.IsNone() { + blockHash := issuanceProof.BlockHeader.BlockHash() + txBytes, err := fn.Serialize(&issuanceProof.AnchorTx) + if err != nil { + return fmt.Errorf("failed to serialize tx: %w", err) + } + + txDbID, err := dbTx.UpsertChainTx(ctx, ChainTxParams{ + Txid: txIDBytes, + RawTx: txBytes, + BlockHeight: sqlInt32(issuanceProof.BlockHeight), + BlockHash: blockHash.CloneBytes(), + }) + if err != nil { + return fmt.Errorf("unable to upsert chain tx: %w", err) + } + + chainTxDbID = fn.Some(txDbID) + } + + txDbID, err := chainTxDbID.UnwrapOrErr( + errors.New("missing chain tx db id"), + ) + if err != nil { + return err + } + + _, err = dbTx.UpsertSupplyPreCommit( + ctx, sqlc.UpsertSupplyPreCommitParams{ + TaprootInternalKey: taprootInternalKeyBytes, + GroupKey: groupKeyBytes, + Outpoint: outPointBytes, + ChainTxnDbID: txDbID, + }, + ) + if err != nil { + return fmt.Errorf("unable to upsert supply pre-commit: %w", err) + } + + return nil +} + // UpsertProofLeaf inserts or updates a proof leaf within the universe tree, // stored at the base key. The metaReveal type is purely optional, and should be // specified if the genesis proof committed to a non-zero meta hash. @@ -599,7 +740,7 @@ func (b *BaseUniverseTree) UpsertProofLeaf(ctx context.Context, } issuanceProof, err := universeUpsertProofLeaf( - ctx, dbTx, namespace, b.id.ProofType.String(), + ctx, dbTx, namespace, b.id.ProofType, b.id.GroupKey, key, leaf, metaReveal, blockHeight, ) if err != nil { @@ -728,7 +869,8 @@ func upsertMultiverseLeafEntry(ctx context.Context, dbTx BaseUniverseStore, // NOTE: This function accepts a db transaction, as it's used when making // broader DB updates. func universeUpsertProofLeaf(ctx context.Context, dbTx BaseUniverseStore, - namespace string, proofTypeStr string, groupKey *btcec.PublicKey, + namespace string, proofType universe.ProofType, + groupKey *btcec.PublicKey, key universe.LeafKey, leaf *universe.Leaf, metaReveal *proof.MetaReveal, blockHeight lfn.Option[uint32]) (*universe.Proof, error) { @@ -774,7 +916,7 @@ func universeUpsertProofLeaf(ctx context.Context, dbTx BaseUniverseStore, NamespaceRoot: namespace, AssetID: fn.ByteSlice(leaf.ID()), GroupKey: groupKeyBytes, - ProofType: sqlStr(proofTypeStr), + ProofType: sqlStr(proofType.String()), }) if err != nil { return nil, err @@ -794,6 +936,9 @@ func universeUpsertProofLeaf(ctx context.Context, dbTx BaseUniverseStore, return nil, fmt.Errorf("unable to decode proof: %w", err) } + // Upsert into the DB: the genesis point, asset genesis, + // group key reveal, and the anchoring transaction for the issuance or + // transfer. assetGenID, err := upsertAssetGen( ctx, dbTx, leaf.Genesis, leaf.GroupKey, &leafProof, ) @@ -801,6 +946,17 @@ func universeUpsertProofLeaf(ctx context.Context, dbTx BaseUniverseStore, return nil, err } + // If the asset group supports supply commitments and this is an + // issuance proof, then we may need to log the supply pre-commitment + // output. + err = maybeUpsertSupplyPreCommit( + ctx, dbTx, proofType, leafProof, metaReveal, + ) + if err != nil { + return nil, fmt.Errorf("unable to upsert supply "+ + "pre-commit: %w", err) + } + // If the block height isn't specified, then we'll attempt to extract it // from the proof itself. if blockHeight.IsNone() && leafProof.BlockHeight > 0 { diff --git a/universe/supplycommit/env.go b/universe/supplycommit/env.go index b84176e97..05e06738c 100644 --- a/universe/supplycommit/env.go +++ b/universe/supplycommit/env.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightninglabs/taproot-assets/tapsend" + "github.com/lightninglabs/taproot-assets/universe" lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -69,6 +70,22 @@ func (s SupplySubTree) String() string { } } +// ToUniverseProofType converts the supply subtree type to the corresponding +// universe proof type. +func (s SupplySubTree) ToUniverseProofType() (universe.ProofType, error) { + switch s { + case MintTreeType: + return universe.ProofTypeMintSupply, nil + case BurnTreeType: + return universe.ProofTypeBurn, nil + case IgnoreTreeType: + return universe.ProofTypeIgnore, nil + default: + return universe.ProofTypeUnspecified, fmt.Errorf("unknown "+ + "supply subtree type: %s", s) + } +} + // AllSupplySubTrees contains all possible valid SupplySubTree values. var AllSupplySubTrees = []SupplySubTree{ MintTreeType, @@ -628,8 +645,8 @@ type CommitmentTracker interface { // UnspentPrecommits returns the set of unspent pre-commitments for a // given asset spec. The asset spec will only specify a group key, and // not also an asset ID. - UnspentPrecommits(ctx context.Context, - assetSpec asset.Specifier) lfn.Result[PreCommits] + UnspentPrecommits(ctx context.Context, assetSpec asset.Specifier, + localIssuerOnly bool) lfn.Result[PreCommits] // SupplyCommit returns the root commitment for a given asset spec. From // the PoV of the chain, this is a singleton instance. diff --git a/universe/supplycommit/mock.go b/universe/supplycommit/mock.go index 6660e0a4d..974ff2cf0 100644 --- a/universe/supplycommit/mock.go +++ b/universe/supplycommit/mock.go @@ -66,9 +66,10 @@ type mockCommitmentTracker struct { } func (m *mockCommitmentTracker) UnspentPrecommits(ctx context.Context, - assetSpec asset.Specifier) lfn.Result[PreCommits] { + assetSpec asset.Specifier, + localIssuerOnly bool) lfn.Result[PreCommits] { - args := m.Called(ctx, assetSpec) + args := m.Called(ctx, assetSpec, localIssuerOnly) return args.Get(0).(lfn.Result[PreCommits]) } diff --git a/universe/supplycommit/state_machine_test.go b/universe/supplycommit/state_machine_test.go index 73e4ec83e..b3a62f2a5 100644 --- a/universe/supplycommit/state_machine_test.go +++ b/universe/supplycommit/state_machine_test.go @@ -397,6 +397,7 @@ func (h *supplyCommitTestHarness) expectTreeFetches() { func (h *supplyCommitTestHarness) expectCommitmentFetches() { h.mockCommits.On( "UnspentPrecommits", mock.Anything, mock.Anything, + mock.Anything, ).Return( lfn.Ok[PreCommits](nil), ).Once() diff --git a/universe/supplycommit/transitions.go b/universe/supplycommit/transitions.go index c9b6684f9..9f80837a7 100644 --- a/universe/supplycommit/transitions.go +++ b/universe/supplycommit/transitions.go @@ -719,7 +719,7 @@ func (c *CommitTxCreateState) ProcessEvent(event Event, // commitments, the current supply root (which may not exist), // and the new supply root. preCommits, err := env.Commitments.UnspentPrecommits( - ctx, env.AssetSpec, + ctx, env.AssetSpec, true, ).Unpack() if err != nil { return nil, fmt.Errorf("unable to fetch "+ diff --git a/universe/supplyverifier/env.go b/universe/supplyverifier/env.go index 013ec3400..12b816c78 100644 --- a/universe/supplyverifier/env.go +++ b/universe/supplyverifier/env.go @@ -30,7 +30,8 @@ type SupplyCommitView interface { // UnspentPrecommits returns the set of unspent pre-commitments for a // given asset spec. UnspentPrecommits(ctx context.Context, - assetSpec asset.Specifier) lfn.Result[supplycommit.PreCommits] + assetSpec asset.Specifier, + localIssuerOnly bool) lfn.Result[supplycommit.PreCommits] // SupplyCommit returns the latest supply commitment for a given asset // spec. diff --git a/universe/supplyverifier/verifier.go b/universe/supplyverifier/verifier.go index aa1db54da..ebd89ce78 100644 --- a/universe/supplyverifier/verifier.go +++ b/universe/supplyverifier/verifier.go @@ -97,18 +97,19 @@ func (v *Verifier) ensurePrecommitsSpent(ctx context.Context, // Fetch all unspent pre-commitment outputs for the asset group. allPreCommits, err := v.cfg.SupplyCommitView.UnspentPrecommits( - ctx, assetSpec, + ctx, assetSpec, false, ).Unpack() if err != nil { return fmt.Errorf("unable to fetch unspent pre-commitments: %w", err) } - // TODO(ffranr): If commitment.SpentCommitment is none, then we - // should ensure that at least one pre-commitment is spent. - // Before implementing this check, we need to ensure that - // remote issued supply pre-commitments are correctly populated and - // retrieved from the db. + // If no supply-commitment spend is recorded, require at least one + // unspent mint pre-commitment output for the initial supply commitment. + if commitment.SpentCommitment.IsNone() && len(allPreCommits) == 0 { + return fmt.Errorf("no unspent supply pre-commitment outputs " + + "for the initial supply commitment") + } // Filter pre-commits to only include those that are at block heights // less than or equal to the commitment's anchor block height. All @@ -422,7 +423,7 @@ func (v *Verifier) verifyIssuanceLeaf(ctx context.Context, _, err = issuanceProof.Verify(ctx, nil, lookup, vCtx) if err != nil { - return fmt.Errorf("burn leaf proof failed verification: %w", + return fmt.Errorf("issuance proof failed verification: %w", err) } @@ -439,7 +440,7 @@ func (v *Verifier) verifyIssuanceLeaf(ctx context.Context, } if issuanceLeaf.IsBurn { - return fmt.Errorf("IsBurn is enexpectedly true for issuance " + + return fmt.Errorf("IsBurn is unexpectedly true for issuance " + "leaf") } @@ -462,7 +463,9 @@ func (v *Verifier) verifyIssuanceLeaf(ctx context.Context, return fmt.Errorf("missing group key in issuance leaf") } - if issuanceProof.Asset.GroupKey == issuanceLeaf.GroupKey { + proofGroupPubKey := issuanceProof.Asset.GroupKey.GroupPubKey + leafGroupPubKey := issuanceLeaf.GroupKey.GroupPubKey + if !proofGroupPubKey.IsEqual(&leafGroupPubKey) { return fmt.Errorf("group key in issuance leaf does not match " + "group key in issuance proof") }