Skip to content

Commit 16f9f9a

Browse files
committed
tapdb: add FetchSupplyLeavesByHeight to SupplyTreeStore
In this commit, we add a new method to the SupplyTreeStore that is able to read out the leaves of a supply tree based on a start and end height. This will be useful for writing the new syncing state machine and the sub-system that serves the supply tree syncer.
1 parent 35b2210 commit 16f9f9a

File tree

2 files changed

+321
-1
lines changed

2 files changed

+321
-1
lines changed

tapdb/supply_tree.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package tapdb
22

33
import (
4+
"bytes"
45
"context"
6+
"database/sql"
57
"encoding/hex"
8+
"errors"
69
"fmt"
710

811
"github.com/btcsuite/btcd/btcec/v2"
912
"github.com/lightninglabs/taproot-assets/asset"
1013
"github.com/lightninglabs/taproot-assets/mssmt"
1114
"github.com/lightninglabs/taproot-assets/proof"
15+
"github.com/lightninglabs/taproot-assets/tapdb/sqlc"
1216
"github.com/lightninglabs/taproot-assets/universe"
1317
"github.com/lightninglabs/taproot-assets/universe/supplycommit"
1418

@@ -599,3 +603,112 @@ func (s *SupplyTreeStore) ApplySupplyUpdates(ctx context.Context,
599603

600604
return finalRoot, nil
601605
}
606+
607+
// SupplyUpdate is a struct that holds a supply update event and its block
608+
// height.
609+
type SupplyUpdate struct {
610+
supplycommit.SupplyUpdateEvent
611+
BlockHeight uint32
612+
}
613+
614+
// FetchSupplyLeavesByHeight fetches all supply leaves for a given asset
615+
// specifier within a given block height range.
616+
func (s *SupplyTreeStore) FetchSupplyLeavesByHeight(ctx context.Context,
617+
spec asset.Specifier, startHeight, endHeight uint32) ([]SupplyUpdate, error) {
618+
619+
groupKey, err := spec.UnwrapGroupKeyOrErr()
620+
if err != nil {
621+
return nil, fmt.Errorf("group key must be "+
622+
"specified for supply tree: %w", err)
623+
}
624+
625+
var updates []SupplyUpdate
626+
627+
readTx := NewBaseUniverseReadTx()
628+
dbErr := s.db.ExecTx(ctx, &readTx, func(db BaseUniverseStore) error {
629+
for _, treeType := range []supplycommit.SupplySubTree{
630+
supplycommit.MintTreeType, supplycommit.BurnTreeType,
631+
supplycommit.IgnoreTreeType,
632+
} {
633+
namespace := subTreeNamespace(groupKey, treeType)
634+
635+
leaves, err := db.QuerySupplyLeavesByHeight(
636+
ctx, sqlc.QuerySupplyLeavesByHeightParams{
637+
Namespace: namespace,
638+
StartHeight: sqlInt32(startHeight),
639+
EndHeight: sqlInt32(endHeight),
640+
},
641+
)
642+
if err != nil {
643+
if errors.Is(err, sql.ErrNoRows) {
644+
continue
645+
}
646+
647+
return fmt.Errorf("failed to query "+
648+
"supply leaves: %w", err)
649+
}
650+
651+
for _, leaf := range leaves {
652+
var event supplycommit.SupplyUpdateEvent
653+
switch treeType {
654+
case supplycommit.MintTreeType:
655+
var mintEvent supplycommit.NewMintEvent
656+
err = mintEvent.Decode(
657+
bytes.NewReader(
658+
leaf.SupplyLeafBytes,
659+
),
660+
)
661+
if err != nil {
662+
return fmt.Errorf("failed "+
663+
"to decode mint "+
664+
"event: %w", err)
665+
}
666+
667+
event = &mintEvent
668+
669+
case supplycommit.BurnTreeType:
670+
var burnEvent supplycommit.NewBurnEvent
671+
err = burnEvent.Decode(
672+
bytes.NewReader(
673+
leaf.SupplyLeafBytes,
674+
),
675+
)
676+
if err != nil {
677+
return fmt.Errorf("failed "+
678+
"to decode burn "+
679+
"event: %w", err)
680+
}
681+
682+
event = &burnEvent
683+
684+
case supplycommit.IgnoreTreeType:
685+
var ignoreEvent supplycommit.NewIgnoreEvent
686+
err = ignoreEvent.Decode(
687+
bytes.NewReader(
688+
leaf.SupplyLeafBytes,
689+
),
690+
)
691+
if err != nil {
692+
return fmt.Errorf("failed "+
693+
"to decode ignore "+
694+
"event: %w", err)
695+
}
696+
event = &ignoreEvent
697+
}
698+
699+
updates = append(updates, SupplyUpdate{
700+
SupplyUpdateEvent: event,
701+
BlockHeight: extractSqlInt32[uint32](
702+
leaf.BlockHeight,
703+
),
704+
})
705+
}
706+
}
707+
return nil
708+
})
709+
if dbErr != nil {
710+
return nil, dbErr
711+
}
712+
713+
return updates, nil
714+
}

tapdb/supply_tree_test.go

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ func randIgnoreTupleGen(t *rapid.T,
248248
ScriptKey: asset.ToSerialized(scriptKey.PubKey),
249249
OutPoint: op,
250250
},
251-
Amount: 100,
251+
Amount: 100,
252+
BlockHeight: rapid.Uint32Range(1, 1000).Draw(t, "block_height"),
252253
}
253254

254255
// Create a signature for the ignore tuple.
@@ -367,6 +368,101 @@ func setupSupplyTreeTestForProps(t *testing.T) (*SupplyTreeStore,
367368
return supplyStore, spec, eventGen
368369
}
369370

371+
// createMintEventWithHeight creates a mint event with a specific block height.
372+
func createMintEventWithHeight(t *testing.T, groupKey *btcec.PublicKey,
373+
height uint32) *supplycommit.NewMintEvent {
374+
375+
mintAsset := asset.RandAsset(t, asset.Normal)
376+
mintAsset.GroupKey = &asset.GroupKey{GroupPubKey: *groupKey}
377+
mintAsset.GroupKey.Witness = mintAsset.PrevWitnesses[0].TxWitness
378+
379+
mintProof := randProof(t, mintAsset)
380+
mintProof.BlockHeight = height
381+
mintProof.GroupKeyReveal = asset.NewGroupKeyRevealV0(
382+
asset.ToSerialized(groupKey), nil,
383+
)
384+
385+
var proofBuf bytes.Buffer
386+
require.NoError(t, mintProof.Encode(&proofBuf))
387+
388+
mintLeaf := universe.Leaf{
389+
GenesisWithGroup: universe.GenesisWithGroup{
390+
Genesis: mintAsset.Genesis,
391+
GroupKey: mintAsset.GroupKey,
392+
},
393+
Asset: &mintProof.Asset,
394+
Amt: mintProof.Asset.Amount,
395+
RawProof: proofBuf.Bytes(),
396+
}
397+
398+
mintKey := universe.AssetLeafKey{
399+
BaseLeafKey: universe.BaseLeafKey{
400+
OutPoint: mintProof.OutPoint(),
401+
ScriptKey: &mintProof.Asset.ScriptKey,
402+
},
403+
AssetID: mintProof.Asset.ID(),
404+
}
405+
406+
return &supplycommit.NewMintEvent{
407+
LeafKey: mintKey,
408+
IssuanceProof: mintLeaf,
409+
}
410+
}
411+
412+
// createBurnEventWithHeight creates a burn event with a specific block height.
413+
func createBurnEventWithHeight(t *testing.T, baseGenesis asset.Genesis,
414+
groupKey *asset.GroupKey, db BatchedUniverseTree,
415+
height uint32) *supplycommit.NewBurnEvent {
416+
417+
burnAsset := createBurnAsset(t)
418+
burnAsset.Genesis = baseGenesis
419+
burnAsset.GroupKey = groupKey
420+
421+
burnProof := randProof(t, burnAsset)
422+
burnProof.BlockHeight = height
423+
burnProof.GenesisReveal = &baseGenesis
424+
425+
// Ensure genesis exists for this burn leaf in the DB.
426+
ctx := context.Background()
427+
genesisPointID, err := upsertGenesisPoint(
428+
ctx, db, burnAsset.Genesis.FirstPrevOut,
429+
)
430+
require.NoError(t, err)
431+
_, err = upsertGenesis(
432+
ctx, db, genesisPointID, burnAsset.Genesis,
433+
)
434+
require.NoError(t, err)
435+
436+
burnLeaf := &universe.BurnLeaf{
437+
UniverseKey: universe.AssetLeafKey{
438+
BaseLeafKey: universe.BaseLeafKey{
439+
OutPoint: burnProof.OutPoint(),
440+
ScriptKey: &burnProof.Asset.ScriptKey,
441+
},
442+
AssetID: burnProof.Asset.ID(),
443+
},
444+
BurnProof: burnProof,
445+
}
446+
447+
return &supplycommit.NewBurnEvent{
448+
BurnLeaf: *burnLeaf,
449+
}
450+
}
451+
452+
// createIgnoreEventWithHeight creates an ignore event with a specific block
453+
// height.
454+
func createIgnoreEventWithHeight(t *testing.T, baseAssetID asset.ID,
455+
db BatchedUniverseTree, height uint32) *supplycommit.NewIgnoreEvent {
456+
457+
signedTuple := randIgnoreTuple(t, db)
458+
signedTuple.IgnoreTuple.Val.ID = baseAssetID
459+
signedTuple.IgnoreTuple.Val.BlockHeight = height
460+
461+
return &supplycommit.NewIgnoreEvent{
462+
SignedIgnoreTuple: signedTuple,
463+
}
464+
}
465+
370466
// TestSupplyTreeStoreApplySupplyUpdates tests that the ApplySupplyUpdates meets
371467
// a series of key invariant via property based testing.
372468
func TestSupplyTreeStoreApplySupplyUpdates(t *testing.T) {
@@ -538,3 +634,114 @@ func TestSupplyTreeStoreApplySupplyUpdates(t *testing.T) {
538634
)
539635
require.NoError(t, err)
540636
}
637+
638+
// TestSupplyTreeStoreFetchSupplyLeavesByHeight tests the
639+
// FetchSupplyLeavesByHeight method.
640+
func TestSupplyTreeStoreFetchSupplyLeavesByHeight(t *testing.T) {
641+
t.Parallel()
642+
643+
supplyStore, spec, _ := setupSupplyTreeTestForProps(t)
644+
ctxb := context.Background()
645+
dbTxer := supplyStore.db
646+
647+
groupKey, err := spec.UnwrapGroupKeyOrErr()
648+
require.NoError(t, err)
649+
assetID := spec.UnwrapIdToPtr()
650+
651+
fullGroupKey := &asset.GroupKey{
652+
GroupPubKey: *groupKey,
653+
}
654+
655+
// Create events with specific block heights, we'll use these heights
656+
// below to ensure that the new leaf height is properly set/read all the
657+
// way down the call stack.
658+
mintEvent100 := createMintEventWithHeight(t, groupKey, 100)
659+
burnEvent200 := createBurnEventWithHeight(
660+
t, asset.RandGenesis(t, asset.Normal), fullGroupKey, dbTxer,
661+
200,
662+
)
663+
ignoreEvent300 := createIgnoreEventWithHeight(t, *assetID, dbTxer, 300)
664+
mintEvent400 := createMintEventWithHeight(t, groupKey, 400)
665+
666+
updates := []supplycommit.SupplyUpdateEvent{
667+
mintEvent100, burnEvent200, ignoreEvent300, mintEvent400,
668+
}
669+
670+
// Apply updates.
671+
_, err = supplyStore.ApplySupplyUpdates(ctxb, spec, updates)
672+
require.NoError(t, err)
673+
674+
testCases := []struct {
675+
name string
676+
startHeight uint32
677+
endHeight uint32
678+
expectedCount int
679+
expectedHeights []uint32
680+
}{
681+
{
682+
name: "range including first",
683+
startHeight: 0,
684+
endHeight: 150,
685+
expectedCount: 1,
686+
expectedHeights: []uint32{100},
687+
},
688+
{
689+
name: "range including second",
690+
startHeight: 150,
691+
endHeight: 250,
692+
expectedCount: 1,
693+
expectedHeights: []uint32{200},
694+
},
695+
{
696+
name: "range including all",
697+
startHeight: 0,
698+
endHeight: 500,
699+
expectedCount: 4,
700+
expectedHeights: []uint32{100, 200, 300, 400},
701+
},
702+
{
703+
name: "exact range",
704+
startHeight: 100,
705+
endHeight: 400,
706+
expectedCount: 4,
707+
expectedHeights: []uint32{100, 200, 300, 400},
708+
},
709+
{
710+
name: "inner range",
711+
startHeight: 101,
712+
endHeight: 399,
713+
expectedCount: 2,
714+
expectedHeights: []uint32{200, 300},
715+
},
716+
{
717+
name: "range after all",
718+
startHeight: 501,
719+
endHeight: 1000,
720+
expectedCount: 0,
721+
expectedHeights: nil,
722+
},
723+
{
724+
name: "range before all",
725+
startHeight: 0,
726+
endHeight: 99,
727+
expectedCount: 0,
728+
expectedHeights: nil,
729+
},
730+
}
731+
732+
for _, tc := range testCases {
733+
t.Run(tc.name, func(t *testing.T) {
734+
leaves, err := supplyStore.FetchSupplyLeavesByHeight(
735+
ctxb, spec, tc.startHeight, tc.endHeight,
736+
)
737+
require.NoError(t, err)
738+
require.Len(t, leaves, tc.expectedCount)
739+
740+
var heights []uint32
741+
for _, leaf := range leaves {
742+
heights = append(heights, leaf.BlockHeight)
743+
}
744+
require.ElementsMatch(t, tc.expectedHeights, heights)
745+
})
746+
}
747+
}

0 commit comments

Comments
 (0)