Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multiverse: add overlay points for universe trees #624

Merged
merged 16 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
guggero marked this conversation as resolved.
Show resolved Hide resolved

// 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