diff --git a/tapgarden/custodian.go b/tapgarden/custodian.go index fc16cec0c..4af79b0ad 100644 --- a/tapgarden/custodian.go +++ b/tapgarden/custodian.go @@ -1,7 +1,6 @@ package tapgarden import ( - "bytes" "errors" "fmt" "strings" @@ -617,6 +616,7 @@ func (c *Custodian) checkProofAvailable(event *address.Event) error { AssetID: fn.Ptr(event.Addr.AssetID), GroupKey: event.Addr.GroupKey, ScriptKey: event.Addr.ScriptKey, + OutPoint: &event.Outpoint, }) switch { case errors.Is(err, proof.ErrProofNotFound): @@ -626,14 +626,23 @@ func (c *Custodian) checkProofAvailable(event *address.Event) error { return fmt.Errorf("error fetching proof for event: %w", err) } - file := proof.NewEmptyFile(proof.V0) - if err := file.Decode(bytes.NewReader(blob)); err != nil { - return fmt.Errorf("error decoding proof file: %w", err) + // At this point, we expect the proof to be a full file, containing the + // whole provenance chain (as required by implementers of the + // proof.NotifyArchiver.FetchProof() method). So if we don't we can't + // continue. + if !blob.IsFile() { + return fmt.Errorf("expected proof to be a full file, but got " + + "something else") + } + + file, err := blob.AsFile() + if err != nil { + return fmt.Errorf("error extracting proof file: %w", err) } // Exit early on empty proof (shouldn't happen outside of test cases). if file.IsEmpty() { - return fmt.Errorf("archive contained empty proof file: %w", err) + return fmt.Errorf("archive contained empty proof file") } lastProof, err := file.LastProof() @@ -654,9 +663,91 @@ func (c *Custodian) checkProofAvailable(event *address.Event) error { // and pending address event. If a proof successfully matches the desired state // of the address, that completes the inbound transfer of an asset. func (c *Custodian) mapProofToEvent(p proof.Blob) error { - file := proof.NewEmptyFile(proof.V0) - if err := file.Decode(bytes.NewReader(p)); err != nil { - return fmt.Errorf("error decoding proof file: %w", err) + // We arrive here if we are notified about a new proof. The notification + // interface allows that proof to be a single transition proof. So if + // we don't have a full file yet, we need to fetch it now. The + // proof.NotifyArchiver.FetchProof() method will return the full file as + // per its Godoc. + var ( + proofBlob = p + lastProof *proof.Proof + err error + ) + if !p.IsFile() { + log.Debugf("Received single proof, inspecting if matches event") + lastProof, err = p.AsSingleProof() + if err != nil { + return fmt.Errorf("error decoding proof: %w", err) + } + + // Before we go ahead and fetch the full file, let's make sure + // we are actually interested in this proof. + matchesEvent := false + for _, event := range c.events { + if AddrMatchesAsset(event.Addr, &lastProof.Asset) && + event.Outpoint == lastProof.OutPoint() { + + matchesEvent = true + } + } + if !matchesEvent { + log.Debugf("Proof doesn't match any events, skipping.") + return nil + } + + ctxt, cancel := c.WithCtxQuit() + defer cancel() + + loc := proof.Locator{ + AssetID: fn.Ptr(lastProof.Asset.ID()), + ScriptKey: *lastProof.Asset.ScriptKey.PubKey, + OutPoint: fn.Ptr(lastProof.OutPoint()), + } + if lastProof.Asset.GroupKey != nil { + loc.GroupKey = &lastProof.Asset.GroupKey.GroupPubKey + } + + log.Debugf("Received single proof, fetching full file") + proofBlob, err = c.cfg.ProofNotifier.FetchProof(ctxt, loc) + if err != nil { + return fmt.Errorf("error fetching full proof file for "+ + "event: %w", err) + } + + // Do we already have this proof in our main archive? This + // should only be false if we got the notification from our + // local universe instead of the local proof archive (which the + // couriers use). + haveProof, err := c.cfg.ProofArchive.HasProof(ctxt, loc) + if err != nil { + return fmt.Errorf("error checking if proof is "+ + "available: %w", err) + } + + // We don't have the proof yet, or not in all backends, so we + // need to import it now. + if !haveProof { + headerVerifier := GenHeaderVerifier( + ctxt, c.cfg.ChainBridge, + ) + err = c.cfg.ProofArchive.ImportProofs( + ctxt, headerVerifier, c.cfg.GroupVerifier, + false, &proof.AnnotatedProof{ + Locator: loc, + Blob: proofBlob, + }, + ) + if err != nil { + return fmt.Errorf("error importing proof "+ + "file into main archive: %w", err) + } + } + } + + // Now we can be sure we have a file. + file, err := proofBlob.AsFile() + if err != nil { + return fmt.Errorf("error extracting proof file: %w", err) } // Exit early on empty proof (shouldn't happen outside of test cases). @@ -667,13 +758,18 @@ func (c *Custodian) mapProofToEvent(p proof.Blob) error { // We got the proof from the multi archiver, which verifies it before // giving it to us. So we don't have to verify them again and can - // directly look at the last state. - lastProof, err := file.LastProof() - if err != nil { - return fmt.Errorf("error fetching last proof: %w", err) + // directly look at the last state. We can skip extracting the last + // proof if we started out with a single proof in the first place, which + // we already parsed above. + if lastProof == nil { + lastProof, err = file.LastProof() + if err != nil { + return fmt.Errorf("error fetching last proof: %w", err) + } } - log.Infof("Received new proof file, version=%d, num_proofs=%d", - file.Version, file.NumProofs()) + log.Infof("Received new proof file for asset ID %s, version=%d,"+ + "num_proofs=%d", lastProof.Asset.ID().String(), file.Version, + file.NumProofs()) // Check if any of our in-flight events match the last proof's state. for _, event := range c.events {