arbitrator deletes signed offer when key image confirmed spent

This commit is contained in:
woodser 2023-02-06 16:41:36 -05:00
parent 60dc4901e4
commit 5feb487039
4 changed files with 112 additions and 30 deletions

View file

@ -17,6 +17,8 @@ import monero.daemon.model.MoneroKeyImageSpentStatus;
/** /**
* Poll for changes to the spent status of key images. * Poll for changes to the spent status of key images.
*
* TODO: move to monero-java?
*/ */
@Slf4j @Slf4j
public class MoneroKeyImagePoller { public class MoneroKeyImagePoller {
@ -240,7 +242,7 @@ public class MoneroKeyImagePoller {
setIsPolling(keyImages.size() > 0 && listeners.size() > 0); setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
} }
private void setIsPolling(boolean enabled) { private synchronized void setIsPolling(boolean enabled) {
if (enabled) { if (enabled) {
if (!isPolling) { if (!isPolling) {
isPolling = true; // TODO monero-java: looper.isPolling() isPolling = true; // TODO monero-java: looper.isPolling()

View file

@ -69,6 +69,7 @@ public class OfferBookService {
private final JsonFileManager jsonFileManager; private final JsonFileManager jsonFileManager;
private final CoreMoneroConnectionsService connectionsService; private final CoreMoneroConnectionsService connectionsService;
// poll key images of offers
private MoneroKeyImagePoller keyImagePoller; private MoneroKeyImagePoller keyImagePoller;
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
@ -96,7 +97,7 @@ public class OfferBookService {
this.connectionsService = connectionsService; this.connectionsService = connectionsService;
jsonFileManager = new JsonFileManager(storageDir); jsonFileManager = new JsonFileManager(storageDir);
// listen for monero connection changes // listen for connection changes to monerod
connectionsService.addListener(new MoneroConnectionManagerListener() { connectionsService.addListener(new MoneroConnectionManagerListener() {
@Override @Override
public void onConnectionChanged(MoneroRpcConnection connection) { public void onConnectionChanged(MoneroRpcConnection connection) {
@ -255,25 +256,25 @@ public class OfferBookService {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void maybeInitializeKeyImagePoller() { private synchronized void maybeInitializeKeyImagePoller() {
synchronized (this) { if (keyImagePoller != null) return;
if (keyImagePoller != null) return; keyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs());
keyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs());
keyImagePoller.addListener(new MoneroKeyImageListener() { // handle when key images spent
@Override keyImagePoller.addListener(new MoneroKeyImageListener() {
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) { @Override
for (String keyImage : spentStatuses.keySet()) { public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
updateAffectedOffers(keyImage); for (String keyImage : spentStatuses.keySet()) {
} updateAffectedOffers(keyImage);
} }
}); }
});
// first poll after 5s
new Thread(() -> { // first poll after 5s
GenUtils.waitFor(5000); new Thread(() -> {
keyImagePoller.poll(); GenUtils.waitFor(5000);
}); keyImagePoller.poll();
} });
} }
private long getKeyImageRefreshPeriodMs() { private long getKeyImageRefreshPeriodMs() {

View file

@ -21,6 +21,8 @@ import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.api.CoreContext; import bisq.core.api.CoreContext;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.MoneroKeyImageListener;
import bisq.core.btc.wallet.MoneroKeyImagePoller;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.exceptions.TradePriceOutOfToleranceException; import bisq.core.exceptions.TradePriceOutOfToleranceException;
@ -56,6 +58,7 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.SendDirectMessageListener;
import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager; import bisq.network.p2p.peers.PeerManager;
import common.utils.GenUtils;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Capabilities; import bisq.common.app.Capabilities;
@ -84,6 +87,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -92,6 +96,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import lombok.Getter; import lombok.Getter;
import monero.common.MoneroConnectionManagerListener;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -114,7 +121,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private final KeyRing keyRing; private final KeyRing keyRing;
private final User user; private final User user;
private final P2PService p2PService; private final P2PService p2PService;
private final CoreMoneroConnectionsService connectionService; private final CoreMoneroConnectionsService connectionsService;
private final BtcWalletService btcWalletService; private final BtcWalletService btcWalletService;
private final XmrWalletService xmrWalletService; private final XmrWalletService xmrWalletService;
private final TradeWalletService tradeWalletService; private final TradeWalletService tradeWalletService;
@ -141,6 +148,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
@Getter @Getter
private final AccountAgeWitnessService accountAgeWitnessService; private final AccountAgeWitnessService accountAgeWitnessService;
// poll key images of signed offers
private MoneroKeyImagePoller signedOfferKeyImagePoller;
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, Initialization // Constructor, Initialization
@ -151,7 +163,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
KeyRing keyRing, KeyRing keyRing,
User user, User user,
P2PService p2PService, P2PService p2PService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionsService,
BtcWalletService btcWalletService, BtcWalletService btcWalletService,
XmrWalletService xmrWalletService, XmrWalletService xmrWalletService,
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
@ -171,7 +183,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.keyRing = keyRing; this.keyRing = keyRing;
this.user = user; this.user = user;
this.p2PService = p2PService; this.p2PService = p2PService;
this.connectionService = connectionService; this.connectionsService = connectionsService;
this.btcWalletService = btcWalletService; this.btcWalletService = btcWalletService;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
this.tradeWalletService = tradeWalletService; this.tradeWalletService = tradeWalletService;
@ -191,6 +203,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE);
this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers
// listen for connection changes to monerod
connectionsService.addListener(new MoneroConnectionManagerListener() {
@Override
public void onConnectionChanged(MoneroRpcConnection connection) {
maybeInitializeKeyImagePoller();
signedOfferKeyImagePoller.setDaemon(connectionsService.getDaemon());
signedOfferKeyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
}
});
// remove open offer if reserved funds spent // remove open offer if reserved funds spent
offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() { offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() {
@Override @Override
@ -214,7 +236,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
persistenceManager.readPersisted(persisted -> { persistenceManager.readPersisted(persisted -> {
openOffers.setAll(persisted.getList()); openOffers.setAll(persisted.getList());
openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService));
// read signed offers // read signed offers
signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> {
signedOffers.setAll(signedOfferPersisted.getList()); signedOffers.setAll(signedOfferPersisted.getList());
@ -225,6 +247,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
completeHandler); completeHandler);
} }
private synchronized void maybeInitializeKeyImagePoller() {
if (signedOfferKeyImagePoller != null) return;
signedOfferKeyImagePoller = new MoneroKeyImagePoller(connectionsService.getDaemon(), getKeyImageRefreshPeriodMs());
// handle when key images confirmed spent
signedOfferKeyImagePoller.addListener(new MoneroKeyImageListener() {
@Override
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
for (Entry<String, MoneroKeyImageSpentStatus> entry : spentStatuses.entrySet()) {
if (entry.getValue() == MoneroKeyImageSpentStatus.CONFIRMED) {
removeSignedOffers(entry.getKey());
}
}
}
});
// first poll in 5s
new Thread(() -> {
GenUtils.waitFor(5000);
signedOfferKeyImagePoller.poll();
});
}
private long getKeyImageRefreshPeriodMs() {
return connectionsService.isConnectionLocal() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
p2PService.addDecryptedDirectMessageListener(this); p2PService.addDecryptedDirectMessageListener(this);
@ -264,6 +313,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
lastUnlockedBalance = newUnlockedBalance; lastUnlockedBalance = newUnlockedBalance;
} }
}); });
// initialize key image poller for signed offers
maybeInitializeKeyImagePoller();
// poll spent status of key images
for (SignedOffer signedOffer : signedOffers.getList()) {
signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages());
}
} }
private void cleanUpAddressEntries() { private void cleanUpAddressEntries() {
@ -672,9 +729,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
private void addSignedOffer(SignedOffer openOffer) { private void addSignedOffer(SignedOffer signedOffer) {
log.info("Adding SignedOffer offer for offer {}", signedOffer.getOfferId());
synchronized (signedOffers) { synchronized (signedOffers) {
signedOffers.add(openOffer); signedOffers.add(signedOffer);
signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages());
}
}
private void removeSignedOffer(SignedOffer signedOffer) {
log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId());
synchronized (signedOffers) {
signedOffers.remove(signedOffer);
signedOfferKeyImagePoller.removeKeyImages(signedOffer.getReserveTxKeyImages());
}
}
private void removeSignedOffers(String keyImage) {
for (SignedOffer signedOffer : new ArrayList<SignedOffer>(signedOffers.getList())) {
if (signedOffer.getReserveTxKeyImages().contains(keyImage)) {
removeSignedOffer(signedOffer);
}
} }
} }
@ -1008,7 +1083,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
// Don't allow trade start if Monero node is not fully synced // Don't allow trade start if Monero node is not fully synced
if (!connectionService.isSyncedWithinTolerance()) { if (!connectionsService.isSyncedWithinTolerance()) {
errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced."; errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced.";
log.info(errorMessage); log.info(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);

View file

@ -1,6 +1,7 @@
package bisq.core.offer; package bisq.core.offer;
import bisq.core.api.CoreContext; import bisq.core.api.CoreContext;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.trade.TradableList; import bisq.core.trade.TradableList;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
@ -52,6 +53,7 @@ public class OpenOfferManagerTest {
public void testStartEditOfferForActiveOffer() { public void testStartEditOfferForActiveOffer() {
P2PService p2PService = mock(P2PService.class); P2PService p2PService = mock(P2PService.class);
OfferBookService offerBookService = mock(OfferBookService.class); OfferBookService offerBookService = mock(OfferBookService.class);
CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class);
when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class));
@ -59,7 +61,7 @@ public class OpenOfferManagerTest {
null, null,
null, null,
p2PService, p2PService,
null, connectionsService,
null, null,
null, null,
null, null,
@ -100,13 +102,14 @@ public class OpenOfferManagerTest {
public void testStartEditOfferForDeactivatedOffer() { public void testStartEditOfferForDeactivatedOffer() {
P2PService p2PService = mock(P2PService.class); P2PService p2PService = mock(P2PService.class);
OfferBookService offerBookService = mock(OfferBookService.class); OfferBookService offerBookService = mock(OfferBookService.class);
CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class);
when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class));
final OpenOfferManager manager = new OpenOfferManager(coreContext, final OpenOfferManager manager = new OpenOfferManager(coreContext,
null, null,
null, null,
p2PService, p2PService,
null, connectionsService,
null, null,
null, null,
null, null,
@ -139,6 +142,7 @@ public class OpenOfferManagerTest {
public void testStartEditOfferForOfferThatIsCurrentlyEdited() { public void testStartEditOfferForOfferThatIsCurrentlyEdited() {
P2PService p2PService = mock(P2PService.class); P2PService p2PService = mock(P2PService.class);
OfferBookService offerBookService = mock(OfferBookService.class); OfferBookService offerBookService = mock(OfferBookService.class);
CoreMoneroConnectionsService connectionsService = mock(CoreMoneroConnectionsService.class);
when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class));
@ -147,7 +151,7 @@ public class OpenOfferManagerTest {
null, null,
null, null,
p2PService, p2PService,
null, connectionsService,
null, null,
null, null,
null, null,