Skip to content

Commit

Permalink
Merge pull request lightninglabs#624 from lightninglabs/multiverse-trees
Browse files Browse the repository at this point in the history
multiverse: add overlay points for universe trees
  • Loading branch information
Roasbeef authored Dec 13, 2023
2 parents a51aaa4 + 55f07b1 commit 7c0688e
Show file tree
Hide file tree
Showing 27 changed files with 2,436 additions and 1,009 deletions.
43 changes: 43 additions & 0 deletions cmd/tapcli/universe.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var universeCommands = []cli.Command{
Usage: "Interact with a local or remote tap universe",
Category: "Universe",
Subcommands: []cli.Command{
multiverseRootCommand,
universeRootsCommand,
universeDeleteRootCommand,
universeLeavesCommand,
Expand All @@ -51,6 +52,48 @@ var universeCommands = []cli.Command{
},
}

var multiverseRootCommand = cli.Command{
Name: "multiverse",
ShortName: "m",
Description: "Show the multiverse root",
Usage: `
Calculate the multiverse root from the current known asset universes of
the given proof type.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: proofTypeName,
Usage: "the type of proof to show the root for, " +
"either 'issuance' or 'transfer'",
Value: universe.ProofTypeIssuance.String(),
},
},
Action: multiverseRoot,
}

func multiverseRoot(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getUniverseClient(ctx)
defer cleanUp()

rpcProofType, err := parseProofType(ctx)
if err != nil {
return err
}

multiverseRoot, err := client.MultiverseRoot(
ctxc, &unirpc.MultiverseRootRequest{
ProofType: *rpcProofType,
},
)
if err != nil {
return err
}

printRespJSON(multiverseRoot)
return nil
}

var universeRootsCommand = cli.Command{
Name: "roots",
ShortName: "r",
Expand Down
37 changes: 37 additions & 0 deletions fn/either.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package fn

// Either is a type that can be either left or right.
type Either[L any, R any] struct {
left Option[L]
right Option[R]
}

// NewLeft returns an Either with a left value.
func NewLeft[L any, R any](l L) Either[L, R] {
return Either[L, R]{left: Some(l), right: None[R]()}
}

// NewRight returns an Either with a right value.
func NewRight[L any, R any](r R) Either[L, R] {
return Either[L, R]{left: None[L](), right: Some(r)}
}

// WhenLeft executes the given function if the Either is left.
func (e Either[L, R]) WhenLeft(f func(L)) {
e.left.WhenSome(f)
}

// WhenRight executes the given function if the Either is right.
func (e Either[L, R]) WhenRight(f func(R)) {
e.right.WhenSome(f)
}

// IsLeft returns true if the Either is left.
func (e Either[L, R]) IsLeft() bool {
return e.left.IsSome()
}

// IsRight returns true if the Either is right.
func (e Either[L, R]) IsRight() bool {
return e.right.IsSome()
}
6 changes: 3 additions & 3 deletions fn/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ func Any[T any](xs []T, pred func(T) bool) bool {
return false
}

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

Expand Down
148 changes: 148 additions & 0 deletions fn/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package fn

// Option[A] represents a value which may or may not be there. This is very
// often preferable to nil-able pointers.
type Option[A any] struct {
isSome bool
some A
}

// Some trivially injects a value into an optional context.
//
// Some : A -> Option[A].
func Some[A any](a A) Option[A] {
return Option[A]{
isSome: true,
some: a,
}
}

// None trivially constructs an empty option
//
// None : Option[A].
func None[A any]() Option[A] {
return Option[A]{}
}

// ElimOption is the universal Option eliminator. It can be used to safely
// handle all possible values inside the Option by supplying two continuations.
//
// ElimOption : (Option[A], () -> B, A -> B) -> B.
func ElimOption[A, B any](o Option[A], b func() B, f func(A) B) B {
if o.isSome {
return f(o.some)
}

return b()
}

// UnwrapOr is used to extract a value from an option, and we supply the default
// value in the case when the Option is empty.
//
// UnwrapOr : (Option[A], A) -> A.
func (o Option[A]) UnwrapOr(a A) A {
if o.isSome {
return o.some
}

return a
}

// WhenSome is used to conditionally perform a side-effecting function that
// accepts a value of the type that parameterizes the option. If this function
// performs no side effects, WhenSome is useless.
//
// WhenSome : (Option[A], A -> ()) -> ().
func (o Option[A]) WhenSome(f func(A)) {
if o.isSome {
f(o.some)
}
}

// IsSome returns true if the Option contains a value
//
// IsSome : Option[A] -> bool.
func (o Option[A]) IsSome() bool {
return o.isSome
}

// IsNone returns true if the Option is empty
//
// IsNone : Option[A] -> bool.
func (o Option[A]) IsNone() bool {
return !o.isSome
}

// FlattenOption joins multiple layers of Options together such that if any of
// the layers is None, then the joined value is None. Otherwise the innermost
// Some value is returned.
//
// FlattenOption : Option[Option[A]] -> Option[A].
func FlattenOption[A any](oo Option[Option[A]]) Option[A] {
if oo.IsNone() {
return None[A]()
}
if oo.some.IsNone() {
return None[A]()
}

return oo.some
}

// ChainOption transforms a function A -> Option[B] into one that accepts an
// Option[A] as an argument.
//
// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B].
func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] {
return func(o Option[A]) Option[B] {
if o.isSome {
return f(o.some)
}

return None[B]()
}
}

// MapOption transforms a pure function A -> B into one that will operate
// inside the Option context.
//
// MapOption : (A -> B) -> Option[A] -> Option[B].
func MapOption[A, B any](f func(A) B) func(Option[A]) Option[B] {
return func(o Option[A]) Option[B] {
if o.isSome {
return Some(f(o.some))
}

return None[B]()
}
}

// LiftA2Option transforms a pure function (A, B) -> C into one that will
// operate in an Option context. For the returned function, if either of its
// arguments are None, then the result will be None.
//
// LiftA2Option : ((A, B) -> C) -> (Option[A], Option[B]) -> Option[C].
func LiftA2Option[A, B, C any](
f func(A, B) C) func(Option[A], Option[B]) Option[C] {

return func(o1 Option[A], o2 Option[B]) Option[C] {
if o1.isSome && o2.isSome {
return Some(f(o1.some, o2.some))
}

return None[C]()
}
}

// Alt chooses the left Option if it is full, otherwise it chooses the right
// option. This can be useful in a long chain if you want to choose between
// many different ways of producing the needed value.
//
// Alt : Option[A] -> Option[A] -> Option[A].
func (o Option[A]) Alt(o2 Option[A]) Option[A] {
if o.isSome {
return o
}

return o2
}
8 changes: 8 additions & 0 deletions internal/test/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ func SchnorrKey(t testing.TB, pubKey *btcec.PublicKey) *btcec.PublicKey {
return key
}

func SchnorrKeysEqual(t testing.TB, a, b *btcec.PublicKey) bool {
if a == nil || b == nil {
return a == b
}

return SchnorrKey(t, a).IsEqual(SchnorrKey(t, b))
}

func RandPubKey(t testing.TB) *btcec.PublicKey {
return SchnorrPubKey(t, RandPrivKey(t))
}
Expand Down
29 changes: 29 additions & 0 deletions itest/universe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,35 @@ func testUniverseSync(t *harnessTest) {
require.True(
t.t, AssertUniverseRootsEqual(universeRoots, universeRootsBob),
)

// Test the multiverse root is equal for both nodes.
multiverseRootAlice, err := t.tapd.MultiverseRoot(
ctxt, &unirpc.MultiverseRootRequest{
ProofType: unirpc.ProofType_PROOF_TYPE_ISSUANCE,
},
)
require.NoError(t.t, err)

// For Bob we query with the actual IDs of the universe we are aware of.
multiverseRootBob, err := bob.MultiverseRoot(
ctxt, &unirpc.MultiverseRootRequest{
ProofType: unirpc.ProofType_PROOF_TYPE_ISSUANCE,
SpecificIds: uniIDs,
},
)
require.NoError(t.t, err)

require.Equal(
t.t, multiverseRootAlice.MultiverseRoot.RootHash,
multiverseRootBob.MultiverseRoot.RootHash,
)

// We also expect the proof's root hash to be equal to the actual
// multiverse root.
require.Equal(
t.t, firstAssetUniProof.MultiverseRoot.RootHash,
multiverseRootBob.MultiverseRoot.RootHash,
)
}

// unmarshalMerkleSumNode un-marshals a protobuf MerkleSumNode.
Expand Down
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ var (
Entity: "mint",
Action: "read",
}},
"/universerpc.Universe/MultiverseRoot": {{
Entity: "universe",
Action: "read",
}},
"/universerpc.Universe/AssetRoots": {{
Entity: "universe",
Action: "read",
Expand Down
53 changes: 53 additions & 0 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2960,6 +2960,59 @@ func marshalUniverseRoot(node universe.Root) (*unirpc.UniverseRoot, error) {
}, nil
}

// MultiverseRoot returns the root of the multiverse tree. This is useful to
// determine the equality of two multiverse trees, since the root can directly
// be compared to another multiverse root to find out if a sync is required.
func (r *rpcServer) MultiverseRoot(ctx context.Context,
req *unirpc.MultiverseRootRequest) (*unirpc.MultiverseRootResponse,
error) {

proofType, err := UnmarshalUniProofType(req.ProofType)
if err != nil {
return nil, fmt.Errorf("invalid proof type: %w", err)
}

if proofType == universe.ProofTypeUnspecified {
return nil, fmt.Errorf("proof type must be specified")
}

filterByIDs := make([]universe.Identifier, len(req.SpecificIds))
for idx, rpcID := range req.SpecificIds {
filterByIDs[idx], err = UnmarshalUniID(rpcID)
if err != nil {
return nil, fmt.Errorf("unable to parse universe id: "+
"%w", err)
}

// Allow the RPC user to not specify the proof type for each ID
// individually since the outer one is mandatory.
if filterByIDs[idx].ProofType == universe.ProofTypeUnspecified {
filterByIDs[idx].ProofType = proofType
}

if filterByIDs[idx].ProofType != proofType {
return nil, fmt.Errorf("proof type mismatch in ID "+
"%d: %v != %v", idx, filterByIDs[idx].ProofType,
proofType)
}
}

rootNode, err := r.cfg.UniverseArchive.MultiverseRoot(
ctx, proofType, filterByIDs,
)
if err != nil {
return nil, fmt.Errorf("unable to fetch multiverse root: %w",
err)
}

var resp unirpc.MultiverseRootResponse
rootNode.WhenSome(func(node universe.MultiverseRoot) {
resp.MultiverseRoot = marshalMssmtNode(node)
})

return &resp, nil
}

// AssetRoots queries for the known Universe roots associated with each known
// asset. These roots represent the supply/audit state for each known asset.
func (r *rpcServer) AssetRoots(ctx context.Context,
Expand Down
Loading

0 comments on commit 7c0688e

Please sign in to comment.