From f90fa56a8ef12fa1fd6fdc99f6e88deeec9210b4 Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 6 Sep 2023 13:33:10 +0100 Subject: [PATCH 01/13] tapfreighter: fix proof transition state doc --- tapfreighter/chain_porter.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tapfreighter/chain_porter.go b/tapfreighter/chain_porter.go index 6859a60c8..c9c4d87eb 100644 --- a/tapfreighter/chain_porter.go +++ b/tapfreighter/chain_porter.go @@ -1012,8 +1012,9 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) { err := p.storeProofs(¤tPkg) return ¤tPkg, err - // At this point, the transfer transaction is confirmed on-chain. We go - // on to store the sender and receiver proofs in the proof archive. + // At this point, the transfer transaction is confirmed on-chain, and + // we've stored the sender and receiver proofs in the proof archive. + // We'll now attempt to transfer the receiver proof to the receiver. case SendStateReceiverProofTransfer: // We'll set the package state to complete early here so the // main loop breaks out. We'll continue to attempt proof From 9899ee459316966e8abfb48721ccbc3167e66f5e Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 29 Aug 2023 18:32:09 +0100 Subject: [PATCH 02/13] rpc: move MarshalAsset function to new file taprpc/marshal.go This refactor allows us to avoid an import cycle when the RPC proof courier marshals an asset. --- cmd/tapcli/universe.go | 3 +- rpcserver.go | 97 ++------------------------------------- taprpc/marshal.go | 100 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 95 deletions(-) create mode 100644 taprpc/marshal.go diff --git a/cmd/tapcli/universe.go b/cmd/tapcli/universe.go index 99e04a79b..a21cf1db4 100644 --- a/cmd/tapcli/universe.go +++ b/cmd/tapcli/universe.go @@ -7,6 +7,7 @@ import ( tap "github.com/lightninglabs/taproot-assets" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/lncfg" @@ -417,7 +418,7 @@ func universeProofInsert(ctx *cli.Context) error { if err != nil { return err } - rpcAsset, err := tap.MarshalAsset( + rpcAsset, err := taprpc.MarshalAsset( ctxc, &assetProof.Asset, false, true, nil, ) if err != nil { diff --git a/rpcserver.go b/rpcserver.go index b9341f54f..892e48b72 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -604,7 +604,7 @@ func (r *rpcServer) fetchRpcAssets(ctx context.Context, withWitness, func (r *rpcServer) marshalChainAsset(ctx context.Context, a *tapdb.ChainAsset, withWitness bool) (*taprpc.Asset, error) { - rpcAsset, err := MarshalAsset( + rpcAsset, err := taprpc.MarshalAsset( ctx, a.Asset, a.IsSpent, withWitness, r.cfg.AddrBook, ) if err != nil { @@ -641,97 +641,6 @@ func (r *rpcServer) marshalChainAsset(ctx context.Context, a *tapdb.ChainAsset, return rpcAsset, nil } -// KeyLookup is used to determine whether a key is under the control of the -// local wallet. -type KeyLookup interface { - // IsLocalKey returns true if the key is under the control of the - // wallet and can be derived by it. - IsLocalKey(ctx context.Context, desc keychain.KeyDescriptor) bool -} - -func MarshalAsset(ctx context.Context, a *asset.Asset, - isSpent, withWitness bool, - keyRing KeyLookup) (*taprpc.Asset, error) { - - assetID := a.Genesis.ID() - scriptKeyIsLocal := false - if a.ScriptKey.TweakedScriptKey != nil && keyRing != nil { - scriptKeyIsLocal = keyRing.IsLocalKey( - ctx, a.ScriptKey.RawKey, - ) - } - - rpcAsset := &taprpc.Asset{ - Version: int32(a.Version), - AssetGenesis: &taprpc.GenesisInfo{ - GenesisPoint: a.Genesis.FirstPrevOut.String(), - Name: a.Genesis.Tag, - MetaHash: a.Genesis.MetaHash[:], - AssetId: assetID[:], - OutputIndex: a.Genesis.OutputIndex, - }, - AssetType: taprpc.AssetType(a.Type), - Amount: a.Amount, - LockTime: int32(a.LockTime), - RelativeLockTime: int32(a.RelativeLockTime), - ScriptVersion: int32(a.ScriptVersion), - ScriptKey: a.ScriptKey.PubKey.SerializeCompressed(), - ScriptKeyIsLocal: scriptKeyIsLocal, - IsSpent: isSpent, - IsBurn: a.IsBurn(), - } - - if a.GroupKey != nil { - var rawKey []byte - if a.GroupKey.RawKey.PubKey != nil { - rawKey = a.GroupKey.RawKey.PubKey.SerializeCompressed() - } - rpcAsset.AssetGroup = &taprpc.AssetGroup{ - RawGroupKey: rawKey, - TweakedGroupKey: a.GroupKey.GroupPubKey.SerializeCompressed(), - AssetIdSig: a.GroupKey.Sig.Serialize(), - } - } - - if withWitness { - for idx := range a.PrevWitnesses { - witness := a.PrevWitnesses[idx] - - prevID := witness.PrevID - rpcPrevID := &taprpc.PrevInputAsset{ - AnchorPoint: prevID.OutPoint.String(), - AssetId: prevID.ID[:], - ScriptKey: prevID.ScriptKey[:], - } - - var rpcSplitCommitment *taprpc.SplitCommitment - if witness.SplitCommitment != nil { - rootAsset, err := MarshalAsset( - ctx, &witness.SplitCommitment.RootAsset, - false, true, nil, - ) - if err != nil { - return nil, err - } - - rpcSplitCommitment = &taprpc.SplitCommitment{ - RootAsset: rootAsset, - } - } - - rpcAsset.PrevWitnesses = append( - rpcAsset.PrevWitnesses, &taprpc.PrevWitness{ - PrevId: rpcPrevID, - TxWitness: witness.TxWitness, - SplitCommitment: rpcSplitCommitment, - }, - ) - } - } - - return rpcAsset, nil -} - func (r *rpcServer) listBalancesByAsset(ctx context.Context, assetID *asset.ID) (*taprpc.ListBalancesResponse, error) { @@ -2764,7 +2673,7 @@ func (r *rpcServer) AssetLeafKeys(ctx context.Context, return resp, nil } -func marshalAssetLeaf(ctx context.Context, keys KeyLookup, +func marshalAssetLeaf(ctx context.Context, keys taprpc.KeyLookup, assetLeaf *universe.MintingLeaf) (*unirpc.AssetLeaf, error) { // In order to display the full asset, we'll also encode the genesis @@ -2774,7 +2683,7 @@ func marshalAssetLeaf(ctx context.Context, keys KeyLookup, return nil, err } - rpcAsset, err := MarshalAsset( + rpcAsset, err := taprpc.MarshalAsset( ctx, &assetLeaf.GenesisProof.Asset, false, true, keys, ) if err != nil { diff --git a/taprpc/marshal.go b/taprpc/marshal.go new file mode 100644 index 000000000..452499b3b --- /dev/null +++ b/taprpc/marshal.go @@ -0,0 +1,100 @@ +package taprpc + +import ( + "context" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightningnetwork/lnd/keychain" +) + +// KeyLookup is used to determine whether a key is under the control of the +// local wallet. +type KeyLookup interface { + // IsLocalKey returns true if the key is under the control of the + // wallet and can be derived by it. + IsLocalKey(ctx context.Context, desc keychain.KeyDescriptor) bool +} + +// MarshalAsset converts an asset to its rpc representation. +func MarshalAsset(ctx context.Context, a *asset.Asset, + isSpent, withWitness bool, + keyRing KeyLookup) (*Asset, error) { + + assetID := a.Genesis.ID() + scriptKeyIsLocal := false + if a.ScriptKey.TweakedScriptKey != nil && keyRing != nil { + scriptKeyIsLocal = keyRing.IsLocalKey( + ctx, a.ScriptKey.RawKey, + ) + } + + rpcAsset := &Asset{ + Version: int32(a.Version), + AssetGenesis: &GenesisInfo{ + GenesisPoint: a.Genesis.FirstPrevOut.String(), + Name: a.Genesis.Tag, + MetaHash: a.Genesis.MetaHash[:], + AssetId: assetID[:], + OutputIndex: a.Genesis.OutputIndex, + }, + AssetType: AssetType(a.Type), + Amount: a.Amount, + LockTime: int32(a.LockTime), + RelativeLockTime: int32(a.RelativeLockTime), + ScriptVersion: int32(a.ScriptVersion), + ScriptKey: a.ScriptKey.PubKey.SerializeCompressed(), + ScriptKeyIsLocal: scriptKeyIsLocal, + IsSpent: isSpent, + IsBurn: a.IsBurn(), + } + + if a.GroupKey != nil { + var rawKey []byte + if a.GroupKey.RawKey.PubKey != nil { + rawKey = a.GroupKey.RawKey.PubKey.SerializeCompressed() + } + rpcAsset.AssetGroup = &AssetGroup{ + RawGroupKey: rawKey, + TweakedGroupKey: a.GroupKey.GroupPubKey.SerializeCompressed(), + AssetIdSig: a.GroupKey.Sig.Serialize(), + } + } + + if withWitness { + for idx := range a.PrevWitnesses { + witness := a.PrevWitnesses[idx] + + prevID := witness.PrevID + rpcPrevID := &PrevInputAsset{ + AnchorPoint: prevID.OutPoint.String(), + AssetId: prevID.ID[:], + ScriptKey: prevID.ScriptKey[:], + } + + var rpcSplitCommitment *SplitCommitment + if witness.SplitCommitment != nil { + rootAsset, err := MarshalAsset( + ctx, &witness.SplitCommitment.RootAsset, + false, true, nil, + ) + if err != nil { + return nil, err + } + + rpcSplitCommitment = &SplitCommitment{ + RootAsset: rootAsset, + } + } + + rpcAsset.PrevWitnesses = append( + rpcAsset.PrevWitnesses, &PrevWitness{ + PrevId: rpcPrevID, + TxWitness: witness.TxWitness, + SplitCommitment: rpcSplitCommitment, + }, + ) + } + } + + return rpcAsset, nil +} From 9184c17649af77d1a661d640950c4019ca361e1b Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 31 Aug 2023 15:22:56 +0100 Subject: [PATCH 03/13] taprpc: add marshal functions for universe RPC types --- taprpc/universerpc/marshal.go | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 taprpc/universerpc/marshal.go diff --git a/taprpc/universerpc/marshal.go b/taprpc/universerpc/marshal.go new file mode 100644 index 000000000..f6f12dec9 --- /dev/null +++ b/taprpc/universerpc/marshal.go @@ -0,0 +1,53 @@ +package universerpc + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" +) + +// MarshalOutpoint marshals a wire.OutPoint into an RPC ready Outpoint. +// +// TODO(ffranr): Move this package's Outpoint type and this marshal function to +// somewhere more general. +func MarshalOutpoint(outPoint wire.OutPoint) *Outpoint { + return &Outpoint{ + HashStr: outPoint.Hash.String(), + Index: int32(outPoint.Index), + } +} + +// MarshalAssetKey returns an RPC ready AssetKey. +func MarshalAssetKey(outPoint wire.OutPoint, + scriptKeyPubKey *btcec.PublicKey) *AssetKey { + + scriptKeyBytes := scriptKeyPubKey.SerializeCompressed() + + return &AssetKey{ + Outpoint: &AssetKey_Op{ + Op: MarshalOutpoint(outPoint), + }, + ScriptKey: &AssetKey_ScriptKeyBytes{ + ScriptKeyBytes: scriptKeyBytes, + }, + } +} + +// MarshalUniverseID returns an RPC ready universe ID. +func MarshalUniverseID(assetIDBytes []byte, groupKeyBytes []byte) *ID { + // We will marshal either a group key ID or an asset ID. If group key + // bytes are given, we marshal a group key ID, otherwise we marshal an + // asset ID. + if groupKeyBytes != nil { + return &ID{ + Id: &ID_GroupKey{ + GroupKey: groupKeyBytes, + }, + } + } + + return &ID{ + Id: &ID_AssetId{ + AssetId: assetIDBytes, + }, + } +} From d97665fa0a76e0cbc114051eebdf70f570236a81 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 31 Aug 2023 15:21:40 +0100 Subject: [PATCH 04/13] proof: clarify doc for Proof.PrevOut --- proof/proof.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proof/proof.go b/proof/proof.go index 4dd9af215..67f9d5b4a 100644 --- a/proof/proof.go +++ b/proof/proof.go @@ -131,7 +131,9 @@ type Watcher interface { // Proof encodes all of the data necessary to prove a valid state transition for // an asset has occurred within an on-chain transaction. type Proof struct { - // PrevOut is the previous on-chain outpoint of the asset. + // PrevOut is the previous on-chain outpoint of the asset. This outpoint + // is that of the first on-chain input. Outpoints which correspond to + // the other inputs can be found in AdditionalInputs. PrevOut wire.OutPoint // BlockHeader is the current block header committing to the on-chain From fa90ec946a5f3c815f46b552ef7561264bec018f Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 6 Sep 2023 16:43:12 +0100 Subject: [PATCH 05/13] asset: add asset method for retrieving PrevID which handles split commit This commit adds a method to the Asset structure which returns the asset's primary PrevID and takes account of the fact that the asset may correspond to a split output. --- asset/asset.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/asset/asset.go b/asset/asset.go index e7e6f92bd..870ffc9cf 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -822,6 +822,37 @@ func (a *Asset) IsBurn() bool { return IsBurnKey(a.ScriptKey.PubKey, a.PrevWitnesses[0]) } +// PrimaryPrevID returns the primary prev ID of an asset. This is the prev ID of +// the first witness, unless the first witness is a split-commitment witness, +// in which case it is the prev ID of the first witness of the root asset. +// The first witness effectively corresponds to the asset's direct lineage. +func (a *Asset) PrimaryPrevID() (*PrevID, error) { + if len(a.PrevWitnesses) == 0 { + return nil, fmt.Errorf("asset missing previous witnesses") + } + + // The primary prev ID is stored on the root asset if this asset is a + // split output. We determine whether this asset is a split output by + // inspecting the first previous witness. + primaryWitness := a.PrevWitnesses[0] + isSplitOutput := IsSplitCommitWitness(primaryWitness) + + // If this is a split output, then we need to look up the first PrevID + // in the split root asset. + if isSplitOutput { + rootAsset := primaryWitness.SplitCommitment.RootAsset + if len(rootAsset.PrevWitnesses) == 0 { + return nil, fmt.Errorf("asset missing previous " + + "witnesses") + } + return rootAsset.PrevWitnesses[0].PrevID, nil + } + + // This asset is not a split output, so we can just return the PrevID + // found in the first witness. + return primaryWitness.PrevID, nil +} + // Copy returns a deep copy of an Asset. func (a *Asset) Copy() *Asset { assetCopy := *a From 3d768f78c99a08a118a4cf2d0bfd8d7a2476b2a5 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 29 Aug 2023 18:35:21 +0100 Subject: [PATCH 06/13] multi: add universe RPC proof courier --- proof/archive.go | 4 + proof/courier.go | 285 +++++++++++++++++++++++++++++++++++++ tapdb/assets_store_test.go | 8 +- tapgarden/custodian.go | 1 + 4 files changed, 296 insertions(+), 2 deletions(-) diff --git a/proof/archive.go b/proof/archive.go index 23faccf18..95ca9b967 100644 --- a/proof/archive.go +++ b/proof/archive.go @@ -57,6 +57,10 @@ type Locator struct { // ScriptKey specifies the script key of the asset to fetch/store. This // field MUST be specified. ScriptKey btcec.PublicKey + + // OutPoint is the outpoint of the associated asset. This field is + // optional. + OutPoint *wire.OutPoint } // Hash returns a SHA256 hash of the bytes serialized locator. diff --git a/proof/courier.go b/proof/courier.go index 2272dd073..ca75cb627 100644 --- a/proof/courier.go +++ b/proof/courier.go @@ -14,6 +14,8 @@ import ( "github.com/lightninglabs/lightning-node-connect/hashmailrpc" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/taprpc" + unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -32,6 +34,10 @@ const ( // HashmailCourierType is a courier that uses the hashmail protocol to // deliver proofs. HashmailCourierType = "hashmail" + + // UniverseRpcCourierType is a courier that uses the daemon universe RPC + // endpoints to deliver proofs. + UniverseRpcCourierType = "universerpc" ) // CourierHarness interface is an integration testing harness for a proof @@ -95,6 +101,8 @@ func ParseCourierAddrUrl(addr url.URL) (CourierAddr, error) { switch addr.Scheme { case HashmailCourierType: return NewHashMailCourierAddr(addr) + case UniverseRpcCourierType: + return NewUniverseRpcCourierAddr(addr) } return nil, fmt.Errorf("unknown courier address protocol "+ @@ -159,6 +167,70 @@ func NewHashMailCourierAddr(addr url.URL) (*HashMailCourierAddr, error) { }, nil } +// UniverseRpcCourierAddr is a universe RPC protocol specific implementation of +// the CourierAddr interface. +type UniverseRpcCourierAddr struct { + addr url.URL +} + +// Url returns the url.URL representation of the courier address. +func (h *UniverseRpcCourierAddr) Url() *url.URL { + return &h.addr +} + +// NewCourier generates a new courier service handle. +func (h *UniverseRpcCourierAddr) NewCourier(_ context.Context, + cfg *CourierCfg, recipient Recipient) (Courier, error) { + + // Ensure that the courier address is a universe RPC address. + if h.addr.Scheme != UniverseRpcCourierType { + return nil, fmt.Errorf("unsupported courier protocol: %v", + h.addr.Scheme) + } + + // Connect to the universe RPC server. + dialOpts, err := serverDialOpts() + if err != nil { + return nil, err + } + + serverAddr := fmt.Sprintf( + "%s:%s", h.addr.Hostname(), h.addr.Port(), + ) + conn, err := grpc.Dial(serverAddr, dialOpts...) + if err != nil { + return nil, err + } + + client := unirpc.NewUniverseClient(conn) + + // Instantiate the events subscribers map. + subscribers := make( + map[uint64]*fn.EventReceiver[fn.Event], + ) + + return &UniverseRpcCourier{ + recipient: recipient, + client: client, + deliveryLog: cfg.DeliveryLog, + subscribers: subscribers, + }, nil +} + +// NewUniverseRpcCourierAddr generates a new universe RPC courier address from a +// given URL. This function also performs protocol specific address validation. +func NewUniverseRpcCourierAddr(addr url.URL) (*UniverseRpcCourierAddr, error) { + // We expect the port number to be specified. + if addr.Port() == "" { + return nil, fmt.Errorf("proof courier URI address port " + + "unspecified") + } + + return &UniverseRpcCourierAddr{ + addr, + }, nil +} + // NewCourier instantiates a new courier service handle given a service URL // address. func NewCourier(ctx context.Context, addr url.URL, cfg *CourierCfg, @@ -849,6 +921,219 @@ func (h *HashMailCourier) SetSubscribers( // proof.Courier interface. var _ Courier = (*HashMailCourier)(nil) +// UniverseRpcCourier is a universe RPC proof courier service handle. It +// implements the Courier interface. +type UniverseRpcCourier struct { + // recipient describes the recipient of the proof. + recipient Recipient + + // client is the RPC client that the courier will use to interact with + // the universe RPC server. + client unirpc.UniverseClient + + // deliveryLog is the log that the courier will use to record the + // attempted delivery of proofs to the receiver. + deliveryLog DeliveryLog + + // subscribers is a map of components that want to be notified on new + // events, keyed by their subscription ID. + subscribers map[uint64]*fn.EventReceiver[fn.Event] + + // subscriberMtx guards the subscribers map and access to the + // subscriptionID. + subscriberMtx sync.Mutex +} + +// DeliverProof attempts to delivery a proof file to the receiver. +func (c *UniverseRpcCourier) DeliverProof(ctx context.Context, + annotatedProof *AnnotatedProof) error { + + // Decode annotated proof into proof file. + proofFile := &File{} + err := proofFile.Decode(bytes.NewReader(annotatedProof.Blob)) + if err != nil { + return err + } + + // Iterate over each proof in the proof file and submit to the courier + // service. + for i := 0; i < proofFile.NumProofs(); i++ { + transitionProof, err := proofFile.ProofAt(uint32(i)) + if err != nil { + return err + } + proofAsset := transitionProof.Asset + + // Construct asset leaf. + rpcAsset, err := taprpc.MarshalAsset( + ctx, &proofAsset, true, true, nil, + ) + if err != nil { + return err + } + + var proofBuf bytes.Buffer + if err := transitionProof.Encode(&proofBuf); err != nil { + return fmt.Errorf("error encoding proof file: %w", err) + } + + assetLeaf := unirpc.AssetLeaf{ + Asset: rpcAsset, + IssuanceProof: proofBuf.Bytes(), + } + + // Construct universe key. + outPoint := transitionProof.OutPoint() + assetKey := unirpc.MarshalAssetKey( + outPoint, proofAsset.ScriptKey.PubKey, + ) + assetID := proofAsset.ID() + universeID := unirpc.MarshalUniverseID(assetID[:], nil) + universeKey := unirpc.UniverseKey{ + Id: universeID, + LeafKey: assetKey, + } + + // Before attempting to deliver the proof, log that an attempted + // delivery is about to occur. + var groupPubKey *btcec.PublicKey + if proofAsset.GroupKey != nil { + groupPubKey = &proofAsset.GroupKey.GroupPubKey + } + loc := Locator{ + AssetID: &assetID, + GroupKey: groupPubKey, + ScriptKey: *proofAsset.ScriptKey.PubKey, + OutPoint: &outPoint, + } + err = c.deliveryLog.StoreProofDeliveryAttempt(ctx, loc) + if err != nil { + return fmt.Errorf("unable to log proof delivery "+ + "attempt: %w", err) + } + + // Submit proof to courier. + _, err = c.client.InsertProof(ctx, &unirpc.AssetProof{ + Key: &universeKey, + AssetLeaf: &assetLeaf, + }) + if err != nil { + return fmt.Errorf("error inserting proof into "+ + "universe courier service: %w", err) + } + } + + return err +} + +// ReceiveProof attempts to obtain a proof file from the courier service. The +// final proof in the target proof file is identified by the given locator. +func (c *UniverseRpcCourier) ReceiveProof(ctx context.Context, + originLocator Locator) (*AnnotatedProof, error) { + + // In order to reconstruct the proof file we must collect all the + // transition proofs that make up the main chain of proofs. That is + // accomplished by iterating backwards through the main chain of proofs + // until we reach the genesis point (minting proof). + + // We will update the locator at each iteration. + loc := originLocator + + // revProofs is a slice of transition proofs ordered from latest to + // earliest (the issuance proof comes last in the slice). This ordering + // is a reversal of that found in the proof file. + var revProofs []Proof + + for { + assetID := *loc.AssetID + universeID := unirpc.MarshalUniverseID(assetID[:], nil) + assetKey := unirpc.MarshalAssetKey( + *loc.OutPoint, &loc.ScriptKey, + ) + universeKey := unirpc.UniverseKey{ + Id: universeID, + LeafKey: assetKey, + } + + resp, err := c.client.QueryProof(ctx, &universeKey) + if err != nil { + return nil, err + } + + // Decode transition proof from query response. + proofBlob := resp.AssetLeaf.IssuanceProof + var transitionProof Proof + if err := transitionProof.Decode( + bytes.NewReader(proofBlob), + ); err != nil { + return nil, err + } + + revProofs = append(revProofs, transitionProof) + + // Break if we've reached the genesis point (the asset is the + // genesis asset). + if transitionProof.Asset.HasGenesisWitness() { + break + } + + // Update locator with principal input to the current outpoint. + prevID, err := transitionProof.Asset.PrimaryPrevID() + if err != nil { + return nil, err + } + + // Parse script key public key. + scriptKeyPubKey, err := btcec.ParsePubKey(prevID.ScriptKey[:]) + if err != nil { + return nil, fmt.Errorf("failed to parse script key "+ + "public key from Proof.PrevID: %w", err) + } + loc.ScriptKey = *scriptKeyPubKey + + loc.AssetID = &prevID.ID + loc.OutPoint = &prevID.OutPoint + } + + // Append proofs to proof file in reverse order to their collected + // order. + proofFile := &File{} + for i := len(revProofs) - 1; i >= 0; i-- { + err := proofFile.AppendProof(revProofs[i]) + if err != nil { + return nil, fmt.Errorf("error appending proof to "+ + "proof file: %w", err) + } + } + + // Encode the full proof file. + var buf bytes.Buffer + if err := proofFile.Encode(&buf); err != nil { + return nil, fmt.Errorf("error encoding proof file: %w", err) + } + proofFileBlob := buf.Bytes() + + return &AnnotatedProof{ + Locator: originLocator, + Blob: proofFileBlob, + }, nil +} + +// SetSubscribers sets the subscribers for the courier. This method is +// thread-safe. +func (c *UniverseRpcCourier) SetSubscribers( + subscribers map[uint64]*fn.EventReceiver[fn.Event]) { + + c.subscriberMtx.Lock() + defer c.subscriberMtx.Unlock() + + c.subscribers = subscribers +} + +// A compile-time assertion to ensure the UniverseRpcCourier meets the +// proof.Courier interface. +var _ Courier = (*UniverseRpcCourier)(nil) + // DeliveryLog is an interface that allows the courier to log the (attempted) // delivery of a proof. type DeliveryLog interface { diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 87144b067..c4f60524a 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -339,7 +339,9 @@ func TestImportAssetProof(t *testing.T) { // Finally, we'll verify all the anchor information that was inserted // on disk. require.Equal(t, testProof.AnchorBlockHash, dbAsset.AnchorBlockHash) - require.Equal(t, testProof.OutPoint, dbAsset.AnchorOutpoint) + require.Equal( + t, testProof.AssetSnapshot.OutPoint, dbAsset.AnchorOutpoint, + ) require.Equal(t, testProof.AnchorTx.TxHash(), dbAsset.AnchorTx.TxHash()) // We should also be able to fetch the proof we just inserted using the @@ -395,7 +397,9 @@ func TestImportAssetProof(t *testing.T) { // Finally, we'll verify all the anchor information that was inserted // on disk. require.Equal(t, testProof.AnchorBlockHash, dbAsset.AnchorBlockHash) - require.Equal(t, testProof.OutPoint, dbAsset.AnchorOutpoint) + require.Equal( + t, testProof.AssetSnapshot.OutPoint, dbAsset.AnchorOutpoint, + ) require.Equal(t, testProof.AnchorTx.TxHash(), dbAsset.AnchorTx.TxHash()) } diff --git a/tapgarden/custodian.go b/tapgarden/custodian.go index 5bbe4ba86..efd840738 100644 --- a/tapgarden/custodian.go +++ b/tapgarden/custodian.go @@ -386,6 +386,7 @@ func (c *Custodian) inspectWalletTx(walletTx *lndclient.Transaction) error { loc := proof.Locator{ AssetID: &assetID, ScriptKey: addr.ScriptKey, + OutPoint: &op, } addrProof, err := courier.ReceiveProof(ctx, loc) if err != nil { From 1a180afe552c9d8bf3dd3b9e5c062ae75660b5da Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 13 Sep 2023 18:58:56 +0100 Subject: [PATCH 07/13] tapgarden: add proof retrieval delay to custodian --- tapgarden/custodian.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tapgarden/custodian.go b/tapgarden/custodian.go index efd840738..ab33d2d2e 100644 --- a/tapgarden/custodian.go +++ b/tapgarden/custodian.go @@ -18,6 +18,10 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" ) +const ( + defaultProofRetrievalDelay = 5 * time.Second +) + // CustodianConfig houses all the items that the Custodian needs to carry out // its duties. type CustodianConfig struct { @@ -382,6 +386,10 @@ func (c *Custodian) inspectWalletTx(walletTx *lndclient.Transaction) error { return } + // Sleep to give the sender an opportunity to transfer + // the proof to the proof courier service. + time.Sleep(defaultProofRetrievalDelay) + // Attempt to receive proof via proof courier service. loc := proof.Locator{ AssetID: &assetID, From a3ada70fce1b615e1d2629d6c6fbd0d04cf9b176 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 7 Sep 2023 15:36:31 +0100 Subject: [PATCH 08/13] proof: remove roasbeef's TODO now that we have two different couriers --- proof/courier.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/proof/courier.go b/proof/courier.go index ca75cb627..b4ae137e7 100644 --- a/proof/courier.go +++ b/proof/courier.go @@ -54,8 +54,6 @@ type CourierHarness interface { // part of the non-interactive send flow. A sender can use this given the // abstracted Addr/source type to send a proof to the receiver. Conversely, a // receiver can use this to fetch a proof from the sender. -// -// TODO(roasbeef): FileSystemCourier, RpcCourier type Courier interface { // DeliverProof attempts to delivery a proof to the receiver, using the // information in the Addr type. From 8904c8d058999ce572767afcb1b45859a15c7a25 Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 6 Sep 2023 16:30:47 +0100 Subject: [PATCH 09/13] rpc: add debug messages for InsertProof and QueryProof endpoints --- rpcserver.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index 892e48b72..7b3173506 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2909,10 +2909,17 @@ func (r *rpcServer) QueryProof(ctx context.Context, return nil, err } + rpcsLog.Debugf("[QueryProof]: fetching proof at "+ + "(universeID=%x, leafKey=%x)", universeID, + leafKey.UniverseKey()) + proofs, err := r.cfg.BaseUniverse.FetchIssuanceProof( ctx, universeID, leafKey, ) if err != nil { + rpcsLog.Debugf("[QueryProof]: error querying for proof at "+ + "(universeID=%x, leafKey=%x)", universeID, + leafKey.UniverseKey()) return nil, err } @@ -2920,6 +2927,10 @@ func (r *rpcServer) QueryProof(ctx context.Context, // not be fully specified proof := proofs[0] + rpcsLog.Debugf("[QueryProof]: found proof at "+ + "(universeID=%x, leafKey=%x)", universeID, + leafKey.UniverseKey()) + return r.marshalIssuanceProof(ctx, req, proof) } @@ -2976,6 +2987,10 @@ func (r *rpcServer) InsertProof(ctx context.Context, return nil, err } + rpcsLog.Debugf("[InsertProof]: inserting proof at "+ + "(universeID=%x, leafKey=%x)", universeID, + leafKey.UniverseKey()) + newUniverseState, err := r.cfg.BaseUniverse.RegisterIssuance( ctx, universeID, leafKey, assetLeaf, ) @@ -2983,6 +2998,10 @@ func (r *rpcServer) InsertProof(ctx context.Context, return nil, err } + universeRootHash := newUniverseState.UniverseRoot.NodeHash() + rpcsLog.Debugf("[InsertProof]: proof inserted, new universe root: %x", + universeRootHash[:]) + return r.marshalIssuanceProof(ctx, req.Key, newUniverseState) } From 6b6b7499bb9bbf204c1bb593171c7a76bae8afaf Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 16 Aug 2023 16:08:14 +0100 Subject: [PATCH 10/13] universe: allow registering non-issuance proofs in universe/multiverse --- universe/base.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/universe/base.go b/universe/base.go index 8d8e0879d..3f81e33ef 100644 --- a/universe/base.go +++ b/universe/base.go @@ -236,14 +236,6 @@ func (a *MintingArchive) verifyIssuanceProof(ctx context.Context, id Identifier, return nil, fmt.Errorf("asset id mismatch: expected %v, got %v", id.AssetID, newAsset.ID()) - // The outpoint of the final resting place of the asset should match - // the leaf key - // - // TODO(roasbeef): this restrict to issuance - case assetSnapshot.OutPoint != key.MintingOutpoint: - return nil, fmt.Errorf("outpoint mismatch: expected %v, got %v", - key.MintingOutpoint, assetSnapshot.OutPoint) - // The script key should also match exactly. case !newAsset.ScriptKey.PubKey.IsEqual(key.ScriptKey.PubKey): return nil, fmt.Errorf("script key mismatch: expected %v, got "+ From 0417d8d1a01c862b3983dfad5e40ccd6cb712dda Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 7 Sep 2023 15:02:08 +0100 Subject: [PATCH 11/13] universe: use previous asset snapshot when verifying transition proof --- universe/base.go | 81 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/universe/base.go b/universe/base.go index 3f81e33ef..adc97415d 100644 --- a/universe/base.go +++ b/universe/base.go @@ -7,8 +7,10 @@ import ( "fmt" "sync" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/davecgh/go-spew/spew" + "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" ) @@ -170,11 +172,21 @@ func (a *MintingArchive) RegisterIssuance(ctx context.Context, id Identifier, // Otherwise, this is a new proof, so we'll first perform validation of // the minting leaf to ensure it's a valid issuance proof. // - // The proofs we insert are just the state transition, so we'll encode - // it as a file first as that's what the expected wants. // // TODO(roasbeef): add option to skip proof verification? - assetSnapshot, err := a.verifyIssuanceProof(ctx, id, key, leaf) + + // Before we can validate a non-issuance proof we need to fetch the + // previous asset snapshot (which is the proof verification result for + // the previous/parent proof in the proof file). + prevAssetSnapshot, err := a.getPrevAssetSnapshot(ctx, id, *newProof) + if err != nil { + return nil, fmt.Errorf("unable to fetch previous asset "+ + "snapshot: %w", err) + } + + assetSnapshot, err := a.verifyIssuanceProof( + ctx, id, key, leaf, prevAssetSnapshot, + ) if err != nil { return nil, err } @@ -207,10 +219,11 @@ func (a *MintingArchive) RegisterIssuance(ctx context.Context, id Identifier, // verifyIssuanceProof verifies the passed minting leaf is a valid issuance // proof, returning the asset snapshot if so. func (a *MintingArchive) verifyIssuanceProof(ctx context.Context, id Identifier, - key BaseKey, leaf *MintingLeaf) (*proof.AssetSnapshot, error) { + key BaseKey, leaf *MintingLeaf, + prevAssetSnapshot *proof.AssetSnapshot) (*proof.AssetSnapshot, error) { assetSnapshot, err := leaf.GenesisProof.Verify( - ctx, nil, a.cfg.HeaderVerifier, + ctx, prevAssetSnapshot, a.cfg.HeaderVerifier, ) if err != nil { return nil, fmt.Errorf("unable to verify proof: %v", err) @@ -259,7 +272,7 @@ func (a *MintingArchive) RegisterNewIssuanceBatch(ctx context.Context, err := fn.ParSlice( ctx, items, func(ctx context.Context, i *IssuanceItem) error { assetSnapshot, err := a.verifyIssuanceProof( - ctx, i.ID, i.Key, i.Leaf, + ctx, i.ID, i.Key, i.Leaf, nil, ) if err != nil { return err @@ -298,6 +311,62 @@ func (a *MintingArchive) RegisterNewIssuanceBatch(ctx context.Context, return nil } +// getPrevAssetSnapshot returns the previous asset snapshot for the passed +// proof. If the proof is a genesis proof, then nil is returned. +func (a *MintingArchive) getPrevAssetSnapshot(ctx context.Context, + uniID Identifier, newProof proof.Proof) (*proof.AssetSnapshot, error) { + + // If this is a genesis proof, then there is no previous asset (and + // therefore no previous asset snapshot). + if newProof.Asset.HasGenesisWitness() { + return nil, nil + } + + // Query for proof associated with the previous asset. + prevID, err := newProof.Asset.PrimaryPrevID() + if err != nil { + return nil, err + } + + if prevID == nil { + return nil, fmt.Errorf("no previous asset ID found") + } + + // Parse script key for previous asset. + prevScriptKeyPubKey, err := btcec.ParsePubKey( + prevID.ScriptKey[:], + ) + if err != nil { + return nil, fmt.Errorf("unable to parse previous "+ + "script key: %v", err) + } + prevScriptKey := asset.NewScriptKey(prevScriptKeyPubKey) + + prevBaseKey := BaseKey{ + MintingOutpoint: prevID.OutPoint, + ScriptKey: &prevScriptKey, + } + + prevProofs, err := a.cfg.Multiverse.FetchIssuanceProof( + ctx, uniID, prevBaseKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to fetch previous "+ + "proof: %v", err) + } + + prevProof := prevProofs[0].Leaf.GenesisProof + + // Construct minimal asset snapshot for previous asset. + // This is a minimal the proof verification result for the + // previous (input) asset. We know that it was already verified + // as it was present in the multiverse/universe archive. + return &proof.AssetSnapshot{ + Asset: &prevProof.Asset, + OutPoint: prevID.OutPoint, + }, nil +} + // FetchIssuanceProof attempts to fetch an issuance proof for the target base // leaf based on the universe identifier (assetID/groupKey). func (a *MintingArchive) FetchIssuanceProof(ctx context.Context, id Identifier, From f39dd1a65aae0a9ec78ef487cc6fe786f03de944 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 31 Aug 2023 19:43:25 +0100 Subject: [PATCH 12/13] itest: fix nits --- itest/send_test.go | 4 ++-- itest/test_harness.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/itest/send_test.go b/itest/send_test.go index 4ea74bd9e..3513486d1 100644 --- a/itest/send_test.go +++ b/itest/send_test.go @@ -20,8 +20,8 @@ import ( "github.com/stretchr/testify/require" ) -// testBasicSend tests that we can properly send assets back and forth between -// nodes. +// testBasicSendUnidirectional tests that we can properly send assets back and +// forth between nodes. func testBasicSendUnidirectional(t *harnessTest) { var ( ctxb = context.Background() diff --git a/itest/test_harness.go b/itest/test_harness.go index 77825bcb7..4346db69c 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -175,7 +175,7 @@ func (h *harnessTest) LogfTimestamped(format string, args ...interface{}) { } // shutdown stops both the mock universe and tapd server. -func (h *harnessTest) shutdown(t *testing.T) error { +func (h *harnessTest) shutdown(_ *testing.T) error { h.universeServer.stop() if h.proofCourier != nil { From 147af79b670d14ba7b77ba74dcfcdb02b949aaba Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 31 Aug 2023 21:59:33 +0100 Subject: [PATCH 13/13] itest: add universe RPC harness and utilise for basic send --- itest/tapd_harness.go | 7 +++++ itest/test_harness.go | 3 +++ itest/test_list_on_test.go | 5 ++++ itest/universerpc_harness.go | 51 ++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 itest/universerpc_harness.go diff --git a/itest/tapd_harness.go b/itest/tapd_harness.go index 3d722ffea..284522936 100644 --- a/itest/tapd_harness.go +++ b/itest/tapd_harness.go @@ -177,6 +177,13 @@ func newTapdHarness(t *testing.T, ht *harnessTest, cfg tapdConfig, ReceiverAckTimeout: receiverAckTimeout, BackoffCfg: backoffCfg, } + + case *UniverseRPCHarness: + finalCfg.DefaultProofCourierAddr = fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + typedProofCourier.ListenAddr, + ) + default: finalCfg.DefaultProofCourierAddr = "" finalCfg.HashMailCourier = nil diff --git a/itest/test_harness.go b/itest/test_harness.go index 4346db69c..18605cd6d 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -284,6 +284,9 @@ func setupHarnesses(t *testing.T, ht *harnessTest, port := nextAvailablePort() apHarness := NewApertureHarness(ht.t, port) proofCourier = &apHarness + + case proof.UniverseRpcCourierType: + proofCourier = NewUniverseRPCHarness(t, ht, lndHarness.Bob) } // Start the proof courier harness if specified. diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 906814ecb..ecdc77bcc 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -43,6 +43,11 @@ var testCases = []*testCase{ test: testBasicSendUnidirectional, proofCourierType: proof.HashmailCourierType, }, + { + name: "basic send universerpc proof courier", + test: testBasicSendUnidirectional, + proofCourierType: proof.UniverseRpcCourierType, + }, { name: "resume pending package send", test: testResumePendingPackageSend, diff --git a/itest/universerpc_harness.go b/itest/universerpc_harness.go new file mode 100644 index 000000000..91f3a1ec4 --- /dev/null +++ b/itest/universerpc_harness.go @@ -0,0 +1,51 @@ +package itest + +import ( + "testing" + + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/stretchr/testify/require" +) + +// UniverseRPCHarness is an integration testing harness for the universe tap +// service. +type UniverseRPCHarness struct { + // service is the instance of the universe tap service. + service *tapdHarness + + // ListenAddr is the address that the service is listening on. + ListenAddr string +} + +// NewUniverseRPCHarness creates a new test harness for a universe tap service. +func NewUniverseRPCHarness(t *testing.T, ht *harnessTest, + lndHarness *node.HarnessNode) *UniverseRPCHarness { + + service, err := newTapdHarness( + t, ht, tapdConfig{ + NetParams: harnessNetParams, + LndNode: lndHarness, + }, nil, nil, nil, + ) + require.NoError(t, err) + + return &UniverseRPCHarness{ + service: service, + ListenAddr: service.rpcHost(), + } +} + +// Start starts the service. +func (h *UniverseRPCHarness) Start(_ chan error) error { + return h.service.start(false) +} + +// Stop stops the service. +func (h *UniverseRPCHarness) Stop() error { + return h.service.stop(true) +} + +// Ensure that NewUniverseRPCHarness implements the proof.CourierHarness +// interface. +var _ proof.CourierHarness = (*UniverseRPCHarness)(nil)