Skip to content

Commit 7c0688e

Browse files
authored
Merge pull request #624 from lightninglabs/multiverse-trees
multiverse: add overlay points for universe trees
2 parents a51aaa4 + 55f07b1 commit 7c0688e

27 files changed

+2436
-1009
lines changed

cmd/tapcli/universe.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var universeCommands = []cli.Command{
3838
Usage: "Interact with a local or remote tap universe",
3939
Category: "Universe",
4040
Subcommands: []cli.Command{
41+
multiverseRootCommand,
4142
universeRootsCommand,
4243
universeDeleteRootCommand,
4344
universeLeavesCommand,
@@ -51,6 +52,48 @@ var universeCommands = []cli.Command{
5152
},
5253
}
5354

55+
var multiverseRootCommand = cli.Command{
56+
Name: "multiverse",
57+
ShortName: "m",
58+
Description: "Show the multiverse root",
59+
Usage: `
60+
Calculate the multiverse root from the current known asset universes of
61+
the given proof type.
62+
`,
63+
Flags: []cli.Flag{
64+
cli.StringFlag{
65+
Name: proofTypeName,
66+
Usage: "the type of proof to show the root for, " +
67+
"either 'issuance' or 'transfer'",
68+
Value: universe.ProofTypeIssuance.String(),
69+
},
70+
},
71+
Action: multiverseRoot,
72+
}
73+
74+
func multiverseRoot(ctx *cli.Context) error {
75+
ctxc := getContext()
76+
client, cleanUp := getUniverseClient(ctx)
77+
defer cleanUp()
78+
79+
rpcProofType, err := parseProofType(ctx)
80+
if err != nil {
81+
return err
82+
}
83+
84+
multiverseRoot, err := client.MultiverseRoot(
85+
ctxc, &unirpc.MultiverseRootRequest{
86+
ProofType: *rpcProofType,
87+
},
88+
)
89+
if err != nil {
90+
return err
91+
}
92+
93+
printRespJSON(multiverseRoot)
94+
return nil
95+
}
96+
5497
var universeRootsCommand = cli.Command{
5598
Name: "roots",
5699
ShortName: "r",

fn/either.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package fn
2+
3+
// Either is a type that can be either left or right.
4+
type Either[L any, R any] struct {
5+
left Option[L]
6+
right Option[R]
7+
}
8+
9+
// NewLeft returns an Either with a left value.
10+
func NewLeft[L any, R any](l L) Either[L, R] {
11+
return Either[L, R]{left: Some(l), right: None[R]()}
12+
}
13+
14+
// NewRight returns an Either with a right value.
15+
func NewRight[L any, R any](r R) Either[L, R] {
16+
return Either[L, R]{left: None[L](), right: Some(r)}
17+
}
18+
19+
// WhenLeft executes the given function if the Either is left.
20+
func (e Either[L, R]) WhenLeft(f func(L)) {
21+
e.left.WhenSome(f)
22+
}
23+
24+
// WhenRight executes the given function if the Either is right.
25+
func (e Either[L, R]) WhenRight(f func(R)) {
26+
e.right.WhenSome(f)
27+
}
28+
29+
// IsLeft returns true if the Either is left.
30+
func (e Either[L, R]) IsLeft() bool {
31+
return e.left.IsSome()
32+
}
33+
34+
// IsRight returns true if the Either is right.
35+
func (e Either[L, R]) IsRight() bool {
36+
return e.right.IsSome()
37+
}

fn/func.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ func Any[T any](xs []T, pred func(T) bool) bool {
142142
return false
143143
}
144144

145-
// None returns true if the passed predicate returns false for all items in the
146-
// slice.
147-
func None[T any](xs []T, pred func(T) bool) bool {
145+
// NotAny returns true if the passed predicate returns false for all items in
146+
// the slice.
147+
func NotAny[T any](xs []T, pred func(T) bool) bool {
148148
return !Any(xs, pred)
149149
}
150150

fn/option.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package fn
2+
3+
// Option[A] represents a value which may or may not be there. This is very
4+
// often preferable to nil-able pointers.
5+
type Option[A any] struct {
6+
isSome bool
7+
some A
8+
}
9+
10+
// Some trivially injects a value into an optional context.
11+
//
12+
// Some : A -> Option[A].
13+
func Some[A any](a A) Option[A] {
14+
return Option[A]{
15+
isSome: true,
16+
some: a,
17+
}
18+
}
19+
20+
// None trivially constructs an empty option
21+
//
22+
// None : Option[A].
23+
func None[A any]() Option[A] {
24+
return Option[A]{}
25+
}
26+
27+
// ElimOption is the universal Option eliminator. It can be used to safely
28+
// handle all possible values inside the Option by supplying two continuations.
29+
//
30+
// ElimOption : (Option[A], () -> B, A -> B) -> B.
31+
func ElimOption[A, B any](o Option[A], b func() B, f func(A) B) B {
32+
if o.isSome {
33+
return f(o.some)
34+
}
35+
36+
return b()
37+
}
38+
39+
// UnwrapOr is used to extract a value from an option, and we supply the default
40+
// value in the case when the Option is empty.
41+
//
42+
// UnwrapOr : (Option[A], A) -> A.
43+
func (o Option[A]) UnwrapOr(a A) A {
44+
if o.isSome {
45+
return o.some
46+
}
47+
48+
return a
49+
}
50+
51+
// WhenSome is used to conditionally perform a side-effecting function that
52+
// accepts a value of the type that parameterizes the option. If this function
53+
// performs no side effects, WhenSome is useless.
54+
//
55+
// WhenSome : (Option[A], A -> ()) -> ().
56+
func (o Option[A]) WhenSome(f func(A)) {
57+
if o.isSome {
58+
f(o.some)
59+
}
60+
}
61+
62+
// IsSome returns true if the Option contains a value
63+
//
64+
// IsSome : Option[A] -> bool.
65+
func (o Option[A]) IsSome() bool {
66+
return o.isSome
67+
}
68+
69+
// IsNone returns true if the Option is empty
70+
//
71+
// IsNone : Option[A] -> bool.
72+
func (o Option[A]) IsNone() bool {
73+
return !o.isSome
74+
}
75+
76+
// FlattenOption joins multiple layers of Options together such that if any of
77+
// the layers is None, then the joined value is None. Otherwise the innermost
78+
// Some value is returned.
79+
//
80+
// FlattenOption : Option[Option[A]] -> Option[A].
81+
func FlattenOption[A any](oo Option[Option[A]]) Option[A] {
82+
if oo.IsNone() {
83+
return None[A]()
84+
}
85+
if oo.some.IsNone() {
86+
return None[A]()
87+
}
88+
89+
return oo.some
90+
}
91+
92+
// ChainOption transforms a function A -> Option[B] into one that accepts an
93+
// Option[A] as an argument.
94+
//
95+
// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B].
96+
func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] {
97+
return func(o Option[A]) Option[B] {
98+
if o.isSome {
99+
return f(o.some)
100+
}
101+
102+
return None[B]()
103+
}
104+
}
105+
106+
// MapOption transforms a pure function A -> B into one that will operate
107+
// inside the Option context.
108+
//
109+
// MapOption : (A -> B) -> Option[A] -> Option[B].
110+
func MapOption[A, B any](f func(A) B) func(Option[A]) Option[B] {
111+
return func(o Option[A]) Option[B] {
112+
if o.isSome {
113+
return Some(f(o.some))
114+
}
115+
116+
return None[B]()
117+
}
118+
}
119+
120+
// LiftA2Option transforms a pure function (A, B) -> C into one that will
121+
// operate in an Option context. For the returned function, if either of its
122+
// arguments are None, then the result will be None.
123+
//
124+
// LiftA2Option : ((A, B) -> C) -> (Option[A], Option[B]) -> Option[C].
125+
func LiftA2Option[A, B, C any](
126+
f func(A, B) C) func(Option[A], Option[B]) Option[C] {
127+
128+
return func(o1 Option[A], o2 Option[B]) Option[C] {
129+
if o1.isSome && o2.isSome {
130+
return Some(f(o1.some, o2.some))
131+
}
132+
133+
return None[C]()
134+
}
135+
}
136+
137+
// Alt chooses the left Option if it is full, otherwise it chooses the right
138+
// option. This can be useful in a long chain if you want to choose between
139+
// many different ways of producing the needed value.
140+
//
141+
// Alt : Option[A] -> Option[A] -> Option[A].
142+
func (o Option[A]) Alt(o2 Option[A]) Option[A] {
143+
if o.isSome {
144+
return o
145+
}
146+
147+
return o2
148+
}

internal/test/helpers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ func SchnorrKey(t testing.TB, pubKey *btcec.PublicKey) *btcec.PublicKey {
9999
return key
100100
}
101101

102+
func SchnorrKeysEqual(t testing.TB, a, b *btcec.PublicKey) bool {
103+
if a == nil || b == nil {
104+
return a == b
105+
}
106+
107+
return SchnorrKey(t, a).IsEqual(SchnorrKey(t, b))
108+
}
109+
102110
func RandPubKey(t testing.TB) *btcec.PublicKey {
103111
return SchnorrPubKey(t, RandPrivKey(t))
104112
}

itest/universe_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,35 @@ func testUniverseSync(t *harnessTest) {
261261
require.True(
262262
t.t, AssertUniverseRootsEqual(universeRoots, universeRootsBob),
263263
)
264+
265+
// Test the multiverse root is equal for both nodes.
266+
multiverseRootAlice, err := t.tapd.MultiverseRoot(
267+
ctxt, &unirpc.MultiverseRootRequest{
268+
ProofType: unirpc.ProofType_PROOF_TYPE_ISSUANCE,
269+
},
270+
)
271+
require.NoError(t.t, err)
272+
273+
// For Bob we query with the actual IDs of the universe we are aware of.
274+
multiverseRootBob, err := bob.MultiverseRoot(
275+
ctxt, &unirpc.MultiverseRootRequest{
276+
ProofType: unirpc.ProofType_PROOF_TYPE_ISSUANCE,
277+
SpecificIds: uniIDs,
278+
},
279+
)
280+
require.NoError(t.t, err)
281+
282+
require.Equal(
283+
t.t, multiverseRootAlice.MultiverseRoot.RootHash,
284+
multiverseRootBob.MultiverseRoot.RootHash,
285+
)
286+
287+
// We also expect the proof's root hash to be equal to the actual
288+
// multiverse root.
289+
require.Equal(
290+
t.t, firstAssetUniProof.MultiverseRoot.RootHash,
291+
multiverseRootBob.MultiverseRoot.RootHash,
292+
)
264293
}
265294

266295
// unmarshalMerkleSumNode un-marshals a protobuf MerkleSumNode.

perms/perms.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ var (
136136
Entity: "mint",
137137
Action: "read",
138138
}},
139+
"/universerpc.Universe/MultiverseRoot": {{
140+
Entity: "universe",
141+
Action: "read",
142+
}},
139143
"/universerpc.Universe/AssetRoots": {{
140144
Entity: "universe",
141145
Action: "read",

rpcserver.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2960,6 +2960,59 @@ func marshalUniverseRoot(node universe.Root) (*unirpc.UniverseRoot, error) {
29602960
}, nil
29612961
}
29622962

2963+
// MultiverseRoot returns the root of the multiverse tree. This is useful to
2964+
// determine the equality of two multiverse trees, since the root can directly
2965+
// be compared to another multiverse root to find out if a sync is required.
2966+
func (r *rpcServer) MultiverseRoot(ctx context.Context,
2967+
req *unirpc.MultiverseRootRequest) (*unirpc.MultiverseRootResponse,
2968+
error) {
2969+
2970+
proofType, err := UnmarshalUniProofType(req.ProofType)
2971+
if err != nil {
2972+
return nil, fmt.Errorf("invalid proof type: %w", err)
2973+
}
2974+
2975+
if proofType == universe.ProofTypeUnspecified {
2976+
return nil, fmt.Errorf("proof type must be specified")
2977+
}
2978+
2979+
filterByIDs := make([]universe.Identifier, len(req.SpecificIds))
2980+
for idx, rpcID := range req.SpecificIds {
2981+
filterByIDs[idx], err = UnmarshalUniID(rpcID)
2982+
if err != nil {
2983+
return nil, fmt.Errorf("unable to parse universe id: "+
2984+
"%w", err)
2985+
}
2986+
2987+
// Allow the RPC user to not specify the proof type for each ID
2988+
// individually since the outer one is mandatory.
2989+
if filterByIDs[idx].ProofType == universe.ProofTypeUnspecified {
2990+
filterByIDs[idx].ProofType = proofType
2991+
}
2992+
2993+
if filterByIDs[idx].ProofType != proofType {
2994+
return nil, fmt.Errorf("proof type mismatch in ID "+
2995+
"%d: %v != %v", idx, filterByIDs[idx].ProofType,
2996+
proofType)
2997+
}
2998+
}
2999+
3000+
rootNode, err := r.cfg.UniverseArchive.MultiverseRoot(
3001+
ctx, proofType, filterByIDs,
3002+
)
3003+
if err != nil {
3004+
return nil, fmt.Errorf("unable to fetch multiverse root: %w",
3005+
err)
3006+
}
3007+
3008+
var resp unirpc.MultiverseRootResponse
3009+
rootNode.WhenSome(func(node universe.MultiverseRoot) {
3010+
resp.MultiverseRoot = marshalMssmtNode(node)
3011+
})
3012+
3013+
return &resp, nil
3014+
}
3015+
29633016
// AssetRoots queries for the known Universe roots associated with each known
29643017
// asset. These roots represent the supply/audit state for each known asset.
29653018
func (r *rpcServer) AssetRoots(ctx context.Context,

0 commit comments

Comments
 (0)