From f4f53630d56350a4226bd79e9340341511cc957d Mon Sep 17 00:00:00 2001
From: woodser <13068859+woodser@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:08:37 -0400
Subject: [PATCH] automatically cancel offers with duplicate key images

---
 .../haveno/core/api/CoreOffersService.java    |  1 +
 .../haveno/core/offer/OpenOfferManager.java   | 24 +++++++++++++++++++
 2 files changed, 25 insertions(+)

diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java
index f036fb13ea..f9c450b825 100644
--- a/core/src/main/java/haveno/core/api/CoreOffersService.java
+++ b/core/src/main/java/haveno/core/api/CoreOffersService.java
@@ -265,6 +265,7 @@ public class CoreOffersService {
                 if (!seenKeyImages.add(keyImage)) {
                     for (Offer offer2 : offers) {
                         if (offer == offer2) continue;
+                        if (offer2.getOfferPayload().getReserveTxKeyImages() == null) continue;
                         if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
                             log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
                             duplicateFundedOffers.add(offer2);
diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
index 32a1507f77..81d2b2b821 100644
--- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java
+++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
@@ -97,6 +97,7 @@ import haveno.network.p2p.peers.PeerManager;
 import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -888,6 +889,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
             List<String> errorMessages = new ArrayList<String>();
             synchronized (processOffersLock) {
                 List<OpenOffer> openOffers = getOpenOffers();
+                removeOffersWithDuplicateKeyImages(openOffers);
                 for (OpenOffer pendingOffer : openOffers) {
                     if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
                     if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts
@@ -919,6 +921,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
         }, THREAD_ID);
     }
 
+    private void removeOffersWithDuplicateKeyImages(List<OpenOffer> openOffers) {
+
+        // collect offers with duplicate key images
+        Set<String> keyImages = new HashSet<>();
+        Set<OpenOffer> offersToRemove = new HashSet<>();
+        for (OpenOffer openOffer : openOffers) {
+            if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue;
+            if (Collections.disjoint(keyImages, openOffer.getOffer().getOfferPayload().getReserveTxKeyImages())) {
+                keyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
+            } else {
+                offersToRemove.add(openOffer);
+            }
+        }
+
+        // remove offers with duplicate key images
+        for (OpenOffer offerToRemove : offersToRemove) {
+            log.warn("Removing open offer which has duplicate key images with other open offers: {}", offerToRemove.getId());
+            doCancelOffer(offerToRemove);
+            openOffers.remove(offerToRemove);
+        }
+    }
+
     private void processPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
         
         // skip if already processing