diff --git a/rpcserver.go b/rpcserver.go index 828558e8f..17e855bd8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3010,7 +3010,7 @@ func (r *rpcServer) VerifyAssetOwnership(ctx context.Context, }, nil } -// UniverseStats returns a set of aggregrate statistics for the current state +// UniverseStats returns a set of aggregate statistics for the current state // of the Universe. func (r *rpcServer) UniverseStats(ctx context.Context, req *unirpc.StatsRequest) (*unirpc.StatsResponse, error) { diff --git a/tapdb/universe.go b/tapdb/universe.go index e929b8367..45f30ab80 100644 --- a/tapdb/universe.go +++ b/tapdb/universe.go @@ -277,6 +277,46 @@ func (b *BaseUniverseTree) RegisterIssuance(ctx context.Context, key universe.BaseKey, leaf *universe.MintingLeaf, metaReveal *proof.MetaReveal) (*universe.IssuanceProof, error) { + var ( + writeTx BaseUniverseStoreOptions + + err error + issuanceProof *universe.IssuanceProof + ) + + // Limit to a single writer at a time. + b.registrationMtx.Lock() + defer b.registrationMtx.Unlock() + + dbErr := b.db.ExecTx(ctx, &writeTx, func(dbTx BaseUniverseStore) error { + issuanceProof, _, err = universeRegisterIssuance( + ctx, dbTx, b.id, key, leaf, metaReveal, + ) + return err + }) + if dbErr != nil { + return nil, dbErr + } + + return issuanceProof, nil +} + +// universeRegisterIssuance inserts a new minting leaf within the universe +// tree, stored at the base key. +// +// This function returns the newly registered issuance proof and the new +// universe root. +// +// NOTE: This function accepts a db transaction, as it's used when making +// broader DB updates. +func universeRegisterIssuance(ctx context.Context, dbTx BaseUniverseStore, + id universe.Identifier, key universe.BaseKey, + leaf *universe.MintingLeaf, + metaReveal *proof.MetaReveal) (*universe.IssuanceProof, mssmt.Node, + error) { + + namespace := idToNameSpace(id) + // With the tree store created, we'll now obtain byte representation of // the minting key, as that'll be the key in the SMT itself. smtKey := key.UniverseKey() @@ -286,103 +326,88 @@ func (b *BaseUniverseTree) RegisterIssuance(ctx context.Context, leafNode := leaf.SmtLeafNode() var groupKeyBytes []byte - if b.id.GroupKey != nil { - groupKeyBytes = schnorr.SerializePubKey(b.id.GroupKey) + if id.GroupKey != nil { + groupKeyBytes = schnorr.SerializePubKey(id.GroupKey) } mintingPointBytes, err := encodeOutpoint(key.MintingOutpoint) if err != nil { - return nil, err + return nil, nil, err } - // Up to this point many writers can perform all required read - // operations to prepare and validate the keys. But since we're now - // actually starting to write, we want to limit this to a single writer - // at a time. - b.registrationMtx.Lock() - defer b.registrationMtx.Unlock() - var ( - writeTx BaseUniverseStoreOptions - leafInclusionProof *mssmt.Proof universeRoot mssmt.Node ) - dbErr := b.db.ExecTx(ctx, &writeTx, func(db BaseUniverseStore) error { - // First, we'll instantiate a new compact tree instance from the - // backing tree store. - universeTree := mssmt.NewCompactedTree( - newTreeStoreWrapperTx(db, b.smtNamespace), - ) - // Now that we have a tree instance linked to this DB - // transaction, we'll insert the leaf into the tree based on - // its SMT key. - _, err := universeTree.Insert(ctx, smtKey, leafNode) - if err != nil { - return err - } + // First, we'll instantiate a new compact tree instance from the + // backing tree store. + universeTree := mssmt.NewCompactedTree( + newTreeStoreWrapperTx(dbTx, namespace), + ) - // Next, we'll upsert the universe root in the DB, which gives - // us the root ID that we'll use to insert the universe leaf - // overlay. - universeRootID, err := db.UpsertUniverseRoot(ctx, NewUniverseRoot{ - NamespaceRoot: b.smtNamespace, - AssetID: fn.ByteSlice(leaf.ID()), - GroupKey: groupKeyBytes, - }) - if err != nil { - return err - } + // Now that we have a tree instance linked to this DB + // transaction, we'll insert the leaf into the tree based on + // its SMT key. + _, err = universeTree.Insert(ctx, smtKey, leafNode) + if err != nil { + return nil, nil, err + } - // Before we insert the asset genesis, we'll insert the meta - // first. The reveal may or may not be populated, which'll also - // insert the opauqe meta blob on disk. - _, err = maybeUpsertAssetMeta( - ctx, db, &leaf.Genesis, metaReveal, - ) - if err != nil { - return err - } + // Next, we'll upsert the universe root in the DB, which gives + // us the root ID that we'll use to insert the universe leaf + // overlay. + universeRootID, err := dbTx.UpsertUniverseRoot(ctx, NewUniverseRoot{ + NamespaceRoot: namespace, + AssetID: fn.ByteSlice(leaf.ID()), + GroupKey: groupKeyBytes, + }) + if err != nil { + return nil, nil, err + } - assetGenID, err := upsertAssetGen( - ctx, db, leaf.Genesis, leaf.GroupKey, - ) - if err != nil { - return err - } + // Before we insert the asset genesis, we'll insert the meta + // first. The reveal may or may not be populated, which'll also + // insert the opauqe meta blob on disk. + _, err = maybeUpsertAssetMeta( + ctx, dbTx, &leaf.Genesis, metaReveal, + ) + if err != nil { + return nil, nil, err + } - scriptKeyBytes := schnorr.SerializePubKey(key.ScriptKey.PubKey) - err = db.InsertUniverseLeaf(ctx, NewUniverseLeaf{ - AssetGenesisID: assetGenID, - ScriptKeyBytes: scriptKeyBytes, - UniverseRootID: universeRootID, - LeafNodeKey: smtKey[:], - LeafNodeNamespace: b.smtNamespace, - MintingPoint: mintingPointBytes, - }) - if err != nil { - return err - } + assetGenID, err := upsertAssetGen( + ctx, dbTx, leaf.Genesis, leaf.GroupKey, + ) + if err != nil { + return nil, nil, err + } - // Finally, we'll obtain the merkle proof from the tree for the - // leaf we just inserted. - leafInclusionProof, err = universeTree.MerkleProof(ctx, smtKey) - if err != nil { - return err - } + scriptKeyBytes := schnorr.SerializePubKey(key.ScriptKey.PubKey) + err = dbTx.InsertUniverseLeaf(ctx, NewUniverseLeaf{ + AssetGenesisID: assetGenID, + ScriptKeyBytes: scriptKeyBytes, + UniverseRootID: universeRootID, + LeafNodeKey: smtKey[:], + LeafNodeNamespace: namespace, + MintingPoint: mintingPointBytes, + }) + if err != nil { + return nil, nil, err + } - // With the insertion complete, we'll now fetch the root of the - // tree as it stands so we can return it to the caller. - universeRoot, err = universeTree.Root(ctx) - if err != nil { - return err - } + // Finally, we'll obtain the merkle proof from the tree for the + // leaf we just inserted. + leafInclusionProof, err = universeTree.MerkleProof(ctx, smtKey) + if err != nil { + return nil, nil, err + } - return nil - }) - if dbErr != nil { - return nil, dbErr + // With the insertion complete, we'll now fetch the root of the tree as + // it stands and return it to the caller. + universeRoot, err = universeTree.Root(ctx) + if err != nil { + return nil, nil, err } return &universe.IssuanceProof{ @@ -390,7 +415,7 @@ func (b *BaseUniverseTree) RegisterIssuance(ctx context.Context, UniverseRoot: universeRoot, InclusionProof: leafInclusionProof, Leaf: leaf, - }, nil + }, universeRoot, nil } // FetchIssuanceProof returns an issuance proof for the target key. If the key @@ -400,9 +425,40 @@ func (b *BaseUniverseTree) RegisterIssuance(ctx context.Context, func (b *BaseUniverseTree) FetchIssuanceProof(ctx context.Context, universeKey universe.BaseKey) ([]*universe.IssuanceProof, error) { - // Depending on the universeKey, we'll either be fetching the details - // of a specific issuance, or all of the issuances for that minting - // outpoint. + var ( + readTx = NewBaseUniverseReadTx() + proofs []*universe.IssuanceProof + ) + + dbErr := b.db.ExecTx(ctx, &readTx, func(dbTx BaseUniverseStore) error { + var err error + proofs, err = universeFetchIssuanceProof( + ctx, b.id, universeKey, dbTx, + ) + return err + }) + if dbErr != nil { + return nil, dbErr + } + + return proofs, nil +} + +// universeFetchIssuanceProof returns issuance proofs for the target universe. +// +// If the given universe key doesn't have a script key specified, then a proof +// will be returned for each minting outpoint. +// +// NOTE: This function accepts a database transaction and is called when making +// broader DB updates. +func universeFetchIssuanceProof(ctx context.Context, + id universe.Identifier, universeKey universe.BaseKey, + dbTx BaseUniverseStore) ([]*universe.IssuanceProof, error) { + + namespace := idToNameSpace(id) + + // Depending on the universeKey, we'll either be fetching the details of + // a specific issuance, or each issuance for that minting outpoint. var targetScriptKey []byte if universeKey.ScriptKey != nil { targetScriptKey = schnorr.SerializePubKey( @@ -417,108 +473,102 @@ func (b *BaseUniverseTree) FetchIssuanceProof(ctx context.Context, var proofs []*universe.IssuanceProof - readTx := NewBaseUniverseReadTx() - dbErr := b.db.ExecTx(ctx, &readTx, func(db BaseUniverseStore) error { - // First, we'll make a new instance of the universe tree, as - // we'll query it directly to obtain the set of leaves we care - // about. - universeTree := mssmt.NewCompactedTree( - newTreeStoreWrapperTx(db, b.smtNamespace), - ) + // First, we'll make a new instance of the universe tree, as we'll query + // it directly to obtain the set of leaves we care about. + universeTree := mssmt.NewCompactedTree( + newTreeStoreWrapperTx(dbTx, namespace), + ) - // Each response will include a merkle proof of inclusion for - // the root, so we'll obtain that now. - rootNode, err := universeTree.Root(ctx) + // Each response will include a merkle proof of inclusion for the root, + // so we'll obtain that now. + rootNode, err := universeTree.Root(ctx) + if err != nil { + return nil, err + } + + // Now that we have the tree, we'll query the set of Universe leaves we + // have directly to determine which ones we care about. + // + // If the script key is blank, then we'll fetch all the leaves in the + // tree. + universeLeaves, err := dbTx.QueryUniverseLeaves( + ctx, UniverseLeafQuery{ + MintingPointBytes: mintingPointBytes, + ScriptKeyBytes: targetScriptKey, + Namespace: namespace, + }, + ) + if err != nil { + return nil, err + } + + if len(universeLeaves) == 0 { + return nil, ErrNoUniverseProofFound + } + + // Now that we have all the leaves we need to query, we'll look each up + // them up in the universe tree, obtaining a merkle proof for each of + // them along the way. + err = fn.ForEachErr(universeLeaves, func(leaf UniverseLeaf) error { + scriptPub, err := schnorr.ParsePubKey(leaf.ScriptKeyBytes) if err != nil { return err } + scriptKey := asset.NewScriptKey(scriptPub) - // Now that we have the tree, we'll query the set of Universe - // leaves we have directly to determine which ones we care - // about. - // - // If the script key is blank, then we'll fetch all of the - // leaves in the tree. - universeLeaves, err := db.QueryUniverseLeaves( - ctx, UniverseLeafQuery{ - MintingPointBytes: mintingPointBytes, - ScriptKeyBytes: targetScriptKey, - Namespace: b.smtNamespace, - }, + // Next, we'll fetch the leaf node from the tree and also obtain + // a merkle proof for the leaf alongside it. + universeKey := universe.BaseKey{ + MintingOutpoint: universeKey.MintingOutpoint, + ScriptKey: &scriptKey, + } + smtKey := universeKey.UniverseKey() + leafProof, err := universeTree.MerkleProof( + ctx, smtKey, ) if err != nil { return err } - if len(universeLeaves) == 0 { - return ErrNoUniverseProofFound + leafAssetGen, err := fetchGenesis( + ctx, dbTx, leaf.GenAssetID, + ) + if err != nil { + return err } - // Now that we have all the leaves we need to query, we'll look - // each up them up in the universe tree, obtaining a merkle - // proof for each of them along the way. - return fn.ForEachErr(universeLeaves, func(leaf UniverseLeaf) error { - scriptPub, err := schnorr.ParsePubKey(leaf.ScriptKeyBytes) - if err != nil { - return err - } - scriptKey := asset.NewScriptKey(scriptPub) - - // Next, we'll fetch the leaf node from the tree and - // also obtain a merkle proof for the leaf along side - // it. - universeKey := universe.BaseKey{ - MintingOutpoint: universeKey.MintingOutpoint, - ScriptKey: &scriptKey, - } - smtKey := universeKey.UniverseKey() - leafProof, err := universeTree.MerkleProof( - ctx, smtKey, - ) - if err != nil { - return err - } - - leafAssetGen, err := fetchGenesis( - ctx, db, leaf.GenAssetID, + issuanceProof := &universe.IssuanceProof{ + MintingKey: universeKey, + UniverseRoot: rootNode, + InclusionProof: leafProof, + Leaf: &universe.MintingLeaf{ + GenesisWithGroup: universe.GenesisWithGroup{ + Genesis: leafAssetGen, + }, + GenesisProof: leaf.GenesisProof, + Amt: uint64(leaf.SumAmt), + }, + } + if id.GroupKey != nil { + leafAssetGroup, err := fetchGroupByGenesis( + ctx, dbTx, leaf.GenAssetID, ) if err != nil { return err } - proof := &universe.IssuanceProof{ - MintingKey: universeKey, - UniverseRoot: rootNode, - InclusionProof: leafProof, - Leaf: &universe.MintingLeaf{ - GenesisWithGroup: universe.GenesisWithGroup{ - Genesis: leafAssetGen, - }, - GenesisProof: leaf.GenesisProof, - Amt: uint64(leaf.SumAmt), - }, - } - if b.id.GroupKey != nil { - leafAssetGroup, err := fetchGroupByGenesis( - ctx, db, leaf.GenAssetID, - ) - if err != nil { - return err - } - - proof.Leaf.GroupKey = &asset.GroupKey{ - GroupPubKey: *b.id.GroupKey, - Sig: leafAssetGroup.Sig, - } + issuanceProof.Leaf.GroupKey = &asset.GroupKey{ + GroupPubKey: *id.GroupKey, + Sig: leafAssetGroup.Sig, } + } - proofs = append(proofs, proof) + proofs = append(proofs, issuanceProof) - return nil - }) + return nil }) - if dbErr != nil { - return nil, dbErr + if err != nil { + return nil, err } return proofs, nil diff --git a/tapdb/universe_forest.go b/tapdb/universe_forest.go index 16b190036..ad61b8caa 100644 --- a/tapdb/universe_forest.go +++ b/tapdb/universe_forest.go @@ -7,10 +7,13 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/mssmt" + "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapdb/sqlc" "github.com/lightninglabs/taproot-assets/universe" ) +const mssmtNamespace = "multiverse" + type ( BaseUniverseRoot = sqlc.UniverseRootsRow ) @@ -18,6 +21,8 @@ type ( // BaseUniverseForestStore is used to interact with a set of base universe // roots, also known as a universe forest. type BaseUniverseForestStore interface { + BaseUniverseStore + UniverseRoots(ctx context.Context) ([]BaseUniverseRoot, error) } @@ -31,8 +36,8 @@ func (b *BaseUniverseForestOptions) ReadOnly() bool { return b.readOnly } -// NewBaseUniverseForestReadTx creates a new read-only transaction for the base -// universe. +// NewBaseUniverseForestReadTx creates a new read-only transaction for the +// universe forest. func NewBaseUniverseForestReadTx() BaseUniverseForestOptions { return BaseUniverseForestOptions{ readOnly: true, @@ -40,16 +45,15 @@ func NewBaseUniverseForestReadTx() BaseUniverseForestOptions { } // BasedUniverseForest is a wrapper around the base universe forest that allows -// us perform batch queries with all the relevant query interfaces. +// us to perform batch transactional databse queries with all the relevant query +// interfaces. type BatchedUniverseForest interface { BaseUniverseForestStore BatchedTx[BaseUniverseForestStore] } -// BaseUniverseForest implements the persistent storage for the Base universe -// for a given asset. The minting outpoints stored of the asset are used to key -// into the universe tree. +// BaseUniverseForest implements the persistent storage for a universe forest. // // NOTE: This implements the universe.BaseForest interface. type BaseUniverseForest struct { @@ -67,12 +71,15 @@ func NewBaseUniverseForest(db BatchedUniverseForest) *BaseUniverseForest { } } -// RootNodes returns the complete set of known root nodes for the set of assets -// tracked in the base Universe. -func (b *BaseUniverseForest) RootNodes(ctx context.Context) ([]universe.BaseRoot, error) { - var uniRoots []universe.BaseRoot +// RootNodes returns the complete set of known base universe root nodes for the +// set of base universes tracked in the universe forest. +func (b *BaseUniverseForest) RootNodes( + ctx context.Context) ([]universe.BaseRoot, error) { - readTx := NewBaseUniverseForestReadTx() + var ( + uniRoots []universe.BaseRoot + readTx = NewBaseUniverseForestReadTx() + ) dbErr := b.db.ExecTx(ctx, &readTx, func(db BaseUniverseForestStore) error { dbRoots, err := db.UniverseRoots(ctx) @@ -123,3 +130,79 @@ func (b *BaseUniverseForest) RootNodes(ctx context.Context) ([]universe.BaseRoot return uniRoots, nil } + +// RegisterIssuance inserts a new minting leaf within the universe +// tree, stored at the base key. +func (b *BaseUniverseForest) RegisterIssuance(ctx context.Context, + id universe.Identifier, key universe.BaseKey, + leaf *universe.MintingLeaf, + metaReveal *proof.MetaReveal) (*universe.IssuanceProof, error) { + + var ( + writeTx BaseUniverseForestOptions + issuanceProof *universe.IssuanceProof + ) + + dbErr := b.db.ExecTx( + ctx, &writeTx, func(dbTx BaseUniverseForestStore) error { + // Register issuance in the asset (group) specific + // universe tree. + var ( + universeRoot mssmt.Node + err error + ) + issuanceProof, universeRoot, err = universeRegisterIssuance( + ctx, dbTx, id, key, leaf, metaReveal, + ) + + // Update multiverse tree with new issuance. + multiverseTree := mssmt.NewCompactedTree( + newTreeStoreWrapperTx(dbTx, mssmtNamespace), + ) + + // We will insert a new leaf node into the multiverse + // tree. + // + // In constructing the leaf node we will first compute + // its value, which is the hash of the universe root + // node hash concatenated with the sum of the asset + // group. + universeRootHash := universeRoot.NodeHash() + assetGroupSum := universeRoot.NodeSum() + + // Convert asset group sum to byte slice so that it can + // be appended to the universe root hash. + assetGroupSumBytes := make([]byte, 8) + for i := uint(0); i < 8; i++ { + assetGroupSumBytes[i] = byte( + assetGroupSum >> (i * 8), + ) + } + leafNodeValue := append( + universeRootHash[:], assetGroupSumBytes..., + ) + + leafNode := mssmt.NewLeafNode( + leafNodeValue, assetGroupSum, + ) + + // Use universe ID as leaf node key. + leafNodeKey := id.Bytes() + + _, err = multiverseTree.Insert( + ctx, leafNodeKey, leafNode, + ) + if err != nil { + return err + } + + return err + }, + ) + + if dbErr != nil { + return nil, dbErr + } + + return issuanceProof, nil +} diff --git a/tapdb/universe_test.go b/tapdb/universe_test.go index 62cb3376e..15b0cbbdc 100644 --- a/tapdb/universe_test.go +++ b/tapdb/universe_test.go @@ -335,7 +335,7 @@ func TestUniverseTreeIsolation(t *testing.T) { normalLeaf, err := insertRandLeaf(t, ctx, normalUniverse, nil) require.NoError(t, err) - // We should be able to get the roots for both fo the trees. + // We should be able to get the roots for both of the trees. groupRoot, _, err := groupUniverse.RootNode(ctx) require.NoError(t, err) diff --git a/universe/base.go b/universe/base.go index 3b96c038d..58efbe92b 100644 --- a/universe/base.go +++ b/universe/base.go @@ -202,8 +202,8 @@ func (a *MintingArchive) RegisterIssuance(ctx context.Context, id Identifier, // Now that we know the proof is valid, we'll insert it into the base // universe backend, and return the new issuance proof. - issuanceProof, err := baseUni.RegisterIssuance( - ctx, key, leaf, assetSnapshot.MetaReveal, + issuanceProof, err := a.cfg.UniverseForest.RegisterIssuance( + ctx, id, key, leaf, assetSnapshot.MetaReveal, ) if err != nil { return nil, fmt.Errorf("unable to register new "+ diff --git a/universe/interface.go b/universe/interface.go index f1a820331..571fd2377 100644 --- a/universe/interface.go +++ b/universe/interface.go @@ -40,14 +40,20 @@ type Identifier struct { GroupKey *btcec.PublicKey } -// String returns a string representation of the ID. -func (i *Identifier) String() string { +// Bytes returns a bytes representation of the ID. +func (i *Identifier) Bytes() [32]byte { if i.GroupKey != nil { h := sha256.Sum256(schnorr.SerializePubKey(i.GroupKey)) - return hex.EncodeToString(h[:]) + return h } - return hex.EncodeToString(i.AssetID[:]) + return i.AssetID +} + +// String returns a string representation of the ID. +func (i *Identifier) String() string { + idBytes := i.Bytes() + return hex.EncodeToString(idBytes[:]) } // StringForLog returns a string representation of the ID for logging. @@ -215,6 +221,10 @@ type BaseForest interface { // of assets tracked in the base Universe. RootNodes(ctx context.Context) ([]BaseRoot, error) + RegisterIssuance(ctx context.Context, id Identifier, key BaseKey, + leaf *MintingLeaf, + metaReveal *proof.MetaReveal) (*IssuanceProof, error) + // TODO(roasbeef): other stats stuff here, like total number of assets, etc // * also eventually want pull/fetch stats, can be pulled out into another instance } @@ -381,11 +391,12 @@ type Syncer interface { // DiffEngine is a Universe diff engine that can be used to compare the state // of two universes and find the set of assets that are different between them. type DiffEngine interface { - BaseForest - // RootNode returns the root node for a given base universe. RootNode(ctx context.Context, id Identifier) (BaseRoot, error) + // RootNodes returns the set of root nodes for all known universes. + RootNodes(ctx context.Context) ([]BaseRoot, error) + // MintingKeys returns all the keys inserted in the universe. MintingKeys(ctx context.Context, id Identifier) ([]BaseKey, error)