Skip to content

Commit ff70ca7

Browse files
authored
Merge pull request #1456 from Roasbeef/burn-tree
tapdb+universe: implement new Universe tree for 1st party burns
2 parents 164f82d + 85b8072 commit ff70ca7

16 files changed

+1538
-238
lines changed

rpcserver.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5200,11 +5200,11 @@ func (r *rpcServer) DeleteAssetRoot(ctx context.Context,
52005200
func marshalLeafKey(leafKey universe.LeafKey) *unirpc.AssetKey {
52015201
return &unirpc.AssetKey{
52025202
Outpoint: &unirpc.AssetKey_OpStr{
5203-
OpStr: leafKey.OutPoint.String(),
5203+
OpStr: leafKey.LeafOutPoint().String(),
52045204
},
52055205
ScriptKey: &unirpc.AssetKey_ScriptKeyBytes{
52065206
ScriptKeyBytes: schnorr.SerializePubKey(
5207-
leafKey.ScriptKey.PubKey,
5207+
leafKey.LeafScriptKey().PubKey,
52085208
),
52095209
},
52105210
}
@@ -5339,7 +5339,7 @@ func (r *rpcServer) AssetLeaves(ctx context.Context,
53395339

53405340
// unmarshalLeafKey un-marshals a leaf key from the RPC form.
53415341
func unmarshalLeafKey(key *unirpc.AssetKey) (universe.LeafKey, error) {
5342-
var leafKey universe.LeafKey
5342+
var leafKey universe.BaseLeafKey
53435343

53445344
switch {
53455345
case key.GetScriptKeyBytes() != nil:
@@ -5589,7 +5589,7 @@ func unmarshalUniverseKey(key *unirpc.UniverseKey) (universe.Identifier,
55895589

55905590
var (
55915591
uniID = universe.Identifier{}
5592-
uniKey = universe.LeafKey{}
5592+
uniKey = universe.BaseLeafKey{}
55935593
err error
55945594
)
55955595

@@ -5602,12 +5602,12 @@ func unmarshalUniverseKey(key *unirpc.UniverseKey) (universe.Identifier,
56025602
return uniID, uniKey, err
56035603
}
56045604

5605-
uniKey, err = unmarshalLeafKey(key.LeafKey)
5605+
leafKey, err := unmarshalLeafKey(key.LeafKey)
56065606
if err != nil {
56075607
return uniID, uniKey, err
56085608
}
56095609

5610-
return uniID, uniKey, nil
5610+
return uniID, leafKey, nil
56115611
}
56125612

56135613
// unmarshalAssetLeaf unmarshals an asset leaf from the RPC form.

tapdb/burn_tree.go

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package tapdb
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"database/sql"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightninglabs/taproot-assets/asset"
13+
"github.com/lightninglabs/taproot-assets/mssmt"
14+
"github.com/lightninglabs/taproot-assets/proof"
15+
"github.com/lightninglabs/taproot-assets/universe"
16+
17+
lfn "github.com/lightningnetwork/lnd/fn/v2"
18+
)
19+
20+
// BurnUniverseTree is a structure that holds the DB for burn operations.
21+
type BurnUniverseTree struct {
22+
db BatchedUniverseTree
23+
}
24+
25+
// NewBurnUniverseTree returns a new BurnUniverseTree with the target DB.
26+
func NewBurnUniverseTree(db BatchedUniverseTree) *BurnUniverseTree {
27+
return &BurnUniverseTree{db: db}
28+
}
29+
30+
// Sum returns the sum of the burn leaves for the given asset.
31+
func (bt *BurnUniverseTree) Sum(ctx context.Context,
32+
spec asset.Specifier) universe.BurnTreeSum {
33+
34+
// Derive identifier from the asset.Specifier.
35+
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
36+
if err != nil {
37+
return lfn.Err[lfn.Option[uint64]](err)
38+
}
39+
40+
// Use the generic helper to get the sum.
41+
return getUniverseTreeSum(ctx, bt.db, id)
42+
}
43+
44+
// ErrNotBurn is returned when a proof is not a burn proof.
45+
var ErrNotBurn = errors.New("not a burn proof")
46+
47+
// InsertBurns attempts to insert a set of new burn leaves into the burn tree
48+
// identified by the passed asset.Specifier. If a given proof isn't a true burn
49+
// proof, then an error is returned. This check is performed upfront. If the
50+
// proof is valid, then the burn leaf is inserted into the tree, with a new
51+
// merkle proof returned.
52+
func (bt *BurnUniverseTree) InsertBurns(ctx context.Context,
53+
spec asset.Specifier,
54+
burnLeaves ...*universe.BurnLeaf) universe.BurnLeafResp {
55+
56+
if len(burnLeaves) == 0 {
57+
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](
58+
fmt.Errorf("no burn leaves provided"),
59+
)
60+
}
61+
62+
// Derive identifier (and thereby the namespace) from the
63+
// asset.Specifier.
64+
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
65+
if err != nil {
66+
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](err)
67+
}
68+
69+
// Perform upfront validation for all proofs. Make sure that all the
70+
// assets are actually burns.
71+
for _, burnLeaf := range burnLeaves {
72+
if !burnLeaf.BurnProof.Asset.IsBurn() {
73+
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](
74+
fmt.Errorf("%w: proof for asset %v is not a "+
75+
"burn proof, has type %v",
76+
ErrNotBurn,
77+
burnLeaf.BurnProof.Asset.ID(),
78+
burnLeaf.BurnProof.Asset.Type),
79+
)
80+
}
81+
}
82+
83+
var finalResults []*universe.AuthenticatedBurnLeaf
84+
85+
var writeTx BaseUniverseStoreOptions
86+
txErr := bt.db.ExecTx(ctx, &writeTx, func(db BaseUniverseStore) error {
87+
for _, burnLeaf := range burnLeaves {
88+
leafKey := burnLeaf.UniverseKey
89+
90+
// Encode the burn proof to get the raw bytes.
91+
var proofBuf bytes.Buffer
92+
err := burnLeaf.BurnProof.Encode(&proofBuf)
93+
if err != nil {
94+
return fmt.Errorf("unable to encode burn "+
95+
"proof: %w", err)
96+
}
97+
rawProofBytes := proofBuf.Bytes()
98+
99+
// Construct the universe.Leaf required by
100+
// universeUpsertProofLeaf.
101+
burnProof := burnLeaf.BurnProof
102+
leaf := &universe.Leaf{
103+
GenesisWithGroup: universe.GenesisWithGroup{
104+
Genesis: burnProof.Asset.Genesis,
105+
GroupKey: burnProof.Asset.GroupKey,
106+
},
107+
RawProof: rawProofBytes,
108+
Asset: &burnLeaf.BurnProof.Asset,
109+
Amt: burnLeaf.BurnProof.Asset.Amount,
110+
}
111+
112+
// Call the generic upsert function. MetaReveal is nil
113+
// for burns, as this isn't an issuance instance. We
114+
// also skip inserting into the multi-verse tree for
115+
// now.
116+
uniProof, err := universeUpsertProofLeaf(
117+
ctx, db, id, leafKey, leaf, nil, true,
118+
)
119+
if err != nil {
120+
return fmt.Errorf("unable to upsert burn "+
121+
"leaf for key %v: %w", leafKey, err)
122+
}
123+
124+
authLeaf := &universe.AuthenticatedBurnLeaf{
125+
BurnLeaf: burnLeaf,
126+
BurnTreeRoot: uniProof.UniverseRoot,
127+
BurnProof: uniProof.UniverseInclusionProof,
128+
}
129+
finalResults = append(finalResults, authLeaf)
130+
}
131+
132+
return nil
133+
})
134+
if txErr != nil {
135+
return lfn.Err[[]*universe.AuthenticatedBurnLeaf](txErr)
136+
}
137+
138+
return lfn.Ok(finalResults)
139+
}
140+
141+
// queryBurnLeaves retrieves UniverseLeaf records based on burn OutPoints.
142+
func queryBurnLeaves(ctx context.Context, dbtx BaseUniverseStore,
143+
spec asset.Specifier,
144+
burnPoints ...wire.OutPoint) ([]UniverseLeaf, error) {
145+
146+
uniNamespace, err := specifierToIdentifier(
147+
spec, universe.ProofTypeBurn,
148+
)
149+
if err != nil {
150+
return nil, fmt.Errorf("error deriving identifier: %w", err)
151+
}
152+
153+
namespace := uniNamespace.String()
154+
155+
// If no burn points are provided, we query all leaves in the namespace.
156+
if len(burnPoints) == 0 {
157+
// If no specific points, query all leaves in the namespace.
158+
dbLeaves, err := dbtx.QueryUniverseLeaves(
159+
ctx, UniverseLeafQuery{
160+
Namespace: namespace,
161+
},
162+
)
163+
if errors.Is(err, sql.ErrNoRows) {
164+
// No leaves found is not an error in this case.
165+
return nil, sql.ErrNoRows
166+
}
167+
if err != nil {
168+
return nil, fmt.Errorf("error querying all leaves "+
169+
"for namespace %s: %w", &uniNamespace, err)
170+
}
171+
172+
return dbLeaves, nil
173+
}
174+
175+
// Otherwise, we'll query for leaves matching the burn points.
176+
var leavesToQuery []UniverseLeaf
177+
for _, burnPoint := range burnPoints {
178+
burnPointBytes, err := encodeOutpoint(burnPoint)
179+
if err != nil {
180+
return nil, fmt.Errorf("unable to encode burn "+
181+
"point %v: %w", burnPoint, err)
182+
}
183+
184+
// Query leaves matching the burn point and namespace.
185+
// ScriptKeyBytes is nil here as we want all script keys for
186+
// this burn point.
187+
dbLeaves, err := dbtx.QueryUniverseLeaves(
188+
ctx, UniverseLeafQuery{
189+
MintingPointBytes: burnPointBytes,
190+
Namespace: namespace,
191+
},
192+
)
193+
if errors.Is(err, sql.ErrNoRows) {
194+
continue
195+
}
196+
if err != nil {
197+
return nil, fmt.Errorf("error querying leaves "+
198+
"for burn point %v: %w", burnPoint, err)
199+
}
200+
201+
leavesToQuery = append(leavesToQuery, dbLeaves...)
202+
}
203+
204+
if len(leavesToQuery) == 0 {
205+
// Return sql.ErrNoRows if no leaves were found.
206+
return nil, sql.ErrNoRows
207+
}
208+
209+
return leavesToQuery, nil
210+
}
211+
212+
// decodeAndBuildAuthBurnLeaf decodes the raw leaf, reconstructs the key, and
213+
// builds the AuthenticatedBurnLeaf.
214+
func decodeAndBuildAuthBurnLeaf(dbLeaf UniverseLeaf) (
215+
*universe.BurnLeaf, uniKey, error) {
216+
217+
var burnProof proof.Proof
218+
err := burnProof.Decode(bytes.NewReader(dbLeaf.GenesisProof))
219+
if err != nil {
220+
return nil, uniKey{}, fmt.Errorf("unable to decode burn "+
221+
"proof: %w", err)
222+
}
223+
224+
// Reconstruct the LeafKey used for SMT insertion.
225+
scriptPub, err := schnorr.ParsePubKey(dbLeaf.ScriptKeyBytes)
226+
if err != nil {
227+
return nil, uniKey{}, fmt.Errorf("unable to parse script "+
228+
"key: %w", err)
229+
}
230+
scriptKey := asset.NewScriptKey(scriptPub)
231+
232+
leafKey := universe.AssetLeafKey{
233+
BaseLeafKey: universe.BaseLeafKey{
234+
OutPoint: burnProof.OutPoint(),
235+
ScriptKey: &scriptKey,
236+
},
237+
AssetID: burnProof.Asset.ID(),
238+
}
239+
240+
burnLeaf := &universe.BurnLeaf{
241+
UniverseKey: &leafKey,
242+
BurnProof: &burnProof,
243+
}
244+
245+
return burnLeaf, leafKey.UniverseKey(), nil
246+
}
247+
248+
// buildAuthBurnLeaf constructs the final AuthenticatedBurnLeaf.
249+
func buildAuthBurnLeaf(decodedLeaf *universe.BurnLeaf,
250+
inclusionProof *mssmt.Proof,
251+
root mssmt.Node) *universe.AuthenticatedBurnLeaf {
252+
253+
return &universe.AuthenticatedBurnLeaf{
254+
BurnLeaf: decodedLeaf,
255+
BurnTreeRoot: root,
256+
BurnProof: inclusionProof,
257+
}
258+
}
259+
260+
// QueryBurns attempts to query a set of burn leaves for the given asset
261+
// specifier. If the burn leaf points are empty, then all burn leaves are
262+
// returned.
263+
func (bt *BurnUniverseTree) QueryBurns(ctx context.Context,
264+
spec asset.Specifier,
265+
burnPoints ...wire.OutPoint) universe.BurnLeafQueryResp {
266+
267+
// Derive identifier from the asset.Specifier.
268+
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
269+
if err != nil {
270+
return lfn.Err[lfn.Option[[]*universe.AuthenticatedBurnLeaf]](
271+
err,
272+
)
273+
}
274+
275+
// Use the generic list helper to list the leaves from the universe
276+
// Tree. We pass in our custom decode function to handle the logic
277+
// specific to BurnLeaf.
278+
return queryUniverseLeavesAndProofs(
279+
ctx, bt.db, spec, id, queryBurnLeaves,
280+
decodeAndBuildAuthBurnLeaf, buildAuthBurnLeaf, burnPoints...,
281+
)
282+
}
283+
284+
// decodeBurnDesc decodes the raw bytes into a BurnDesc.
285+
func decodeBurnDesc(dbLeaf UniverseLeaf) (*universe.BurnDesc, error) {
286+
var burnProof proof.Proof
287+
err := burnProof.Decode(bytes.NewReader(dbLeaf.GenesisProof))
288+
if err != nil {
289+
return nil, fmt.Errorf("unable to decode burn proof: %w", err)
290+
}
291+
292+
// Extract information for BurnDesc.
293+
assetSpec := burnProof.Asset.Specifier()
294+
amt := burnProof.Asset.Amount
295+
burnPoint := burnProof.OutPoint()
296+
297+
return &universe.BurnDesc{
298+
AssetSpec: assetSpec,
299+
Amt: amt,
300+
BurnPoint: burnPoint,
301+
}, nil
302+
}
303+
304+
// ListBurns attempts to list all burn leaves for the given asset.
305+
func (bt *BurnUniverseTree) ListBurns(ctx context.Context,
306+
spec asset.Specifier) universe.ListBurnsResp {
307+
308+
// Derive identifier from the asset.Specifier.
309+
id, err := specifierToIdentifier(spec, universe.ProofTypeBurn)
310+
if err != nil {
311+
return lfn.Err[lfn.Option[[]*universe.BurnDesc]](err)
312+
}
313+
314+
// Use the generic list helper.
315+
return listUniverseLeaves(ctx, bt.db, id, decodeBurnDesc)
316+
}
317+
318+
// Compile-time assertion to ensure BurnUniverseTree implements the
319+
// universe.BurnTree interface.
320+
var _ universe.BurnTree = (*BurnUniverseTree)(nil)

0 commit comments

Comments
 (0)