Skip to content

tapdb: add implementation of supplycommit.SupplyTreeView #1507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: asset-commitment-creator
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 139 additions & 92 deletions tapdb/burn_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import (
"github.com/lightninglabs/taproot-assets/mssmt"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/universe"
"github.com/lightninglabs/taproot-assets/universe/supplycommit"

lfn "github.com/lightningnetwork/lnd/fn/v2"
)

// ErrMissingGroupKey is returned when an operation requires an asset specifier
// with a group key, but none is provided.
var ErrMissingGroupKey = errors.New("asset specifier missing group key")

// BurnUniverseTree is a structure that holds the DB for burn operations.
type BurnUniverseTree struct {
db BatchedUniverseTree
Expand All @@ -31,110 +36,153 @@ func NewBurnUniverseTree(db BatchedUniverseTree) *BurnUniverseTree {
func (bt *BurnUniverseTree) Sum(ctx context.Context,
spec asset.Specifier) universe.BurnTreeSum {

// Derive identifier from the asset.Specifier.
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
if err != nil {
return lfn.Err[lfn.Option[uint64]](err)
groupKey := spec.UnwrapGroupKeyToPtr()
if groupKey == nil {
return lfn.Err[lfn.Option[uint64]](ErrMissingGroupKey)
}
namespace := subTreeNamespace(groupKey, supplycommit.BurnTreeType)

// Use the generic helper to get the sum.
return getUniverseTreeSum(ctx, bt.db, id)
return getUniverseTreeSum(ctx, bt.db, namespace)
}

// ErrNotBurn is returned when a proof is not a burn proof.
var ErrNotBurn = errors.New("not a burn proof")

// InsertBurns attempts to insert a set of new burn leaves into the burn tree
// identified by the passed asset.Specifier. If a given proof isn't a true burn
// proof, then an error is returned. This check is performed upfront. If the
// proof is valid, then the burn leaf is inserted into the tree, with a new
// merkle proof returned.
func (bt *BurnUniverseTree) InsertBurns(ctx context.Context,
spec asset.Specifier,
burnLeaves ...*universe.BurnLeaf) universe.BurnLeafResp {
// insertBurnsInternal performs the insertion of burn leaves within a database
// transaction. It also updates the main supply tree with the new burn sub-tree
// root.
//
// NOTE: This function must be called within a database transaction.
func insertBurnsInternal(ctx context.Context, db BaseUniverseStore,
spec asset.Specifier, burnLeaves ...*universe.BurnLeaf,
) ([]*universe.AuthenticatedBurnLeaf, error) {

if len(burnLeaves) == 0 {
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](
fmt.Errorf("no burn leaves provided"),
)
return nil, fmt.Errorf("no burn leaves provided")
}

// Derive identifier (and thereby the namespace) from the
// asset.Specifier.
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
if err != nil {
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](err)
groupKey := spec.UnwrapGroupKeyToPtr()
if groupKey == nil {
return nil, ErrMissingGroupKey
}

// Given the group key, and the sub-tree type, we'll derive a unique
// namespace for this tree.
subNs := subTreeNamespace(groupKey, supplycommit.BurnTreeType)

// Perform upfront validation for all proofs. Make sure that all the
// assets are actually burns.
for _, burnLeaf := range burnLeaves {
if !burnLeaf.BurnProof.Asset.IsBurn() {
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](
fmt.Errorf("%w: proof for asset %v is not a "+
"burn proof, has type %v",
ErrNotBurn,
burnLeaf.BurnProof.Asset.ID(),
burnLeaf.BurnProof.Asset.Type),
)
return nil, fmt.Errorf("%w: proof for asset %v is "+
"not a burn proof, has type %v",
ErrNotBurn,
burnLeaf.BurnProof.Asset.ID(),
burnLeaf.BurnProof.Asset.Type)
}
}

tree := mssmt.NewCompactedTree(
newTreeStoreWrapperTx(db, subNs),
)

var finalResults []*universe.AuthenticatedBurnLeaf

var writeTx BaseUniverseStoreOptions
txErr := bt.db.ExecTx(ctx, &writeTx, func(db BaseUniverseStore) error {
for _, burnLeaf := range burnLeaves {
leafKey := burnLeaf.UniverseKey

// Encode the burn proof to get the raw bytes.
var proofBuf bytes.Buffer
err := burnLeaf.BurnProof.Encode(&proofBuf)
if err != nil {
return fmt.Errorf("unable to encode burn "+
"proof: %w", err)
}
rawProofBytes := proofBuf.Bytes()

// Construct the universe.Leaf required by
// universeUpsertProofLeaf.
burnProof := burnLeaf.BurnProof
leaf := &universe.Leaf{
GenesisWithGroup: universe.GenesisWithGroup{
Genesis: burnProof.Asset.Genesis,
GroupKey: burnProof.Asset.GroupKey,
},
RawProof: rawProofBytes,
Asset: &burnLeaf.BurnProof.Asset,
Amt: burnLeaf.BurnProof.Asset.Amount,
}

// Call the generic upsert function. MetaReveal is nil
// for burns, as this isn't an issuance instance. We
// also skip inserting into the multi-verse tree for
// now.
uniProof, err := universeUpsertProofLeaf(
ctx, db, id, leafKey, leaf, nil, true,
)
if err != nil {
return fmt.Errorf("unable to upsert burn "+
"leaf for key %v: %w", leafKey, err)
}

authLeaf := &universe.AuthenticatedBurnLeaf{
BurnLeaf: burnLeaf,
BurnTreeRoot: uniProof.UniverseRoot,
BurnProof: uniProof.UniverseInclusionProof,
}
finalResults = append(finalResults, authLeaf)
// First, insert all burn leaves into the burn sub-tree SMT.
for _, burnLeaf := range burnLeaves {
leafKey := burnLeaf.UniverseKey

// Encode the burn proof to get the raw bytes.
var proofBuf bytes.Buffer
err := burnLeaf.BurnProof.Encode(&proofBuf)
if err != nil {
return nil, fmt.Errorf("unable to encode burn "+
"proof: %w", err)
}
rawProofBytes := proofBuf.Bytes()

// Construct the universe.Leaf required by
// universeUpsertProofLeaf.
burnProof := burnLeaf.BurnProof
leaf := &universe.Leaf{
GenesisWithGroup: universe.GenesisWithGroup{
Genesis: burnProof.Asset.Genesis,
GroupKey: burnProof.Asset.GroupKey,
},
RawProof: rawProofBytes,
Asset: &burnLeaf.BurnProof.Asset,
Amt: burnLeaf.BurnProof.Asset.Amount,
IsBurn: true,
}

return nil
// Call the generic upsert function for the burn sub-tree to
// update DB records. MetaReveal is nil for burns.
_, err = universeUpsertProofLeaf(
ctx, db, subNs, supplycommit.BurnTreeType.String(),
groupKey, leafKey, leaf, nil,
)
if err != nil {
return nil, fmt.Errorf("unable to upsert burn "+
"leaf DB records for key %v: %w", leafKey, err)
}
}

// Fetch the final burn sub-tree root after all insertions.
finalBurnRoot, err := tree.Root(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get burn tree root: %w", err)
}

// Now, construct the AuthenticatedBurnLeaf results by fetching proofs
// against the final tree root.
for _, burnLeaf := range burnLeaves {
leafKey := burnLeaf.UniverseKey.UniverseKey()
inclusionProof, err := tree.MerkleProof(ctx, leafKey)
if err != nil {
return nil, fmt.Errorf("failed to get burn proof "+
"for key %v: %w", burnLeaf.UniverseKey, err)
}

authLeaf := &universe.AuthenticatedBurnLeaf{
BurnLeaf: burnLeaf,
BurnTreeRoot: finalBurnRoot,
BurnProof: inclusionProof,
}
finalResults = append(finalResults, authLeaf)
}

return finalResults, nil
}

// InsertBurns attempts to insert a set of new burn leaves into the burn tree
// identified by the passed asset.Specifier. If a given proof isn't a true burn
// proof, then an error is returned. This check is performed upfront. If the
// proof is valid, then the burn leaf is inserted into the tree, with a new
// merkle proof returned.
func (bt *BurnUniverseTree) InsertBurns(ctx context.Context,
spec asset.Specifier,
burnLeaves ...*universe.BurnLeaf) universe.BurnLeafResp {

var (
writeTx BaseUniverseStoreOptions
finalResults []*universe.AuthenticatedBurnLeaf
err error
)
txErr := bt.db.ExecTx(ctx, &writeTx, func(db BaseUniverseStore) error {
finalResults, err = insertBurnsInternal(
ctx, db, spec, burnLeaves...,
)

// TODO(roasbeef): also update the root supply tree?
return err
})
if txErr != nil {
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](txErr)
}

// TODO(roasbeef): cache invalidation?

return lfn.Ok(finalResults)
}

Expand All @@ -143,14 +191,11 @@ func queryBurnLeaves(ctx context.Context, dbtx BaseUniverseStore,
spec asset.Specifier,
burnPoints ...wire.OutPoint) ([]UniverseLeaf, error) {

uniNamespace, err := specifierToIdentifier(
spec, universe.ProofTypeBurn,
)
if err != nil {
return nil, fmt.Errorf("error deriving identifier: %w", err)
groupKey := spec.UnwrapGroupKeyToPtr()
if groupKey == nil {
return nil, ErrMissingGroupKey
}

namespace := uniNamespace.String()
namespace := subTreeNamespace(groupKey, supplycommit.BurnTreeType)

// If no burn points are provided, we query all leaves in the namespace.
if len(burnPoints) == 0 {
Expand All @@ -166,7 +211,7 @@ func queryBurnLeaves(ctx context.Context, dbtx BaseUniverseStore,
}
if err != nil {
return nil, fmt.Errorf("error querying all leaves "+
"for namespace %s: %w", &uniNamespace, err)
"for namespace %s: %w", namespace, err)
}

return dbLeaves, nil
Expand Down Expand Up @@ -264,19 +309,19 @@ func (bt *BurnUniverseTree) QueryBurns(ctx context.Context,
spec asset.Specifier,
burnPoints ...wire.OutPoint) universe.BurnLeafQueryResp {

// Derive identifier from the asset.Specifier.
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
if err != nil {
groupKey := spec.UnwrapGroupKeyToPtr()
if groupKey == nil {
return lfn.Err[lfn.Option[[]*universe.AuthenticatedBurnLeaf]](
err,
ErrMissingGroupKey,
)
}
namespace := subTreeNamespace(groupKey, supplycommit.BurnTreeType)

// Use the generic list helper to list the leaves from the universe
// Tree. We pass in our custom decode function to handle the logic
// specific to BurnLeaf.
return queryUniverseLeavesAndProofs(
ctx, bt.db, spec, id, queryBurnLeaves,
ctx, bt.db, spec, namespace, queryBurnLeaves,
decodeAndBuildAuthBurnLeaf, buildAuthBurnLeaf, burnPoints...,
)
}
Expand Down Expand Up @@ -306,13 +351,15 @@ func (bt *BurnUniverseTree) ListBurns(ctx context.Context,
spec asset.Specifier) universe.ListBurnsResp {

// Derive identifier from the asset.Specifier.
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
if err != nil {
return lfn.Err[lfn.Option[[]*universe.BurnDesc]](err)
groupKey := spec.UnwrapGroupKeyToPtr()
if groupKey == nil {
return lfn.Err[lfn.Option[[]*universe.BurnDesc]](
ErrMissingGroupKey,
)
}
namespace := subTreeNamespace(groupKey, supplycommit.BurnTreeType)

// Use the generic list helper.
return listUniverseLeaves(ctx, bt.db, id, decodeBurnDesc)
return listUniverseLeaves(ctx, bt.db, namespace, decodeBurnDesc)
}

// Compile-time assertion to ensure BurnUniverseTree implements the
Expand Down
33 changes: 21 additions & 12 deletions tapdb/burn_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ func TestBurnUniverseTreeInsertBurns(t *testing.T) {
)
require.True(t, valid)
}

// Verify that all returned proofs share the same final tree.
if len(authLeaves) > 1 {
firstRoot := authLeaves[0].BurnTreeRoot
for i := 1; i < len(authLeaves); i++ {
require.Equal(t,
firstRoot.NodeHash(),
authLeaves[i].BurnTreeRoot.NodeHash(),
"root hash mismatch at index %d", i,
)
require.Equal(t,
firstRoot.NodeSum(),
authLeaves[i].BurnTreeRoot.NodeSum(),
"root sum mismatch at index %d", i,
)
}
}
})

// Test case 2: Inserting with no burn leaves should return an error.
Expand All @@ -173,9 +190,7 @@ func TestBurnUniverseTreeInsertBurns(t *testing.T) {
ctx, invalidSpec, burnLeaves...,
)
require.Error(t, result.Err())
require.Contains(
t, result.Err().Error(), "group key must be set",
)
require.ErrorIs(t, result.Err(), ErrMissingGroupKey)
})

// Test case 4: Inserting non-burn proof should fail.
Expand Down Expand Up @@ -414,9 +429,7 @@ func TestBurnUniverseTreeSum(t *testing.T) {
var invalidSpec asset.Specifier
result := burnTree.Sum(ctx, invalidSpec)
require.Error(t, result.Err())
require.Contains(
t, result.Err().Error(), "group key must be set",
)
require.ErrorIs(t, result.Err(), ErrMissingGroupKey)
})
}

Expand Down Expand Up @@ -522,9 +535,7 @@ func TestBurnUniverseTreeQueryBurns(t *testing.T) {
var invalidSpec asset.Specifier
result := burnTree.QueryBurns(ctx, invalidSpec)
require.Error(t, result.Err())
require.Contains(
t, result.Err().Error(), "group key must be set",
)
require.ErrorIs(t, result.Err(), ErrMissingGroupKey)
})
}

Expand Down Expand Up @@ -607,9 +618,7 @@ func TestBurnUniverseTreeListBurns(t *testing.T) {
invalidSpec := asset.Specifier{}
result := burnTree.ListBurns(ctx, invalidSpec)
require.Error(t, result.Err())
require.Contains(
t, result.Err().Error(), "group key must be set",
)
require.ErrorIs(t, result.Err(), ErrMissingGroupKey)
})
}

Expand Down
Loading
Loading