Skip to content

Commit 11b67f8

Browse files
committed
tapdb: add initial implementation of universe.IgnoreTree
1 parent d83c907 commit 11b67f8

8 files changed

+834
-1
lines changed

tapdb/assets_common.go

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ type UpsertAssetStore interface {
5050
FetchGenesisID(ctx context.Context,
5151
arg sqlc.FetchGenesisIDParams) (int64, error)
5252

53+
// LookupGenesisID fetches the database ID of an asset genesis by its
54+
// asset ID.
55+
LookupGenesisID(ctx context.Context, assetID []byte) (int64, error)
56+
5357
// FetchScriptKeyIDByTweakedKey determines the database ID of a script
5458
// key by querying it by the tweaked key.
5559
FetchScriptKeyIDByTweakedKey(ctx context.Context,

tapdb/ignore_tree.go

+380
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
package tapdb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
8+
"github.com/lightninglabs/taproot-assets/asset"
9+
"github.com/lightninglabs/taproot-assets/mssmt"
10+
"github.com/lightninglabs/taproot-assets/universe"
11+
12+
lfn "github.com/lightningnetwork/lnd/fn"
13+
)
14+
15+
// IgnoreUniverseTree is a structure that holds the DB for ignore operations.
16+
type IgnoreUniverseTree struct {
17+
db BatchedUniverseTree
18+
}
19+
20+
// NewIgnoreUniverseTree returns a new IgnoreUniverseTree with the target DB.
21+
func NewIgnoreUniverseTree(db BatchedUniverseTree) *IgnoreUniverseTree {
22+
return &IgnoreUniverseTree{db: db}
23+
}
24+
25+
// specifierToIdentifier converts an asset.Specifier into a universe.Identifier.
26+
func specifierToIdentifier(spec asset.Specifier) (universe.Identifier, error) {
27+
var id universe.Identifier
28+
29+
// The specifier must have a group key to be able to be used within the
30+
// ignore tree context.
31+
if !spec.HasGroupPubKey() {
32+
return id, fmt.Errorf("group key must be set")
33+
}
34+
35+
id.GroupKey = spec.UnwrapGroupKeyToPtr()
36+
37+
id.ProofType = universe.ProofTypeIgnore
38+
return id, nil
39+
}
40+
41+
// AddTuple adds a new ignore tuples to the ignore tree.
42+
func (it *IgnoreUniverseTree) AddTuples(ctx context.Context,
43+
spec asset.Specifier, tuples ...*universe.SignedIgnoreTuple,
44+
) lfn.Result[[]universe.AuthenticatedIgnoreTuple] {
45+
46+
if len(tuples) == 0 {
47+
return lfn.Err[[]universe.AuthenticatedIgnoreTuple](
48+
fmt.Errorf("no tuples provided"),
49+
)
50+
}
51+
52+
// Derive identifier (and thereby the namespace) from the
53+
// asset.Specifier.
54+
id, err := specifierToIdentifier(spec)
55+
if err != nil {
56+
return lfn.Err[[]universe.AuthenticatedIgnoreTuple](err)
57+
}
58+
59+
namespace := id.String()
60+
61+
groupKeyBytes := schnorr.SerializePubKey(id.GroupKey)
62+
63+
var finalResults []universe.AuthenticatedIgnoreTuple
64+
65+
var writeTx BaseUniverseStoreOptions
66+
txErr := it.db.ExecTx(ctx, &writeTx, func(db BaseUniverseStore) error {
67+
tree := mssmt.NewCompactedTree(
68+
newTreeStoreWrapperTx(db, namespace),
69+
)
70+
71+
// First, insert all tuples into the tree. This'll create a new
72+
// insert universe leaves to reference the SMT leafs. set of
73+
// normal SMT leafs. Once inserted, we'll then also
74+
for _, tup := range tuples {
75+
smtKey := tup.IgnoreTuple.Val.Hash()
76+
77+
ignoreTup := tup.IgnoreTuple.Val
78+
79+
leafNode, err := tup.UniverseLeafNode()
80+
if err != nil {
81+
return err
82+
}
83+
_, err = tree.Insert(ctx, smtKey, leafNode)
84+
if err != nil {
85+
return err
86+
}
87+
88+
// To insert the universe leaf below, we'll need both
89+
// the db primary key for the asset genesis, and also
90+
// the raw bytes of the outpoint to be ignored.
91+
assetGenID, err := db.LookupGenesisID(
92+
ctx, ignoreTup.ID[:],
93+
)
94+
if err != nil {
95+
return fmt.Errorf("error looking up genesis "+
96+
"ID for asset %v: %w", ignoreTup.ID,
97+
err)
98+
}
99+
ignorePointBytes, err := encodeOutpoint(
100+
tup.IgnoreTuple.Val.OutPoint,
101+
)
102+
if err != nil {
103+
return err
104+
}
105+
106+
// With the leaf inserted into the tree, we'll now
107+
// create the universe leaf that references the SMT
108+
// leaf.
109+
universeRootID, err := db.UpsertUniverseRoot(
110+
ctx, NewUniverseRoot{
111+
NamespaceRoot: namespace,
112+
GroupKey: groupKeyBytes,
113+
ProofType: sqlStr(
114+
id.ProofType.String(),
115+
),
116+
},
117+
)
118+
if err != nil {
119+
return err
120+
}
121+
122+
scriptKey := ignoreTup.ScriptKey
123+
err = db.UpsertUniverseLeaf(ctx, UpsertUniverseLeaf{
124+
AssetGenesisID: assetGenID,
125+
ScriptKeyBytes: scriptKey.SchnorrSerialized(), //nolint:lll
126+
UniverseRootID: universeRootID,
127+
LeafNodeKey: smtKey[:],
128+
LeafNodeNamespace: namespace,
129+
MintingPoint: ignorePointBytes,
130+
})
131+
if err != nil {
132+
return err
133+
}
134+
}
135+
136+
// Fetch the final tree root after all insertions.
137+
finalRoot, err := tree.Root(ctx)
138+
if err != nil {
139+
return err
140+
}
141+
142+
// Next, for each inserted tuple, generate its inclusion proof
143+
// from the final tree and build its AuthenticatedIgnoreTuple.
144+
for _, tup := range tuples {
145+
smtKey := tup.IgnoreTuple.Val.Hash()
146+
proof, err := tree.MerkleProof(ctx, smtKey)
147+
if err != nil {
148+
return err
149+
}
150+
151+
authTup := universe.AuthenticatedIgnoreTuple{
152+
SignedIgnoreTuple: tup,
153+
InclusionProof: proof,
154+
IgnoreTreeRoot: finalRoot,
155+
}
156+
157+
finalResults = append(finalResults, authTup)
158+
}
159+
160+
return nil
161+
})
162+
if txErr != nil {
163+
return lfn.Err[[]universe.AuthenticatedIgnoreTuple](txErr)
164+
}
165+
166+
return lfn.Ok(finalResults)
167+
}
168+
169+
// Sum returns the sum of the ignore tuples for the given asset.
170+
func (it *IgnoreUniverseTree) Sum(ctx context.Context,
171+
spec asset.Specifier) universe.SumQueryResp {
172+
173+
// Derive identifier from the asset.Specifier.
174+
id, err := specifierToIdentifier(spec)
175+
if err != nil {
176+
return lfn.Err[lfn.Option[uint64]](err)
177+
}
178+
namespace := id.String()
179+
180+
var sumValue uint64
181+
var foundSum bool
182+
183+
readTx := NewBaseUniverseReadTx()
184+
txErr := it.db.ExecTx(ctx, &readTx, func(db BaseUniverseStore) error {
185+
tree := mssmt.NewCompactedTree(
186+
newTreeStoreWrapperTx(db, namespace),
187+
)
188+
189+
// Get the root of the tree to retrieve the sum.
190+
root, err := tree.Root(ctx)
191+
if err != nil {
192+
return err
193+
}
194+
195+
// If root is empty, return empty sum.
196+
//
197+
// TODO(roasbeef): verify behavior
198+
if root.NodeHash() == mssmt.EmptyTreeRootHash {
199+
return nil
200+
}
201+
202+
// Return the sum from the root.
203+
sumValue = root.NodeSum()
204+
foundSum = true
205+
return nil
206+
})
207+
if txErr != nil {
208+
return lfn.Err[lfn.Option[uint64]](txErr)
209+
}
210+
211+
if !foundSum {
212+
return lfn.Ok(lfn.None[uint64]())
213+
}
214+
215+
return lfn.Ok(lfn.Some(sumValue))
216+
}
217+
218+
// ListTuples returns the list of ignore tuples for the given asset.
219+
func (it *IgnoreUniverseTree) ListTuples(ctx context.Context,
220+
spec asset.Specifier) lfn.Result[[]*universe.IgnoreTuple] {
221+
222+
// Derive identifier from the asset.Specifier.
223+
id, err := specifierToIdentifier(spec)
224+
if err != nil {
225+
return lfn.Err[[]*universe.IgnoreTuple](err)
226+
}
227+
228+
namespace := id.String()
229+
230+
var tuples []*universe.IgnoreTuple
231+
232+
readTx := NewBaseUniverseReadTx()
233+
txErr := it.db.ExecTx(ctx, &readTx, func(db BaseUniverseStore) error {
234+
// To list all the tuples, we'll just query for all the universe
235+
// leaves for this namespace. The namespace is derived from the
236+
// group key, and the proof type, which in this case is ignore.
237+
universeLeaves, err := db.QueryUniverseLeaves(
238+
ctx, UniverseLeafQuery{
239+
Namespace: namespace,
240+
},
241+
)
242+
if err != nil {
243+
return err
244+
}
245+
246+
for _, leaf := range universeLeaves {
247+
leafBytes := leaf.GenesisProof
248+
249+
tuple, err := universe.DecodeSignedIgnoreTuple(
250+
leafBytes,
251+
)
252+
if err != nil {
253+
return fmt.Errorf("error decoding tuple: "+
254+
"%w", err)
255+
}
256+
257+
tuples = append(tuples, &tuple.IgnoreTuple.Val)
258+
}
259+
260+
return nil
261+
})
262+
263+
if txErr != nil {
264+
return lfn.Err[[]*universe.IgnoreTuple](txErr)
265+
}
266+
267+
return lfn.Ok(tuples)
268+
}
269+
270+
// QueryTuples returns the ignore tuples for the given asset.
271+
func (it *IgnoreUniverseTree) QueryTuples(ctx context.Context,
272+
spec asset.Specifier,
273+
queryTuples ...universe.IgnoreTuple) universe.TupleQueryResp {
274+
275+
if len(queryTuples) == 0 {
276+
return lfn.Ok(lfn.None[[]universe.AuthenticatedIgnoreTuple]())
277+
}
278+
279+
// Derive identifier from the asset.Specifier.
280+
id, err := specifierToIdentifier(spec)
281+
if err != nil {
282+
return lfn.Err[lfn.Option[[]universe.AuthenticatedIgnoreTuple]](
283+
err,
284+
)
285+
}
286+
287+
namespace := id.String()
288+
289+
var (
290+
resultTuples []universe.AuthenticatedIgnoreTuple
291+
foundAny bool
292+
)
293+
294+
readTx := NewBaseUniverseReadTx()
295+
txErr := it.db.ExecTx(ctx, &readTx, func(db BaseUniverseStore) error {
296+
// Create the tree within the transaction for getting root and
297+
// proofs.
298+
tree := mssmt.NewCompactedTree(
299+
newTreeStoreWrapperTx(db, namespace),
300+
)
301+
302+
// Get the tree root once for all queries
303+
root, err := tree.Root(ctx)
304+
if err != nil {
305+
return err
306+
}
307+
308+
for _, queryTuple := range queryTuples {
309+
// Generate the SMT key for the query tuple.
310+
smtKey := queryTuple.Hash()
311+
312+
// Create a leaf query for this specific key.
313+
scriptKey := queryTuple.ScriptKey
314+
leafQuery := UniverseLeafQuery{
315+
ScriptKeyBytes: scriptKey.SchnorrSerialized(),
316+
Namespace: namespace,
317+
}
318+
319+
leaves, err := db.QueryUniverseLeaves(
320+
ctx, leafQuery,
321+
)
322+
if err != nil {
323+
return fmt.Errorf("error querying leaf "+
324+
"for tuple: %w", err)
325+
}
326+
327+
// Skip if no leaves found for this tuple.
328+
//
329+
// TODO(roasbeef): move to slice of results? then can
330+
// see if one failed or not for each
331+
if len(leaves) == 0 {
332+
continue
333+
}
334+
335+
// Get the first leaf (there should only be one for this
336+
// specific key).
337+
rawLeaf := leaves[0].GenesisProof
338+
339+
// With the key, we can generate the inclusion proof for
340+
// this tuple.
341+
proof, err := tree.MerkleProof(ctx, smtKey)
342+
if err != nil {
343+
return fmt.Errorf("error generating proof for "+
344+
"tuple: %w", err)
345+
}
346+
347+
// With all the data gathered, we can now create the
348+
// signed tuple along side the universe root and its
349+
// inclusion proof.
350+
signedTuple, err := universe.DecodeSignedIgnoreTuple(
351+
rawLeaf,
352+
)
353+
if err != nil {
354+
return fmt.Errorf("error deserializing "+
355+
"tuple: %w", err)
356+
}
357+
tup := universe.AuthenticatedIgnoreTuple{
358+
SignedIgnoreTuple: &signedTuple,
359+
InclusionProof: proof,
360+
IgnoreTreeRoot: root,
361+
}
362+
resultTuples = append(resultTuples, tup)
363+
364+
foundAny = true
365+
}
366+
367+
return nil
368+
})
369+
if txErr != nil {
370+
return lfn.Err[lfn.Option[[]universe.AuthenticatedIgnoreTuple]](
371+
txErr,
372+
)
373+
}
374+
375+
if !foundAny {
376+
return lfn.Ok(lfn.None[[]universe.AuthenticatedIgnoreTuple]())
377+
}
378+
379+
return lfn.Ok(lfn.Some(resultTuples))
380+
}

0 commit comments

Comments
 (0)