diff --git a/asset/asset.go b/asset/asset.go index dcf05b849..58e4e74e7 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -808,6 +808,42 @@ func (a *Asset) IsUnSpendable() bool { a.Amount == 0 } +// IsSplitCommitWitness returns true if the witness is a split-commitment +// witness. +func IsSplitCommitWitness(witness Witness) bool { + return witness.PrevID != nil && len(witness.TxWitness) == 0 && + witness.SplitCommitment != nil +} + +// 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. +func (a *Asset) PrimaryPrevID() *PrevID { + if len(a.PrevWitnesses) == 0 { + return nil + } + + // 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 + } + return rootAsset.PrevWitnesses[0].PrevID + } + + // This asset is not a split output, so we can just return the PrevID + // found in the first witness. + return primaryWitness.PrevID +} + // Copy returns a deep copy of an Asset. func (a *Asset) Copy() *Asset { assetCopy := *a diff --git a/itest/send_test.go b/itest/send_test.go index e780c95dc..74ab94a53 100644 --- a/itest/send_test.go +++ b/itest/send_test.go @@ -30,7 +30,7 @@ func testBasicSendUnidirectional(t *harnessTest) { const ( numUnits = 10 - numSends = 2 + numSends = 1 ) // Subscribe to receive assent send events from primary tapd node. diff --git a/rpcserver.go b/rpcserver.go index 01b7a7f48..ee8139c07 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2745,6 +2745,10 @@ func (r *rpcServer) QueryProof(ctx context.Context, // not be fully specified proof := proofs[0] + rpcsLog.Infof("[QueryProof]: returning proof at "+ + "(universeID=%x, leafKey=%x)", universeID.String(), + leafKey.UniverseKey()) + return r.marshalIssuanceProof(ctx, req, proof) } @@ -2804,6 +2808,10 @@ func (r *rpcServer) InsertProof(ctx context.Context, return nil, err } + rpcsLog.Infof("[InsertProof]: inserted proof at "+ + "(universeID=%x, leafKey=%x)", universeID.String(), + leafKey.UniverseKey()) + return r.marshalIssuanceProof(ctx, req.Key, newUniverseState) } diff --git a/universe/base.go b/universe/base.go index dd81201ad..0cba2cb8b 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/proof" ) @@ -184,11 +186,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 := newProof.Verify(ctx, nil, a.cfg.HeaderVerifier) + + // 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 := newProof.Verify( + ctx, prevAssetSnapshot, a.cfg.HeaderVerifier, + ) if err != nil { return nil, fmt.Errorf("unable to verify proof: %v", err) } @@ -245,6 +257,66 @@ func (a *MintingArchive) RegisterIssuance(ctx context.Context, id Identifier, return issuanceProof, nil } +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 := newProof.Asset.PrimaryPrevID() + + // Parse script key for previous asset. + prevScriptKeyBytes := prevID.ScriptKey + prevScriptKeyPubKey, err := btcec.ParsePubKey( + prevScriptKeyBytes[:], + ) + 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) + } + prevProofFileBytes := prevProofs[0].Leaf.GenesisProof + + // Parse proof file bytes. + var prevProofFile proof.File + if err := prevProofFile.Decode( + bytes.NewReader(prevProofFileBytes), + ); err != nil { + return nil, err + } + + prevProof, err := prevProofFile.LastProof() + if err != nil { + return nil, err + } + + // 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,