Update to v1.0.10

Update to v1.0.10
This commit is contained in:
retoaccess1 2024-08-11 15:27:39 +02:00 committed by GitHub
commit f2c9cf05eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1701 additions and 870 deletions

View File

@ -26,7 +26,7 @@ See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
## Status of the project ## Status of the project
Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time, but they can be found online. Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time.
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network. A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network.

View File

@ -317,9 +317,6 @@ configure(project(':common')) {
exclude(module: 'animal-sniffer-annotations') exclude(module: 'animal-sniffer-annotations')
} }
// override transitive dependency version from 1.5 to the same version just identified by commit number.
// Remove this if transitive dependency is changed to something else than 1.5
implementation(group: 'com.github.JesusMcCloud', name: 'jtorctl') { version { strictly "[9b5ba2036b]" } }
implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-base:$javafxVersion:$os"
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
} }
@ -610,7 +607,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle' apply from: 'package/package.gradle'
version = '1.0.9-SNAPSHOT' version = '1.0.10-SNAPSHOT'
jar.manifest.attributes( jar.manifest.attributes(
"Implementation-Title": project.name, "Implementation-Title": project.name,

View File

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version { public class Version {
// The application versions // The application versions
// We use semantic versioning with major, minor and patch // We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.9"; public static final String VERSION = "1.0.10";
/** /**
* Holds a list of the tagged resource files for optimizing the getData requests. * Holds a list of the tagged resource files for optimizing the getData requests.

View File

@ -77,6 +77,7 @@ public class Config {
public static final String SEED_NODES = "seedNodes"; public static final String SEED_NODES = "seedNodes";
public static final String BAN_LIST = "banList"; public static final String BAN_LIST = "banList";
public static final String NODE_PORT = "nodePort"; public static final String NODE_PORT = "nodePort";
public static final String HIDDEN_SERVICE_ADDRESS = "hiddenServiceAddress";
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P"; public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
public static final String MAX_CONNECTIONS = "maxConnections"; public static final String MAX_CONNECTIONS = "maxConnections";
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress"; public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
@ -151,6 +152,7 @@ public class Config {
public final File appDataDir; public final File appDataDir;
public final int walletRpcBindPort; public final int walletRpcBindPort;
public final int nodePort; public final int nodePort;
public final String hiddenServiceAddress;
public final int maxMemory; public final int maxMemory;
public final String logLevel; public final String logLevel;
public final List<String> bannedXmrNodes; public final List<String> bannedXmrNodes;
@ -286,6 +288,12 @@ public class Config {
.ofType(Integer.class) .ofType(Integer.class)
.defaultsTo(9999); .defaultsTo(9999);
ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt = ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on") parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
.withRequiredArg() .withRequiredArg()
@ -670,6 +678,7 @@ public class Config {
this.helpRequested = options.has(helpOpt); this.helpRequested = options.has(helpOpt);
this.configFile = configFile; this.configFile = configFile;
this.nodePort = options.valueOf(nodePortOpt); this.nodePort = options.valueOf(nodePortOpt);
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt); this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
this.maxMemory = options.valueOf(maxMemoryOpt); this.maxMemory = options.valueOf(maxMemoryOpt);
this.logLevel = options.valueOf(logLevelOpt); this.logLevel = options.valueOf(logLevelOpt);

View File

@ -112,13 +112,13 @@ public class CoreDisputesService {
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler); disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
tradeManager.requestPersistence(); tradeManager.requestPersistence();
}, trade.getId()); }, trade.getId());
} }
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) { public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
synchronized (trade) { synchronized (trade.getLock()) {
byte[] payoutTxSerialized = null; byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null; String payoutTxHashAsString = null;
@ -163,7 +163,7 @@ public class CoreDisputesService {
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (trade) { synchronized (trade.getLock()) {
try { try {
// create dispute result // create dispute result
@ -275,11 +275,13 @@ public class CoreDisputesService {
disputeResult.summaryNotesProperty().get() disputeResult.summaryNotesProperty().get()
); );
synchronized (dispute.getChatMessages()) {
if (reason == DisputeResult.Reason.OPTION_TRADE && if (reason == DisputeResult.Reason.OPTION_TRADE &&
dispute.getChatMessages().size() > 1 && dispute.getChatMessages().size() > 1 &&
dispute.getChatMessages().get(1).isSystemMessage()) { dispute.getChatMessages().get(1).isSystemMessage()) {
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n"; textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
} }
}
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);

View File

@ -178,7 +178,7 @@ class CoreWalletsService {
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked(); verifyEncryptedWalletIsUnlocked();
try { try {
return xmrWalletService.getWallet().relayTx(metadata); return xmrWalletService.relayTx(metadata);
} catch (Exception ex) { } catch (Exception ex) {
log.error("", ex); log.error("", ex);
throw new IllegalStateException(ex); throw new IllegalStateException(ex);

View File

@ -101,6 +101,7 @@ public final class XmrConnectionService {
private Long lastLogPollErrorTimestamp; private Long lastLogPollErrorTimestamp;
private Long syncStartHeight = null; private Long syncStartHeight = null;
private TaskLooper daemonPollLooper; private TaskLooper daemonPollLooper;
private long lastRefreshPeriodMs = 0;
@Getter @Getter
private boolean isShutDownStarted; private boolean isShutDownStarted;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>(); private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
@ -273,7 +274,11 @@ public final class XmrConnectionService {
} }
public synchronized boolean requestSwitchToNextBestConnection() { public synchronized boolean requestSwitchToNextBestConnection() {
log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); return requestSwitchToNextBestConnection(null);
}
public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
log.warn("Requesting switch to next best monerod, source monerod={}", sourceConnection == null ? getConnection() == null ? null : getConnection().getUri() : sourceConnection.getUri());
// skip if shut down started // skip if shut down started
if (isShutDownStarted) { if (isShutDownStarted) {
@ -281,9 +286,15 @@ public final class XmrConnectionService {
return false; return false;
} }
// skip if connection is already switched
if (sourceConnection != null && sourceConnection != getConnection()) {
log.warn("Skipping switch to next best Monero connection because source connection is not current connection");
return false;
}
// skip if connection is fixed // skip if connection is fixed
if (isFixedConnection() || !connectionManager.getAutoSwitch()) { if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled");
return false; return false;
} }
@ -343,7 +354,11 @@ public final class XmrConnectionService {
} }
public long getRefreshPeriodMs() { public long getRefreshPeriodMs() {
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(); return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(false);
}
private long getInternalRefreshPeriodMs() {
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(true);
} }
public void verifyConnection() { public void verifyConnection() {
@ -413,12 +428,16 @@ public final class XmrConnectionService {
return connection != null && HavenoUtils.isLocalHost(connection.getUri()); return connection != null && HavenoUtils.isLocalHost(connection.getUri());
} }
private long getDefaultRefreshPeriodMs() { private long getDefaultRefreshPeriodMs(boolean internal) {
MoneroRpcConnection connection = getConnection(); MoneroRpcConnection connection = getConnection();
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
if (isConnectionLocalHost(connection)) { if (isConnectionLocalHost(connection)) {
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
else return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing if (lastInfo != null && (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight())) {
return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
} else {
return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
}
} else if (isProxyApplied(connection)) { } else if (isProxyApplied(connection)) {
return REFRESH_PERIOD_ONION_MS; return REFRESH_PERIOD_ONION_MS;
} else { } else {
@ -638,7 +657,7 @@ public final class XmrConnectionService {
// update polling // update polling
doPollDaemon(); doPollDaemon();
if (currentConnection != getConnection()) return; // polling can change connection if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
// notify listeners in parallel // notify listeners in parallel
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected); log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
@ -658,7 +677,7 @@ public final class XmrConnectionService {
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (daemonPollLooper != null) daemonPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemon()); daemonPollLooper = new TaskLooper(() -> pollDaemon());
daemonPollLooper.start(getRefreshPeriodMs()); daemonPollLooper.start(getInternalRefreshPeriodMs());
} }
} }
@ -715,6 +734,13 @@ public final class XmrConnectionService {
// connected to daemon // connected to daemon
isConnected = true; isConnected = true;
// announce connection change if refresh period changes
if (getRefreshPeriodMs() != lastRefreshPeriodMs) {
lastRefreshPeriodMs = getRefreshPeriodMs();
onConnectionChanged(getConnection()); // causes new poll
return;
}
// update properties on user thread // update properties on user thread
UserThread.execute(() -> { UserThread.execute(() -> {

View File

@ -133,6 +133,22 @@ public class WalletAppSetup {
String result; String result;
if (exception == null && errorMsg == null) { if (exception == null && errorMsg == null) {
// update daemon sync progress
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
if (chainDownloadPercentageD < 1) {
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
if (chainDownloadPercentageD > 0.0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
} else {
result = Res.get("mainView.footer.xmrInfo",
Res.get("mainView.footer.xmrInfo.connectingTo"),
getXmrDaemonNetworkAsString());
}
} else {
// update wallet sync progress // update wallet sync progress
double walletDownloadPercentageD = (double) walletDownloadPercentage; double walletDownloadPercentageD = (double) walletDownloadPercentage;
xmrWalletSyncProgress.set(walletDownloadPercentageD); xmrWalletSyncProgress.set(walletDownloadPercentageD);
@ -144,29 +160,15 @@ public class WalletAppSetup {
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
getXmrSplashSyncIconId().set("image-connection-synced"); getXmrSplashSyncIconId().set("image-connection-synced");
downloadCompleteHandler.run(); downloadCompleteHandler.run();
} else if (walletDownloadPercentageD > 0) { } else if (walletDownloadPercentageD >= 0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD)); String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
getXmrSplashSyncIconId().set(""); // clear synced icon getXmrSplashSyncIconId().set(""); // clear synced icon
} else { } else {
// update daemon sync progress
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
if (chainDownloadPercentageD == 1) {
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString); String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
getXmrSplashSyncIconId().set("image-connection-synced"); getXmrSplashSyncIconId().set("image-connection-synced");
} else if (chainDownloadPercentageD > 0.0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
} else {
result = Res.get("mainView.footer.xmrInfo",
Res.get("mainView.footer.xmrInfo.connectingTo"),
getXmrDaemonNetworkAsString());
} }
} }
} else { } else {

View File

@ -114,6 +114,7 @@ public class DisputeMsgEvents {
// We check at every new message if it might be a message sent after the dispute had been closed. If that is the // We check at every new message if it might be a message sent after the dispute had been closed. If that is the
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute // case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
// message arrived. // message arrived.
synchronized (dispute.getChatMessages()) {
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages(); ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
// If last message is not a result message we re-open as we might have received a new message from the // If last message is not a result message we re-open as we might have received a new message from the
// trader/mediator/arbitrator who has reopened the case // trader/mediator/arbitrator who has reopened the case
@ -127,3 +128,4 @@ public class DisputeMsgEvents {
} }
} }
} }
}

View File

@ -28,13 +28,10 @@ import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import javafx.collections.SetChangeListener; import javafx.collections.SetChangeListener;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -218,7 +215,7 @@ public class OfferFilterService {
return result; return result;
} }
public boolean hasValidSignature(Offer offer) { private boolean hasValidSignature(Offer offer) {
// get accepted arbitrator by address // get accepted arbitrator by address
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
@ -230,9 +227,11 @@ public class OfferFilterService {
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address? if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
} else { } else {
// otherwise log warning that arbitrator is unregistered // // otherwise log warning that arbitrator is unregistered
List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()); // List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses); // if (!arbitratorAddresses.isEmpty()) {
// log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
// }
} }
} }

View File

@ -390,6 +390,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedBankIdsList()); null : new ArrayList<>(proto.getAcceptedBankIdsList());
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
null : new ArrayList<>(proto.getAcceptedCountryCodesList()); null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap(); null : proto.getExtraDataMap();
@ -431,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getProtocolVersion(), proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList())); reserveTxKeyImages);
} }
@Override @Override

View File

@ -110,6 +110,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroIncomingTransfer;
@ -688,7 +689,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
addOpenOffer(editedOpenOffer); addOpenOffer(editedOpenOffer);
if (editedOpenOffer.isAvailable()) if (editedOpenOffer.isAvailable())
republishOffer(editedOpenOffer); maybeRepublishOffer(editedOpenOffer);
offersToBeEdited.remove(openOffer.getId()); offersToBeEdited.remove(openOffer.getId());
requestPersistence(); requestPersistence();
@ -863,8 +864,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (processOffersLock) {
List<String> errorMessages = new ArrayList<String>(); List<String> errorMessages = new ArrayList<String>();
synchronized (processOffersLock) {
List<OpenOffer> openOffers = getOpenOffers(); List<OpenOffer> openOffers = getOpenOffers();
for (OpenOffer pendingOffer : openOffers) { for (OpenOffer pendingOffer : openOffers) {
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
@ -887,13 +888,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}); });
HavenoUtils.awaitLatch(latch); HavenoUtils.awaitLatch(latch);
} }
}
requestPersistence(); requestPersistence();
if (errorMessages.isEmpty()) { if (errorMessages.isEmpty()) {
if (resultHandler != null) resultHandler.handleResult(null); if (resultHandler != null) resultHandler.handleResult(null);
} else { } else {
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString()); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
} }
}
}, THREAD_ID); }, THREAD_ID);
} }
@ -962,9 +963,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} else { } else {
// handle sufficient balance // sign and post offer if enough funds
boolean hasFundsReserved = openOffer.getReserveTxHash() != null;
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0; boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
if (hasSufficientBalance) { if (hasFundsReserved || hasSufficientBalance) {
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
return; return;
} else if (openOffer.getScheduledTxHashes() == null) { } else if (openOffer.getScheduledTxHashes() == null) {
@ -1083,11 +1085,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded(); BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded();
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s) xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
MoneroTxWallet splitOutputTx = null; MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
@ -1100,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
xmrWalletService.handleWalletError(e, sourceConnection);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1714,11 +1717,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
synchronized (openOffers) { synchronized (openOffers) {
contained = openOffers.contains(openOffer); contained = openOffers.contains(openOffer);
} }
if (contained && openOffer.isAvailable()) { if (contained) {
// TODO It is not clear yet if it is better for the node and the network to send out all add offer // TODO It is not clear yet if it is better for the node and the network to send out all add offer
// messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have // messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have
// some significant impact to user experience and the network // some significant impact to user experience and the network
republishOffer(openOffer, () -> processListForRepublishOffers(list)); maybeRepublishOffer(openOffer, () -> processListForRepublishOffers(list));
/* republishOffer(openOffer, /* republishOffer(openOffer,
() -> UserThread.runAfter(() -> processListForRepublishOffers(list), () -> UserThread.runAfter(() -> processListForRepublishOffers(list),
@ -1730,13 +1733,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
private void republishOffer(OpenOffer openOffer) { private void maybeRepublishOffer(OpenOffer openOffer) {
republishOffer(openOffer, null); maybeRepublishOffer(openOffer, null);
} }
private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
// skip if prevented from publishing
if (preventedFromPublishing(openOffer)) {
if (completeHandler != null) completeHandler.run();
return;
}
// determine if offer is valid // determine if offer is valid
boolean isValid = true; boolean isValid = true;
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner());
@ -1747,7 +1756,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId());
isValid = false; isValid = false;
} }
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) {
log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId());
isValid = false; isValid = false;
} }
@ -1811,6 +1820,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID); }, THREAD_ID);
} }
private boolean preventedFromPublishing(OpenOffer openOffer) {
return openOffer.isDeactivated() || openOffer.isCanceled();
}
private void startPeriodicRepublishOffersTimer() { private void startPeriodicRepublishOffersTimer() {
stopped = false; stopped = false;
if (periodicRepublishOffersTimer == null) { if (periodicRepublishOffersTimer == null) {
@ -1843,8 +1856,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
final OpenOffer openOffer = openOffersList.get(i); final OpenOffer openOffer = openOffersList.get(i);
UserThread.runAfterRandomDelay(() -> { UserThread.runAfterRandomDelay(() -> {
// we need to check if in the meantime the offer has been removed // we need to check if in the meantime the offer has been removed
if (openOffers.contains(openOffer) && openOffer.isAvailable()) boolean contained = false;
refreshOffer(openOffer, 0, 1); synchronized (openOffers) {
contained = openOffers.contains(openOffer);
}
if (contained) maybeRefreshOffer(openOffer, 0, 1);
}, minDelay, maxDelay, TimeUnit.MILLISECONDS); }, minDelay, maxDelay, TimeUnit.MILLISECONDS);
} }
} else { } else {
@ -1857,13 +1873,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.trace("periodicRefreshOffersTimer already stated"); log.trace("periodicRefreshOffersTimer already stated");
} }
private void refreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) { private void maybeRefreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) {
if (preventedFromPublishing(openOffer)) return;
offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(), offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(),
() -> log.debug("Successful refreshed TTL for offer"), () -> log.debug("Successful refreshed TTL for offer"),
(errorMessage) -> { (errorMessage) -> {
log.warn(errorMessage); log.warn(errorMessage);
if (numAttempts + 1 < maxAttempts) { if (numAttempts + 1 < maxAttempts) {
UserThread.runAfter(() -> refreshOffer(openOffer, numAttempts + 1, maxAttempts), 10); UserThread.runAfter(() -> maybeRefreshOffer(openOffer, numAttempts + 1, maxAttempts), 10);
} }
}); });
} }

View File

@ -30,8 +30,8 @@ import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -59,11 +59,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
// verify monero connection // verify monero connection
model.getXmrWalletService().getConnectionService().verifyConnection(); model.getXmrWalletService().getXmrConnectionService().verifyConnection();
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = null; MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// reset protocol timeout // reset protocol timeout
verifyPending(); verifyPending();
@ -82,14 +82,16 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = model.getXmrWalletService().getXmrConnectionService().getConnection();
try { try {
//if (true) throw new RuntimeException("Pretend error"); //if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage());
model.getXmrWalletService().handleWalletError(e, sourceConnection);
verifyPending();
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout model.getProtocol().startTimeoutTimer(); // reset protocol timeout
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
@ -102,11 +104,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
// reset state with wallet lock // reset state with wallet lock
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
if (reserveTx != null) { if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
offer.getOfferPayload().setReserveTxKeyImages(null); offer.getOfferPayload().setReserveTxKeyImages(null);
}
throw e; throw e;
} }
@ -132,7 +131,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
} }
public void verifyPending() { private boolean isPending() {
if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); return model.getOpenOffer().isPending();
}
private void verifyPending() {
if (!isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
} }
} }

View File

@ -186,24 +186,47 @@ public abstract class SupportManager {
private void onAckMessage(AckMessage ackMessage) { private void onAckMessage(AckMessage ackMessage) {
if (ackMessage.getSourceType() == getAckMessageSourceType()) { if (ackMessage.getSourceType() == getAckMessageSourceType()) {
if (ackMessage.isSuccess()) { if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {} with tradeId {} and uid {}", log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
// ack message on chat message received when dispute is opened and closed // ack message on chat message received when dispute is opened and closed
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) { if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
Trade trade = tradeManager.getTrade(ackMessage.getSourceId()); Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
for (Dispute dispute : trade.getDisputes()) { for (Dispute dispute : trade.getDisputes()) {
synchronized (dispute.getChatMessages()) {
for (ChatMessage chatMessage : dispute.getChatMessages()) { for (ChatMessage chatMessage : dispute.getChatMessages()) {
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) { if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
if (dispute.isClosed()) trade.pollWalletNormallyForMs(30000); // sync to check for payout if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) {
else trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); if (dispute.isClosed()) dispute.reOpen();
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
} else if (dispute.isClosed()) {
trade.pollWalletNormallyForMs(30000); // sync to check for payout
}
}
} }
} }
} }
} }
} else { } else {
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", log.warn("Received AckMessage with error state for {} with tradeId={}, sender={}, errorMessage={}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSenderNodeAddress(), ackMessage.getErrorMessage());
// nack message on chat message received when dispute closed message is nacked
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
for (Dispute dispute : trade.getDisputes()) {
synchronized (dispute.getChatMessages()) {
for (ChatMessage chatMessage : dispute.getChatMessages()) {
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
if (trade.getDisputeState().isCloseRequested()) {
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
dispute.setIsClosed();
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
}
}
}
}
}
}
} }
getAllChatMessages(ackMessage.getSourceId()).stream() getAllChatMessages(ackMessage.getSourceId()).stream()

View File

@ -77,6 +77,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
REOPENED, REOPENED,
CLOSED; CLOSED;
public boolean isOpen() {
return this == NEW || this == OPEN || this == REOPENED;
}
public static Dispute.State fromProto(protobuf.Dispute.State state) { public static Dispute.State fromProto(protobuf.Dispute.State state) {
return ProtoUtil.enumFromProto(Dispute.State.class, state.name()); return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
} }
@ -349,18 +353,21 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void addAndPersistChatMessage(ChatMessage chatMessage) { public void addAndPersistChatMessage(ChatMessage chatMessage) {
synchronized (chatMessages) {
if (!chatMessages.contains(chatMessage)) { if (!chatMessages.contains(chatMessage)) {
chatMessages.add(chatMessage); chatMessages.add(chatMessage);
} else { } else {
log.error("disputeDirectMessage already exists"); log.error("disputeDirectMessage already exists");
} }
} }
}
public boolean isMediationDispute() { public boolean isMediationDispute() {
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION; return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
} }
public boolean removeAllChatMessages() { public boolean removeAllChatMessages() {
synchronized (chatMessages) {
if (chatMessages.size() > 1) { if (chatMessages.size() > 1) {
// removes all chat except the initial guidelines message. // removes all chat except the initial guidelines message.
String firstMessageUid = chatMessages.get(0).getUid(); String firstMessageUid = chatMessages.get(0).getUid();
@ -369,6 +376,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
} }
return false; return false;
} }
}
public void maybeClearSensitiveData() { public void maybeClearSensitiveData() {
String change = ""; String change = "";

View File

@ -90,6 +90,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) { public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
synchronized (disputeList.getObservableList()) {
disputeList.stream().forEach(dispute -> { disputeList.stream().forEach(dispute -> {
String tradeId = dispute.getTradeId(); String tradeId = dispute.getTradeId();
if (dispute.isClosed() && closedDisputeHandler != null) { if (dispute.isClosed() && closedDisputeHandler != null) {
@ -97,6 +98,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
} }
}); });
} }
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -130,8 +132,10 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
} }
ObservableList<Dispute> getObservableList() { ObservableList<Dispute> getObservableList() {
synchronized (disputeList.getObservableList()) {
return disputeList.getObservableList(); return disputeList.getObservableList();
} }
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -151,10 +155,12 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
isAlerting -> { isAlerting -> {
// We get the event before the list gets updated, so we execute on next frame // We get the event before the list gets updated, so we execute on next frame
UserThread.execute(() -> { UserThread.execute(() -> {
synchronized (disputeList.getObservableList()) {
int numAlerts = (int) disputeList.getList().stream() int numAlerts = (int) disputeList.getList().stream()
.mapToLong(x -> x.getBadgeCountProperty().getValue()) .mapToLong(x -> x.getBadgeCountProperty().getValue())
.sum(); .sum();
numOpenDisputes.set(numAlerts); numOpenDisputes.set(numAlerts);
}
}); });
}); });
disputedTradeIds.add(dispute.getTradeId()); disputedTradeIds.add(dispute.getTradeId());

View File

@ -157,6 +157,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeListService.requestPersistence(); disputeListService.requestPersistence();
} }
protected void requestPersistence(Trade trade) {
trade.requestPersistence();
disputeListService.requestPersistence();
}
@Override @Override
public NodeAddress getPeerNodeAddress(ChatMessage message) { public NodeAddress getPeerNodeAddress(ChatMessage message) {
Optional<Dispute> disputeOptional = findDispute(message); Optional<Dispute> disputeOptional = findDispute(message);
@ -182,11 +187,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
@Override @Override
public List<ChatMessage> getAllChatMessages(String tradeId) { public List<ChatMessage> getAllChatMessages(String tradeId) {
synchronized (getDisputeList().getObservableList()) {
return getDisputeList().stream() return getDisputeList().stream()
.filter(dispute -> dispute.getTradeId().equals(tradeId)) .filter(dispute -> dispute.getTradeId().equals(tradeId))
.flatMap(dispute -> dispute.getChatMessages().stream()) .flatMap(dispute -> dispute.getChatMessages().stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
}
@Override @Override
public boolean channelOpen(ChatMessage message) { public boolean channelOpen(ChatMessage message) {
@ -234,7 +241,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
public ObservableList<Dispute> getDisputesAsObservableList() { public ObservableList<Dispute> getDisputesAsObservableList() {
synchronized(disputeListService.getDisputeList()) { synchronized(disputeListService.getDisputeList().getObservableList()) {
return disputeListService.getObservableList(); return disputeListService.getObservableList();
} }
} }
@ -244,7 +251,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
protected T getDisputeList() { protected T getDisputeList() {
synchronized(disputeListService.getDisputeList()) { synchronized(disputeListService.getDisputeList().getObservableList()) {
return disputeListService.getDisputeList(); return disputeListService.getDisputeList();
} }
} }
@ -321,15 +328,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// trader sends message to arbitrator to open dispute // trader sends message to arbitrator to open dispute
public void sendDisputeOpenedMessage(Dispute dispute, public void sendDisputeOpenedMessage(Dispute dispute,
boolean reOpen,
String updatedMultisigHex,
ResultHandler resultHandler, ResultHandler resultHandler,
FaultHandler faultHandler) { FaultHandler faultHandler) {
// get trade // get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) { if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId()); String errorMsg = "Dispute trade does not exist, tradeId=" + dispute.getTradeId();
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
return;
}
// arbitrator cannot open disputes
if (trade.isArbitrator()) {
String errorMsg = "Arbitrators cannot open disputes.";
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
return; return;
} }
@ -343,7 +356,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return; return;
} }
synchronized (disputeList) { synchronized (disputeList.getObservableList()) {
if (disputeList.contains(dispute)) { if (disputeList.contains(dispute)) {
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
log.warn(msg); log.warn(msg);
@ -352,7 +365,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
if (!storedDisputeOptional.isPresent() || reOpen) { if (!storedDisputeOptional.isPresent() || reOpen) {
// add or re-open dispute
if (reOpen) {
dispute = storedDisputeOptional.get();
} else {
disputeList.add(dispute);
}
String disputeInfo = getDisputeInfo(dispute); String disputeInfo = getDisputeInfo(dispute);
String sysMsg = dispute.isSupportTicket() ? String sysMsg = dispute.isSupportTicket() ?
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
@ -367,17 +389,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
p2PService.getAddress()); p2PService.getAddress());
chatMessage.setSystemMessage(true); chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
if (!reOpen) {
disputeList.add(dispute);
}
// create dispute opened message // create dispute opened message
trade.exportMultisigHex();
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
getSupportType(), getSupportType(),
updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex(),
trade.getArbitrator().getPaymentSentMessage()); trade.getArbitrator().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}", "chatMessage.uid={}",
@ -387,6 +407,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
// send dispute opened message // send dispute opened message
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
dispute.getAgentPubKeyRing(), dispute.getAgentPubKeyRing(),
disputeOpenedMessage, disputeOpenedMessage,
@ -420,7 +441,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// We use the chatMessage wrapped inside the openNewDisputeMessage for // We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true); chatMessage.setStoredInMailbox(true);
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
requestPersistence(); requestPersistence();
resultHandler.handleResult(); resultHandler.handleResult();
} }
@ -437,6 +457,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// We use the chatMessage wrapped inside the openNewDisputeMessage for // We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage); chatMessage.setSendMessageError(errorMessage);
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
requestPersistence(); requestPersistence();
faultHandler.handleFault("Sending dispute message failed: " + faultHandler.handleFault("Sending dispute message failed: " +
errorMessage, new DisputeMessageDeliveryFailedException()); errorMessage, new DisputeMessageDeliveryFailedException());
@ -455,18 +476,32 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator // arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) { protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
Dispute dispute = message.getDispute(); Dispute msgDispute = message.getDispute();
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
// get trade // get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
if (trade == null) { if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId()); log.warn("Dispute trade {} does not exist", msgDispute.getTradeId());
return;
}
if (trade.isPayoutPublished()) {
log.warn("Dispute trade {} payout already published", msgDispute.getTradeId());
return; return;
} }
// find existing dispute
Optional<Dispute> storedDisputeOptional = findDispute(msgDispute);
// determine if re-opening dispute
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
// use existing dispute or create new
Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute;
// process on trade thread
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
String errorMessage = null; String errorMessage = null;
PubKeyRing senderPubKeyRing = null; PubKeyRing senderPubKeyRing = null;
try { try {
@ -503,14 +538,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
// get sender // get sender
TradePeer sender;
if (reOpen) { // re-open can come from either peer
sender = trade.isArbitrator() ? trade.getTradePeer(message.getSenderNodeAddress()) : trade.getArbitrator();
senderPubKeyRing = sender.getPubKeyRing();
} else {
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing(); senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
TradePeer sender = trade.getTradePeer(senderPubKeyRing); sender = trade.getTradePeer(senderPubKeyRing);
}
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// update sender node address // update sender node address
sender.setNodeAddress(message.getSenderNodeAddress()); sender.setNodeAddress(message.getSenderNodeAddress());
// message to trader is expected from arbitrator // verify message to trader is expected from arbitrator
if (!trade.isArbitrator() && sender != trade.getArbitrator()) { if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator"); throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
} }
@ -528,16 +569,29 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// add chat message with price info // add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
// add dispute // add or re-open dispute
synchronized (disputeList) { synchronized (disputeList) {
if (!disputeList.contains(dispute)) { if (!disputeList.contains(msgDispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent() || reOpen) {
if (!storedDisputeOptional.isPresent()) {
// update trade state
if (reOpen) {
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
} else {
disputeList.add(dispute); disputeList.add(dispute);
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
}
// send dispute opened message to peer if arbitrator // reset buyer and seller unsigned payout tx hex
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); trade.getBuyer().setUnsignedPayoutTxHex(null);
trade.getSeller().setUnsignedPayoutTxHex(null);
// send dispute opened message to other peer if arbitrator
if (trade.isArbitrator()) {
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
}
tradeManager.requestPersistence(); tradeManager.requestPersistence();
errorMessage = null; errorMessage = null;
} else { } else {
@ -548,7 +602,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// add chat message with mediation info if applicable // add chat message with mediation info if applicable
addMediationResultMessage(dispute); addMediationResultMessage(dispute);
} else { } else {
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId()); throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + msgDispute.getTradeId());
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -561,7 +615,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// use chat message instead of open dispute message for the ack // use chat message instead of open dispute message for the ack
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages(); ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
if (!messages.isEmpty()) { if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0); ChatMessage msg = messages.get(messages.size() - 1); // send ack to sender of last chat message
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage); sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
} }
@ -575,7 +629,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
Contract contractFromOpener, Contract contractFromOpener,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
String updatedMultisigHex) { String updatedMultisigHex) {
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId()); log.info("{} sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is // We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct // being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
// message and not skip the system message of the peer as it would be the case if we have created the system msg // message and not skip the system message of the peer as it would be the case if we have created the system msg
@ -597,6 +651,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return; return;
} }
// create mirrored dispute
Dispute dispute = new Dispute(new Date().getTime(), Dispute dispute = new Dispute(new Date().getTime(),
disputeFromOpener.getTradeId(), disputeFromOpener.getTradeId(),
pubKeyRing.hashCode(), pubKeyRing.hashCode(),
@ -622,10 +677,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx()); dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
// skip if dispute already open
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) {
// Valid case if both have opened a dispute and agent was not online.
if (storedDisputeOptional.isPresent()) {
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
return; return;
} }
@ -647,9 +701,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
addPriceInfoMessage(dispute, 0); addPriceInfoMessage(dispute, 0);
// add or re-open dispute
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
if (reOpen) {
dispute = storedDisputeOptional.get();
dispute.reOpen();
} else {
synchronized (disputeList) { synchronized (disputeList) {
disputeList.add(dispute); disputeList.add(dispute);
} }
}
// get trade // get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
@ -658,10 +719,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return; return;
} }
// We mirrored dispute already! // create dispute opened message with peer dispute
Contract contract = dispute.getContract(); TradePeer peer = trade.getTradePeer(pubKeyRing);
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); PubKeyRing peersPubKeyRing = peer.getPubKeyRing();
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress(); NodeAddress peersNodeAddress = peer.getNodeAddress();
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute, DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(), p2PService.getAddress(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
@ -749,7 +810,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
} }
// create dispute payout tx once per trader if we have their updated multisig hex // create dispute payout tx
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing()); TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) { if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
@ -792,7 +853,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.getChatMessage().setArrived(true); disputeResult.getChatMessage().setArrived(true);
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.pollWalletNormallyForMs(30000); trade.pollWalletNormallyForMs(30000);
requestPersistence(); requestPersistence(trade);
resultHandler.handleResult(); resultHandler.handleResult();
} }
@ -811,7 +872,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.getChatMessage().setStoredInMailbox(true); disputeResult.getChatMessage().setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence(trade);
resultHandler.handleResult(); resultHandler.handleResult();
} }
@ -828,13 +889,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
disputeResult.getChatMessage().setSendMessageError(errorMessage); disputeResult.getChatMessage().setSendMessageError(errorMessage);
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence(trade);
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage)); faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
} }
} }
); );
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence(trade);
} catch (Exception e) { } catch (Exception e) {
faultHandler.handleFault(e.getMessage(), e); faultHandler.handleFault(e.getMessage(), e);
} }
@ -900,11 +961,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update trade state // update trade state
if (updateState) { if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx); trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
trade.updatePayout(payoutTx); trade.updatePayout(payoutTx);
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} }
trade.requestPersistence();
return payoutTx; return payoutTx;
} catch (Exception e) { } catch (Exception e) {
trade.syncAndPollWallet(); trade.syncAndPollWallet();
@ -937,21 +998,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing()); return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
} }
private Optional<Dispute> findDispute(Dispute dispute) { public Optional<Dispute> findDispute(Dispute dispute) {
return findDispute(dispute.getTradeId(), dispute.getTraderId()); return findDispute(dispute.getTradeId(), dispute.getTraderId());
} }
protected Optional<Dispute> findDispute(DisputeResult disputeResult) { public Optional<Dispute> findDispute(DisputeResult disputeResult) {
ChatMessage chatMessage = disputeResult.getChatMessage(); ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null"); checkNotNull(chatMessage, "chatMessage must not be null");
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId()); return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
} }
private Optional<Dispute> findDispute(ChatMessage message) { public Optional<Dispute> findDispute(ChatMessage message) {
return findDispute(message.getTradeId(), message.getTraderId()); return findDispute(message.getTradeId(), message.getTraderId());
} }
protected Optional<Dispute> findDispute(String tradeId, int traderId) { public Optional<Dispute> findDispute(String tradeId, int traderId) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");

View File

@ -78,6 +78,7 @@ import haveno.network.p2p.P2PService;
import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.Connection;
import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.MessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroMultisigSignResult;
@ -87,9 +88,11 @@ import monero.wallet.model.MoneroTxWallet;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -168,7 +171,28 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
@Override @Override
public void cleanupDisputes() { public void cleanupDisputes() {
// no action
// remove disputes opened by arbitrator, which is not allowed
Set<Dispute> toRemoves = new HashSet<>();
List<Dispute> disputes = getDisputeList().getList();
for (Dispute dispute : disputes) {
// get dispute's trade
final Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
// collect dispute if owned by arbitrator
if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) {
toRemoves.add(dispute);
}
}
for (Dispute toRemove : toRemoves) {
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
getDisputeList().remove(toRemove);
}
} }
@Override @Override
@ -216,7 +240,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
ChatMessage chatMessage = null; ChatMessage chatMessage = null;
Dispute dispute = null; Dispute dispute = null;
synchronized (trade) { synchronized (trade.getLock()) {
try { try {
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult(); DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
chatMessage = disputeResult.getChatMessage(); chatMessage = disputeResult.getChatMessage();
@ -252,7 +276,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// save dispute closed message for reprocessing // save dispute closed message for reprocessing
trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage); trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage);
requestPersistence(); requestPersistence(trade);
// verify arbitrator does not receive DisputeClosedMessage // verify arbitrator does not receive DisputeClosedMessage
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) { if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
@ -263,11 +287,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// set dispute state // set dispute state
cleanupRetryMap(uid); cleanupRetryMap(uid);
synchronized (dispute.getChatMessages()) {
if (!dispute.getChatMessages().contains(chatMessage)) { if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
} else { } else {
log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId()); log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId());
} }
}
dispute.setIsClosed(); dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) { if (dispute.disputeResultProperty().get() != null) {
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId); log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
@ -308,7 +334,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (trade.isPayoutPublished()) { if (trade.isPayoutPublished()) {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else { } else {
if (e instanceof IllegalArgumentException) throw e; if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) throw e;
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e); else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
} }
} }
@ -326,17 +352,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// We use the chatMessage as we only persist those not the DisputeClosedMessage. // We use the chatMessage as we only persist those not the DisputeClosedMessage.
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage. // If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
requestPersistence(); requestPersistence(trade);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error processing dispute closed message: " + e.getMessage()); log.warn("Error processing dispute closed message: {}", e.getMessage());
e.printStackTrace(); e.printStackTrace();
requestPersistence(); requestPersistence(trade);
// nack bad message and do not reprocess // nack bad message and do not reprocess
if (e instanceof IllegalArgumentException) { if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) {
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
String warningMsg = "Error processing dispute closed message: " + e.getMessage() + "\n\nOpen another dispute to try again (ctrl+o).";
trade.prependErrorMessage(warningMsg);
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage()); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
requestPersistence(); HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
requestPersistence(trade);
throw e; throw e;
} }
@ -356,7 +386,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) { public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
if (trade.isShutDownStarted()) return; if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
// skip if no need to reprocess // skip if no need to reprocess
if (trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) { if (trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) {
@ -442,12 +472,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
if (trade.getPayoutTxHex() == null) { if (trade.getPayoutTxHex() == null) {
try {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
disputeTxSet.setMultisigTxHex(signedMultisigTxHex); disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
trade.setPayoutTxHex(signedMultisigTxHex); trade.setPayoutTxHex(signedMultisigTxHex);
requestPersistence(); requestPersistence(trade);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage());
}
// verify mining fee is within tolerance by recreating payout tx // verify mining fee is within tolerance by recreating payout tx
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
@ -470,6 +504,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// submit fully signed payout tx to the network // submit fully signed payout tx to the network
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
@ -478,7 +513,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(); if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -487,6 +522,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
trade.updatePayout(disputeTxSet.getTxs().get(0)); trade.updatePayout(disputeTxSet.getTxs().get(0));
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED); trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash()); dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
requestPersistence(trade);
return disputeTxSet; return disputeTxSet;
} }

View File

@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.Sig; import haveno.common.crypto.Sig;
import haveno.common.util.Utilities; import haveno.common.util.Utilities;
import haveno.core.api.XmrConnectionService;
import haveno.core.app.HavenoSetup; import haveno.core.app.HavenoSetup;
import haveno.core.offer.OfferPayload; import haveno.core.offer.OfferPayload;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
@ -106,6 +107,7 @@ public class HavenoUtils {
public static HavenoSetup havenoSetup; public static HavenoSetup havenoSetup;
public static ArbitrationManager arbitrationManager; public static ArbitrationManager arbitrationManager;
public static XmrWalletService xmrWalletService; public static XmrWalletService xmrWalletService;
public static XmrConnectionService xmrConnectionService;
public static OpenOfferManager openOfferManager; public static OpenOfferManager openOfferManager;
public static boolean isSeedNode() { public static boolean isSeedNode() {
@ -502,4 +504,20 @@ public class HavenoUtils {
else if (Config.baseCurrencyNetwork().isStagenet()) return 38081; else if (Config.baseCurrencyNetwork().isStagenet()) return 38081;
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet"); else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
} }
public static void setTopError(String msg) {
havenoSetup.getTopErrorMsg().set(msg);
}
public static boolean isConnectionRefused(Exception e) {
return e != null && e.getMessage().contains("Connection refused");
}
public static boolean isReadTimeout(Exception e) {
return e != null && e.getMessage().contains("Read timed out");
}
public static boolean isUnresponsive(Exception e) {
return isConnectionRefused(e) || isReadTimeout(e);
}
} }

View File

@ -44,7 +44,6 @@ import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil; import haveno.common.proto.ProtoUtil;
import haveno.common.taskrunner.Model; import haveno.common.taskrunner.Model;
import haveno.common.util.Utilities; import haveno.common.util.Utilities;
import haveno.core.api.XmrConnectionService;
import haveno.core.monetary.Price; import haveno.core.monetary.Price;
import haveno.core.monetary.Volume; import haveno.core.monetary.Volume;
import haveno.core.network.MessageState; import haveno.core.network.MessageState;
@ -69,6 +68,7 @@ import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatistics3;
import haveno.core.util.VolumeUtil; import haveno.core.util.VolumeUtil;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletBase;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessage;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
@ -76,14 +76,12 @@ import haveno.network.p2p.P2PService;
import haveno.network.p2p.network.TorNetworkNode; import haveno.network.p2p.network.TorNetworkNode;
import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
@ -135,19 +133,18 @@ import static com.google.common.base.Preconditions.checkNotNull;
* stored in the task model. * stored in the task model.
*/ */
@Slf4j @Slf4j
public abstract class Trade implements Tradable, Model { public abstract class Trade extends XmrWalletBase implements Tradable, Model {
@Getter
public final Object lock = new Object();
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_"; private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
private final Object walletLock = new Object(); protected final Object pollLock = new Object();
private final Object pollLock = new Object(); protected static final Object importMultisigLock = new Object();
private final LongProperty walletHeight = new SimpleLongProperty(0);
private MoneroWallet wallet;
private boolean wasWalletSynced;
private boolean pollInProgress; private boolean pollInProgress;
private boolean restartInProgress; private boolean restartInProgress;
private Subscription protocolErrorStateSubscription; private Subscription protocolErrorStateSubscription;
@ -321,7 +318,11 @@ public abstract class Trade implements Tradable, Model {
} }
public boolean isOpen() { public boolean isOpen() {
return this == DisputeState.DISPUTE_OPENED; return isRequested() && !isClosed();
}
public boolean isCloseRequested() {
return this.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal();
} }
public boolean isClosed() { public boolean isClosed() {
@ -409,9 +410,6 @@ public abstract class Trade implements Tradable, Model {
// Immutable // Immutable
@Getter @Getter
transient final private XmrWalletService xmrWalletService; transient final private XmrWalletService xmrWalletService;
@Getter
transient final private XmrConnectionService xmrConnectionService;
transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0); transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0);
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state); transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase); transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
@ -437,10 +435,6 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
transient private boolean isInitialized; transient private boolean isInitialized;
transient private boolean isFullyInitialized; transient private boolean isFullyInitialized;
@Getter
transient private boolean isShutDownStarted;
@Getter
transient private boolean isShutDown;
// Added in v1.2.0 // Added in v1.2.0
transient private ObjectProperty<BigInteger> tradeAmountProperty; transient private ObjectProperty<BigInteger> tradeAmountProperty;
@ -507,11 +501,12 @@ public abstract class Trade implements Tradable, Model {
@Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) { @Nullable NodeAddress arbitratorNodeAddress) {
super();
this.offer = offer; this.offer = offer;
this.amount = tradeAmount.longValueExact(); this.amount = tradeAmount.longValueExact();
this.price = tradePrice; this.price = tradePrice;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
this.xmrConnectionService = xmrWalletService.getConnectionService(); this.xmrConnectionService = xmrWalletService.getXmrConnectionService();
this.processModel = processModel; this.processModel = processModel;
this.uid = uid; this.uid = uid;
this.takeOfferDate = new Date().getTime(); this.takeOfferDate = new Date().getTime();
@ -846,8 +841,8 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public boolean requestSwitchToNextBestConnection() { public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
if (xmrConnectionService.requestSwitchToNextBestConnection()) { if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
return true; return true;
} }
@ -891,10 +886,6 @@ public abstract class Trade implements Tradable, Model {
}).start(); }).start();
} }
private boolean isReadTimeoutError(String errMsg) {
return errMsg.contains("Read timed out");
}
// TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex? // TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex?
private boolean isInvalidImportError(String errMsg) { private boolean isInvalidImportError(String errMsg) {
return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account"); return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account");
@ -1058,15 +1049,27 @@ public abstract class Trade implements Tradable, Model {
public MoneroTxWallet createTx(MoneroTxConfig txConfig) { public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
return wallet.createTx(txConfig); MoneroTxWallet tx = wallet.createTx(txConfig);
exportMultisigHex();
requestSaveWallet();
return tx;
} }
} }
} }
public void exportMultisigHex() {
synchronized (walletLock) {
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
requestPersistence();
}
}
public void importMultisigHex() { public void importMultisigHex() {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
synchronized (importMultisigLock) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
doImportMultisigHex(); doImportMultisigHex();
break; break;
@ -1074,15 +1077,15 @@ public abstract class Trade implements Tradable, Model {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
} }
} }
} }
}
private void doImportMultisigHex() { private void doImportMultisigHex() {
@ -1154,6 +1157,12 @@ public abstract class Trade implements Tradable, Model {
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
} }
private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
getWallet(); // re-open wallet
}
private String getMultisigHexRole(String multisigHex) { private String getMultisigHexRole(String multisigHex) {
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator"; if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer"; if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
@ -1175,14 +1184,15 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
return doCreatePayoutTx(); return doCreatePayoutTx();
} catch (IllegalArgumentException | IllegalStateException e) { } catch (IllegalArgumentException | IllegalStateException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1220,13 +1230,11 @@ public abstract class Trade implements Tradable, Model {
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
// update state // update state
saveWallet();
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2)); BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(payoutTxFeeSplit); getBuyer().setPayoutTxFee(payoutTxFeeSplit);
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount()); getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
getSeller().setPayoutTxFee(payoutTxFeeSplit); getSeller().setPayoutTxFee(payoutTxFeeSplit);
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount()); getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
return payoutTx; return payoutTx;
} }
@ -1234,6 +1242,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
return createTx(txConfig); return createTx(txConfig);
@ -1242,8 +1251,8 @@ public abstract class Trade implements Tradable, Model {
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1263,6 +1272,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
doProcessPayoutTx(payoutTxHex, sign, publish); doProcessPayoutTx(payoutTxHex, sign, publish);
break; break;
@ -1270,9 +1280,12 @@ public abstract class Trade implements Tradable, Model {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} finally {
requestSaveWallet();
requestPersistence();
} }
} }
} }
@ -1492,13 +1505,13 @@ public abstract class Trade implements Tradable, Model {
// repeatedly acquire lock to clear tasks // repeatedly acquire lock to clear tasks
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
synchronized (this) { synchronized (getLock()) {
HavenoUtils.waitFor(10); HavenoUtils.waitFor(10);
} }
} }
// shut down trade threads // shut down trade threads
synchronized (this) { synchronized (getLock()) {
isInitialized = false; isInitialized = false;
isShutDown = true; isShutDown = true;
List<Runnable> shutDownThreads = new ArrayList<>(); List<Runnable> shutDownThreads = new ArrayList<>();
@ -2299,7 +2312,7 @@ public abstract class Trade implements Tradable, Model {
private void doPublishTradeStatistics() { private void doPublishTradeStatistics() {
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null); String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode; boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode;
TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode); TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode, true);
if (tradeStatistics.isValid()) { if (tradeStatistics.isValid()) {
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId()); log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true); processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
@ -2337,7 +2350,10 @@ public abstract class Trade implements Tradable, Model {
// check if ignored // check if ignored
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (getWallet() == null) return; if (getWallet() == null) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
updatePollPeriod();
return;
}
// set daemon connection (must restart monero-wallet-rpc if proxy uri changed) // set daemon connection (must restart monero-wallet-rpc if proxy uri changed)
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
@ -2397,7 +2413,9 @@ public abstract class Trade implements Tradable, Model {
} }
private void syncWallet(boolean pollWallet) { private void syncWallet(boolean pollWallet) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
synchronized (walletLock) {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
if (isWalletBehind()) { if (isWalletBehind()) {
@ -2406,6 +2424,7 @@ public abstract class Trade implements Tradable, Model {
syncWalletIfBehind(); syncWalletIfBehind();
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
} }
}
// apply tor after wallet synced depending on configuration // apply tor after wallet synced depending on configuration
if (!wasWalletSynced) { if (!wasWalletSynced) {
@ -2417,7 +2436,7 @@ public abstract class Trade implements Tradable, Model {
if (pollWallet) pollWallet(); if (pollWallet) pollWallet();
} catch (Exception e) { } catch (Exception e) {
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
throw e; throw e;
} }
} }
@ -2546,11 +2565,12 @@ public abstract class Trade implements Tradable, Model {
// rescan spent outputs to detect unconfirmed payout tx // rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
wallet.rescanSpent(); wallet.rescanSpent();
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); // do not block polling thread
} }
} }
@ -2595,12 +2615,11 @@ public abstract class Trade implements Tradable, Model {
} }
} }
} catch (Exception e) { } catch (Exception e) {
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet();
if (isConnectionRefused) forceRestartTradeWallet();
else { else {
boolean isWalletConnected = isWalletConnectedToDaemon(); boolean isWalletConnected = isWalletConnectedToDaemon();
if (wallet != null && !isShutDownStarted && isWalletConnected) { if (wallet != null && !isShutDownStarted && isWalletConnected) {
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection());
//e.printStackTrace(); //e.printStackTrace();
} }
} }
@ -2613,9 +2632,15 @@ public abstract class Trade implements Tradable, Model {
} }
private void syncWalletIfBehind() { private void syncWalletIfBehind() {
if (isWalletBehind()) {
synchronized (walletLock) { synchronized (walletLock) {
if (isWalletBehind()) {
// TODO: local tests have timing failures unless sync called directly
if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) {
xmrWalletService.syncWallet(wallet); xmrWalletService.syncWallet(wallet);
} else {
syncWithProgress();
}
walletHeight.set(wallet.getHeight()); walletHeight.set(wallet.getHeight());
} }
} }
@ -2660,7 +2685,8 @@ public abstract class Trade implements Tradable, Model {
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
wallet.rescanBlockchain(); wallet.rescanBlockchain();
} catch (Exception e) { } catch (Exception e) {
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while
throw e; throw e;
} finally { } finally {
@ -2790,7 +2816,7 @@ public abstract class Trade implements Tradable, Model {
if (!isInitialized || isShutDownStarted) return; if (!isInitialized || isShutDownStarted) return;
if (isWalletConnectedToDaemon()) { if (isWalletConnectedToDaemon()) {
e.printStackTrace(); e.printStackTrace();
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getXmrConnectionService().getConnection());
}; };
} }
}, getId()); }, getId());

View File

@ -45,7 +45,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
System.out.println("ArbitratorProtocol.handleInitTradeRequest()"); System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
@ -80,7 +80,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
public void handleDepositRequest(DepositRequest request, NodeAddress sender) { public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId()); System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
Validator.checkTradeId(processModel.getOfferId(), request); Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request); processModel.setTradeMessage(request);

View File

@ -62,7 +62,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)

View File

@ -70,7 +70,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.tradeResultHandler = tradeResultHandler; this.tradeResultHandler = tradeResultHandler;
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
@ -101,7 +101,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
NodeAddress peer) { NodeAddress peer) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)
.with(message) .with(message)

View File

@ -75,7 +75,7 @@ public class BuyerProtocol extends DisputeProtocol {
// re-send payment sent message if not acked // re-send payment sent message if not acked
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
synchronized (trade) { synchronized (trade.getLock()) {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) { if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) {
latchTrade(); latchTrade();
@ -121,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
System.out.println("BuyerProtocol.onPaymentSent()"); System.out.println("BuyerProtocol.onPaymentSent()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
BuyerEvent event = BuyerEvent.PAYMENT_SENT; BuyerEvent event = BuyerEvent.PAYMENT_SENT;

View File

@ -67,7 +67,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)

View File

@ -70,7 +70,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.tradeResultHandler = tradeResultHandler; this.tradeResultHandler = tradeResultHandler;
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
@ -101,7 +101,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
NodeAddress peer) { NodeAddress peer) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)
.with(message) .with(message)

View File

@ -70,7 +70,7 @@ public class SellerProtocol extends DisputeProtocol {
// re-send payment received message if payout not published // re-send payment received message if payout not published
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
synchronized (trade) { synchronized (trade.getLock()) {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) { if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
latchTrade(); latchTrade();
@ -117,7 +117,7 @@ public class SellerProtocol extends DisputeProtocol {
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("SellerProtocol.onPaymentReceived()"); log.info("SellerProtocol.onPaymentReceived()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
latchTrade(); latchTrade();
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
SellerEvent event = SellerEvent.PAYMENT_RECEIVED; SellerEvent event = SellerEvent.PAYMENT_RECEIVED;

View File

@ -243,7 +243,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this); if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
// initialize trade // initialize trade
synchronized (trade) { synchronized (trade.getLock()) {
trade.initialize(processModel.getProvider()); trade.initialize(processModel.getProvider());
// process mailbox messages // process mailbox messages
@ -261,7 +261,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return; if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
depositsConfirmedTasksCalled = true; depositsConfirmedTasksCalled = true;
synchronized (trade) { synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
latchTrade(); latchTrade();
expect(new Condition(trade)) expect(new Condition(trade))
@ -282,7 +282,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
if (trade.isShutDownStarted()) return; if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
// skip if no need to reprocess // skip if no need to reprocess
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
@ -299,7 +299,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
trade.addInitProgressStep(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
// check trade // check trade
if (trade.hasFailed()) { if (trade.hasFailed()) {
@ -335,7 +335,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
// check trade // check trade
if (trade.hasFailed()) { if (trade.hasFailed()) {
@ -379,7 +379,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
trade.addInitProgressStep(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
// check trade // check trade
if (trade.hasFailed()) { if (trade.hasFailed()) {
@ -425,7 +425,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
trade.addInitProgressStep(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
Validator.checkTradeId(processModel.getOfferId(), response); Validator.checkTradeId(processModel.getOfferId(), response);
latchTrade(); latchTrade();
processModel.setTradeMessage(response); processModel.setTradeMessage(response);
@ -455,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
if (!trade.isInitialized() || trade.isShutDown()) return; if (!trade.isInitialized() || trade.isShutDown()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return; if (!trade.isInitialized() || trade.isShutDown()) return;
latchTrade(); latchTrade();
this.errorMessageHandler = null; this.errorMessageHandler = null;
@ -493,7 +493,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// a mailbox message with PaymentSentMessage. // a mailbox message with PaymentSentMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process // TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set. // the mailbox msg once wallet is ready and trade state set.
synchronized (trade) { synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return; if (!trade.isInitialized() || trade.isShutDown()) return;
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
@ -542,7 +542,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
return; return;
} }
synchronized (trade) { synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return; if (!trade.isInitialized() || trade.isShutDown()) return;
latchTrade(); latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
@ -817,7 +817,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) { void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) {
log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getConnectionService().getConnection()); log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection());
if (message != null) { if (message != null) {
sendAckMessage(ackReceiver, message, false, errorMessage); sendAckMessage(ackReceiver, message, false, errorMessage);

View File

@ -87,6 +87,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx); trade.updatePayout(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
trade.requestPersistence();
} }
complete(); complete();
@ -163,7 +164,8 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// convert info to csv // convert info to csv
Integer length = null; Integer length = null;
for (Pair<String, List<Object>> pair : pairs) { for (Pair<String, List<Object>> pair : pairs) {
if (length == null) length = pair.getSecond().size(); if (length == null)
length = pair.getSecond().size();
} }
System.out.println(pairsToCsv(pairs)); System.out.println(pairsToCsv(pairs));
@ -203,5 +205,3 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
return sb.toString(); return sb.toString();
} }
} }

View File

@ -28,9 +28,9 @@ import haveno.core.trade.Trade.State;
import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractRequest;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger; import java.math.BigInteger;
@ -77,7 +77,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs // create deposit tx and freeze inputs
MoneroTxWallet depositTx = null; MoneroTxWallet depositTx = null;
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout // check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId()); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId());
@ -100,12 +100,14 @@ public class MaybeSendSignContractRequest extends TradeTask {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try { try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View File

@ -58,7 +58,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
// if seller, decrypt buyer's payment account payload // if seller, decrypt buyer's payment account payload
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence();
// update state // update state
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);

View File

@ -90,7 +90,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed(); for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed();
} }
processModel.getTradeManager().requestPersistence(); trade.requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View File

@ -76,8 +76,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
// export multisig hex once // export multisig hex once
if (trade.getSelf().getUpdatedMultisigHex() == null) { if (trade.getSelf().getUpdatedMultisigHex() == null) {
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); trade.exportMultisigHex();
processModel.getTradeManager().requestPersistence();
} }
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the

View File

@ -24,8 +24,8 @@ import haveno.core.trade.TakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger; import java.math.BigInteger;
@ -49,7 +49,7 @@ public class TakerReserveTradeFunds extends TradeTask {
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = null; MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout // check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
@ -66,12 +66,14 @@ public class TakerReserveTradeFunds extends TradeTask {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try { try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View File

@ -69,10 +69,13 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
@JsonExclude @JsonExclude
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); private transient static final ZoneId ZONE_ID = ZoneId.systemDefault();
private static final double FUZZ_AMOUNT_PCT = 0.05;
private static final int FUZZ_DATE_HOURS = 24;
public static TradeStatistics3 from(Trade trade, public static TradeStatistics3 from(Trade trade,
@Nullable String referralId, @Nullable String referralId,
boolean isTorNetworkNode) { boolean isTorNetworkNode,
boolean isFuzzed) {
Map<String, String> extraDataMap = new HashMap<>(); Map<String, String> extraDataMap = new HashMap<>();
if (referralId != null) { if (referralId != null) {
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
@ -90,9 +93,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
Offer offer = checkNotNull(trade.getOffer()); Offer offer = checkNotNull(trade.getOffer());
return new TradeStatistics3(offer.getCurrencyCode(), return new TradeStatistics3(offer.getCurrencyCode(),
trade.getPrice().getValue(), trade.getPrice().getValue(),
fuzzTradeAmountReproducibly(trade), isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(),
offer.getPaymentMethod().getId(), offer.getPaymentMethod().getId(),
fuzzTradeDateReproducibly(trade), isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(),
truncatedArbitratorNodeAddress, truncatedArbitratorNodeAddress,
extraDataMap); extraDataMap);
} }
@ -101,8 +104,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
long originalTimestamp = trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime();
long exactAmount = trade.getAmount().longValueExact(); long exactAmount = trade.getAmount().longValueExact();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
long adjustedAmount = (long) random.nextDouble( long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT));
exactAmount * 0.95, exactAmount * 1.05);
log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount);
return adjustedAmount; return adjustedAmount;
} }
@ -110,8 +112,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099
long originalTimestamp = trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime();
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
long adjustedTimestamp = random.nextLong( long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS), originalTimestamp);
originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp);
log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp));
return adjustedTimestamp; return adjustedTimestamp;
} }

View File

@ -110,35 +110,54 @@ public class TradeStatisticsManager {
maybeDumpStatistics(); maybeDumpStatistics();
} }
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> set) { private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> tradeStats) {
// collect trades before May 31, 2024 // collect trades before August 7, 2024
Set<TradeStatistics3> tradesBeforeMay31_24 = set.stream() Set<TradeStatistics3> earlyTrades = tradeStats.stream()
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-05-31T00:00:00Z"))) .filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-08-07T00:00:00Z")))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// collect duplicated trades // collect duplicated trades
Set<TradeStatistics3> duplicated = new HashSet<TradeStatistics3>(); Set<TradeStatistics3> duplicates = new HashSet<TradeStatistics3>();
Set<TradeStatistics3> deduplicated = new HashSet<TradeStatistics3>(); Set<TradeStatistics3> deduplicates = new HashSet<TradeStatistics3>();
for (TradeStatistics3 tradeStatistics : tradesBeforeMay31_24) { Set<TradeStatistics3> usedAsDuplicate = new HashSet<TradeStatistics3>();
if (hasLenientDuplicate(tradeStatistics, deduplicated)) duplicated.add(tradeStatistics); for (TradeStatistics3 tradeStatistic : earlyTrades) {
else deduplicated.add(tradeStatistics); TradeStatistics3 fuzzyDuplicate = findFuzzyDuplicate(tradeStatistic, deduplicates, usedAsDuplicate);
if (fuzzyDuplicate == null) deduplicates.add(tradeStatistic);
else {
duplicates.add(tradeStatistic);
usedAsDuplicate.add(fuzzyDuplicate);
}
} }
// remove duplicated trades // remove duplicated trades
set.removeAll(duplicated); tradeStats.removeAll(duplicates);
} }
private boolean hasLenientDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set) { private TradeStatistics3 findFuzzyDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set, Set<TradeStatistics3> excluded) {
return set.stream().anyMatch(e -> isLenientDuplicate(tradeStatistics, e)); return set.stream().filter(e -> !excluded.contains(e)).filter(e -> isFuzzyDuplicate(tradeStatistics, e)).findFirst().orElse(null);
} }
private boolean isLenientDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) < 120000; if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false;
return isWithin2Minutes && if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false;
tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency()) && if (tradeStatistics1.getPrice() != tradeStatistics2.getPrice()) return false;
tradeStatistics1.getAmount() == tradeStatistics2.getAmount() && return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2);
tradeStatistics1.getPrice() == tradeStatistics2.getPrice(); }
// bug caused all peers to publish same trade with similar timestamps
private boolean isFuzzyDuplicateV1(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.MINUTES.toMillis(2);
return isWithin2Minutes;
}
// bug caused sellers to re-publish their trades with randomized amounts
private static final double FUZZ_AMOUNT_PCT = 0.05;
private static final int FUZZ_DATE_HOURS = 24;
private boolean isFuzzyDuplicateV2(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
boolean isWithinFuzzedHours = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS);
boolean isWithinFuzzedAmount = Math.abs(tradeStatistics1.getAmount() - tradeStatistics2.getAmount()) <= FUZZ_AMOUNT_PCT * tradeStatistics1.getAmount();
return isWithinFuzzedHours && isWithinFuzzedAmount;
} }
public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() { public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() {
@ -206,13 +225,23 @@ public class TradeStatisticsManager {
TradeStatistics3 tradeStatistics3 = null; TradeStatistics3 tradeStatistics3 = null;
try { try {
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode); tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
return; return;
} }
TradeStatistics3 tradeStatistics3Fuzzed = null;
try {
tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true);
} catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
return;
}
boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash()));
if (hasTradeStatistics3) { boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash()));
if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) {
log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.",
trade.getShortId()); trade.getShortId());
return; return;

View File

@ -44,6 +44,7 @@ import haveno.core.offer.OpenOfferManager;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade; import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager;
@ -124,7 +125,7 @@ public class Balances {
private void doUpdateBalances() { private void doUpdateBalances() {
synchronized (this) { synchronized (this) {
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// get wallet balances // get wallet balances
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();

View File

@ -0,0 +1,173 @@
package haveno.core.xmr.wallet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.core.api.XmrConnectionService;
import haveno.core.trade.HavenoUtils;
import haveno.core.xmr.setup.DownloadListener;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import monero.common.TaskLooper;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.MoneroWalletFull;
import monero.wallet.model.MoneroWalletListener;
@Slf4j
public class XmrWalletBase {
// constants
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 60;
public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100;
// inherited
protected MoneroWallet wallet;
@Getter
protected final Object walletLock = new Object();
@Getter
protected XmrConnectionService xmrConnectionService;
protected boolean wasWalletSynced;
protected final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
protected boolean isClosingWallet;
protected boolean isSyncingWithProgress;
protected Long syncStartHeight;
protected TaskLooper syncProgressLooper;
protected CountDownLatch syncProgressLatch;
protected Exception syncProgressError;
protected Timer syncProgressTimeout;
protected final DownloadListener downloadListener = new DownloadListener();
protected final LongProperty walletHeight = new SimpleLongProperty(0);
@Getter
protected boolean isShutDownStarted;
@Getter
protected boolean isShutDown;
// private
private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
private String testReconnectMonerod1 = "http://node.community.rino.io:18081";
private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081";
public XmrWalletBase() {
this.xmrConnectionService = HavenoUtils.xmrConnectionService;
}
public void syncWithProgress() {
syncWithProgress(false);
}
public void syncWithProgress(boolean repeatSyncToLatestHeight) {
synchronized (walletLock) {
// set initial state
isSyncingWithProgress = true;
syncProgressError = null;
long targetHeightAtStart = xmrConnectionService.getTargetHeight();
syncStartHeight = walletHeight.get();
updateSyncProgress(syncStartHeight, targetHeightAtStart);
// test connection changing on startup before wallet synced
if (testReconnectOnStartup) {
UserThread.runAfter(() -> {
log.warn("Testing connection change on startup before wallet synced");
if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2);
else xmrConnectionService.setConnection(testReconnectMonerod1);
}, 1);
testReconnectOnStartup = false; // only run once
}
// native wallet provides sync notifications
if (wallet instanceof MoneroWalletFull) {
if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
wallet.sync(new MoneroWalletListener() {
@Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
updateSyncProgress(height, appliedTargetHeight);
}
});
setWalletSyncedWithProgress();
return;
}
// start polling wallet for progress
syncProgressLatch = new CountDownLatch(1);
syncProgressLooper = new TaskLooper(() -> {
if (wallet == null) return;
long height;
try {
height = wallet.getHeight(); // can get read timeout while syncing
} catch (Exception e) {
log.warn("Error getting wallet height while syncing with progress: " + e.getMessage());
if (wallet != null && !isShutDownStarted) e.printStackTrace();
// stop polling and release latch
syncProgressError = e;
syncProgressLatch.countDown();
return;
}
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
updateSyncProgress(height, appliedTargetHeight);
if (height >= appliedTargetHeight) {
setWalletSyncedWithProgress();
syncProgressLatch.countDown();
}
});
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
syncProgressLooper.start(1000);
// wait for sync to complete
HavenoUtils.awaitLatch(syncProgressLatch);
// stop polling
syncProgressLooper.stop();
syncProgressTimeout.stop();
if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close
isSyncingWithProgress = false;
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
}
}
private void updateSyncProgress(long height, long targetHeight) {
resetSyncProgressTimeout();
UserThread.execute(() -> {
// set wallet height
walletHeight.set(height);
// new wallet reports height 1 before synced
if (height == 1) {
downloadListener.progress(0, targetHeight - height, null);
return;
}
// set progress
long blocksLeft = targetHeight - walletHeight.get();
if (syncStartHeight == null) syncStartHeight = walletHeight.get();
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight));
downloadListener.progress(percent, blocksLeft, null);
});
}
private synchronized void resetSyncProgressTimeout() {
if (syncProgressTimeout != null) syncProgressTimeout.stop();
syncProgressTimeout = UserThread.runAfter(() -> {
if (isShutDownStarted) return;
syncProgressError = new RuntimeException("Sync progress timeout called");
syncProgressLatch.countDown();
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
private void setWalletSyncedWithProgress() {
wasWalletSynced = true;
isSyncingWithProgress = false;
syncProgressTimeout.stop();
}
}

View File

@ -24,7 +24,6 @@ import com.google.inject.name.Named;
import common.utils.JsonUtils; import common.utils.JsonUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.file.FileUtil; import haveno.common.file.FileUtil;
@ -43,7 +42,6 @@ import haveno.core.user.User;
import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.listeners.XmrBalanceListener;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.model.XmrAddressEntryList; import haveno.core.xmr.model.XmrAddressEntryList;
import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.MoneroWalletRpcManager; import haveno.core.xmr.setup.MoneroWalletRpcManager;
import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.setup.WalletsSetup;
import java.io.File; import java.io.File;
@ -55,26 +53,22 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javafx.beans.property.LongProperty; import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import lombok.Getter;
import monero.common.MoneroError; import monero.common.MoneroError;
import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcConnection;
import monero.common.MoneroRpcError; import monero.common.MoneroRpcError;
@ -103,14 +97,13 @@ import monero.wallet.model.MoneroTxPriority;
import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import monero.wallet.model.MoneroWalletConfig; import monero.wallet.model.MoneroWalletConfig;
import monero.wallet.model.MoneroWalletListener;
import monero.wallet.model.MoneroWalletListenerI; import monero.wallet.model.MoneroWalletListenerI;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class XmrWalletService { public class XmrWalletService extends XmrWalletBase {
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class); private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
// monero configuration // monero configuration
@ -134,15 +127,13 @@ public class XmrWalletService {
private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5; private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5;
private static final long POLL_TXS_TOLERANCE_MS = 1000 * 60 * 3; // request connection switch if txs not updated within 3 minutes
private final User user; private final User user;
private final Preferences preferences; private final Preferences preferences;
private final CoreAccountService accountService; private final CoreAccountService accountService;
private final XmrConnectionService xmrConnectionService;
private final XmrAddressEntryList xmrAddressEntryList; private final XmrAddressEntryList xmrAddressEntryList;
private final WalletsSetup walletsSetup; private final WalletsSetup walletsSetup;
private final DownloadListener downloadListener = new DownloadListener();
private final LongProperty walletHeight = new SimpleLongProperty(0);
private final File walletDir; private final File walletDir;
private final File xmrWalletFile; private final File xmrWalletFile;
@ -153,24 +144,15 @@ public class XmrWalletService {
private ChangeListener<? super Number> walletInitListener; private ChangeListener<? super Number> walletInitListener;
private TradeManager tradeManager; private TradeManager tradeManager;
private MoneroWallet wallet;
public static final Object WALLET_LOCK = new Object();
private boolean wasWalletSynced;
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isClosingWallet;
private boolean isShutDownStarted;
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
private Long syncStartHeight;
private TaskLooper syncProgressLooper;
private CountDownLatch syncProgressLatch;
private Timer syncProgressTimeout;
private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 45;
// wallet polling and cache @Getter
public final Object lock = new Object();
private TaskLooper pollLooper; private TaskLooper pollLooper;
private boolean pollInProgress; private boolean pollInProgress;
private Long pollPeriodMs; private Long pollPeriodMs;
private Long lastLogPollErrorTimestamp; private long lastLogPollErrorTimestamp;
private long lastPollTxsTimestamp;
private final Object pollLock = new Object(); private final Object pollLock = new Object();
private Long cachedHeight; private Long cachedHeight;
private BigInteger cachedBalance; private BigInteger cachedBalance;
@ -178,7 +160,6 @@ public class XmrWalletService {
private List<MoneroSubaddress> cachedSubaddresses; private List<MoneroSubaddress> cachedSubaddresses;
private List<MoneroOutputWallet> cachedOutputs; private List<MoneroOutputWallet> cachedOutputs;
private List<MoneroTxWallet> cachedTxs; private List<MoneroTxWallet> cachedTxs;
private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Inject @Inject
@ -194,7 +175,6 @@ public class XmrWalletService {
this.user = user; this.user = user;
this.preferences = preferences; this.preferences = preferences;
this.accountService = accountService; this.accountService = accountService;
this.xmrConnectionService = xmrConnectionService;
this.walletsSetup = walletsSetup; this.walletsSetup = walletsSetup;
this.xmrAddressEntryList = xmrAddressEntryList; this.xmrAddressEntryList = xmrAddressEntryList;
this.walletDir = walletDir; this.walletDir = walletDir;
@ -202,6 +182,8 @@ public class XmrWalletService {
this.useNativeXmrWallet = useNativeXmrWallet; this.useNativeXmrWallet = useNativeXmrWallet;
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME); this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
HavenoUtils.xmrWalletService = this; HavenoUtils.xmrWalletService = this;
HavenoUtils.xmrConnectionService = xmrConnectionService;
this.xmrConnectionService = xmrConnectionService; // TODO: super's is null unless set here from injection
// set monero logging // set monero logging
if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL); if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
@ -316,10 +298,6 @@ public class XmrWalletService {
return xmrConnectionService.getDaemon(); return xmrConnectionService.getDaemon();
} }
public XmrConnectionService getConnectionService() {
return xmrConnectionService;
}
public boolean isProxyApplied() { public boolean isProxyApplied() {
return isProxyApplied(wasWalletSynced); return isProxyApplied(wasWalletSynced);
} }
@ -420,6 +398,10 @@ public class XmrWalletService {
} }
public void forceCloseWallet(MoneroWallet wallet, String path) { public void forceCloseWallet(MoneroWallet wallet, String path) {
if (wallet == null) {
log.warn("Ignoring force close wallet because wallet is null, path={}", path);
return;
}
if (wallet instanceof MoneroWalletRpc) { if (wallet instanceof MoneroWalletRpc) {
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true); MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true);
} else { } else {
@ -456,7 +438,7 @@ public class XmrWalletService {
} }
public MoneroTxWallet createTx(MoneroTxConfig txConfig) { public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
MoneroTxWallet tx = wallet.createTx(txConfig); MoneroTxWallet tx = wallet.createTx(txConfig);
if (Boolean.TRUE.equals(txConfig.getRelay())) { if (Boolean.TRUE.equals(txConfig.getRelay())) {
@ -469,6 +451,14 @@ public class XmrWalletService {
} }
} }
public String relayTx(String metadata) {
synchronized (walletLock) {
String txId = wallet.relayTx(metadata);
requestSaveMainWallet();
return txId;
}
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
//printTxs("XmrWalletService.createTx", tx); //printTxs("XmrWalletService.createTx", tx);
@ -479,7 +469,7 @@ public class XmrWalletService {
* Freeze reserved outputs and thaw unreserved outputs. * Freeze reserved outputs and thaw unreserved outputs.
*/ */
public void fixReservedOutputs() { public void fixReservedOutputs() {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// collect reserved outputs // collect reserved outputs
Set<String> reservedKeyImages = new HashSet<String>(); Set<String> reservedKeyImages = new HashSet<String>();
@ -498,7 +488,7 @@ public class XmrWalletService {
} }
private void freezeReservedOutputs(Set<String> reservedKeyImages) { private void freezeReservedOutputs(Set<String> reservedKeyImages) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// ensure wallet is open // ensure wallet is open
if (wallet == null) { if (wallet == null) {
@ -522,7 +512,7 @@ public class XmrWalletService {
} }
private void thawUnreservedOutputs(Set<String> reservedKeyImages) { private void thawUnreservedOutputs(Set<String> reservedKeyImages) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// ensure wallet is open // ensure wallet is open
if (wallet == null) { if (wallet == null) {
@ -552,7 +542,7 @@ public class XmrWalletService {
*/ */
public void freezeOutputs(Collection<String> keyImages) { public void freezeOutputs(Collection<String> keyImages) {
if (keyImages == null || keyImages.isEmpty()) return; if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// collect outputs to freeze // collect outputs to freeze
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream() List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
@ -574,7 +564,7 @@ public class XmrWalletService {
*/ */
public void thawOutputs(Collection<String> keyImages) { public void thawOutputs(Collection<String> keyImages) {
if (keyImages == null || keyImages.isEmpty()) return; if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// collect outputs to thaw // collect outputs to thaw
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream() List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
@ -627,7 +617,7 @@ public class XmrWalletService {
* @return the reserve tx * @return the reserve tx
*/ */
public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress); log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
@ -648,7 +638,7 @@ public class XmrWalletService {
* @return MoneroTxWallet the multisig deposit tx * @return MoneroTxWallet the multisig deposit tx
*/ */
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) { public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
String feeAddress = trade.getProcessModel().getTradeFeeAddress(); String feeAddress = trade.getProcessModel().getTradeFeeAddress();
@ -666,7 +656,7 @@ public class XmrWalletService {
} }
private MoneroTxWallet createTradeTx(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { private MoneroTxWallet createTradeTx(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
// create a list of subaddresses to attempt spending from in preferred order // create a list of subaddresses to attempt spending from in preferred order
@ -903,7 +893,7 @@ public class XmrWalletService {
Runnable shutDownTask = () -> { Runnable shutDownTask = () -> {
// remove listeners // remove listeners
synchronized (WALLET_LOCK) { synchronized (walletLock) {
if (wallet != null) { if (wallet != null) {
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) { for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
wallet.removeListener(listener); wallet.removeListener(listener);
@ -913,7 +903,7 @@ public class XmrWalletService {
} }
// shut down threads // shut down threads
synchronized (this) { synchronized (getLock()) {
List<Runnable> shutDownThreads = new ArrayList<>(); List<Runnable> shutDownThreads = new ArrayList<>();
shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID)); shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID));
ThreadUtils.awaitTasks(shutDownThreads); ThreadUtils.awaitTasks(shutDownThreads);
@ -1285,9 +1275,19 @@ public class XmrWalletService {
else log.info(appliedMsg); else log.info(appliedMsg);
// listen for connection changes // listen for connection changes
xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> { xmrConnectionService.addConnectionListener(connection -> {
if (wasWalletSynced && !isSyncingWithProgress) {
ThreadUtils.execute(() -> {
onConnectionChanged(connection); onConnectionChanged(connection);
}, THREAD_ID)); }, THREAD_ID);
} else {
// force restart main wallet if connection changed while syncing
log.warn("Force restarting main wallet because connection changed while syncing");
forceRestartMainWallet();
return;
}
});
// initialize main wallet when daemon synced // initialize main wallet when daemon synced
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
@ -1306,11 +1306,20 @@ public class XmrWalletService {
} }
private void maybeInitMainWallet(boolean sync, int numAttempts) { private void maybeInitMainWallet(boolean sync, int numAttempts) {
ThreadUtils.execute(() -> doMaybeInitMainWallet(sync, numAttempts), THREAD_ID); ThreadUtils.execute(() -> {
try {
doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
} catch (Exception e) {
log.warn("Error initializing main wallet: " + e.getMessage());
e.printStackTrace();
HavenoUtils.setTopError(e.getMessage());
throw e;
}
}, THREAD_ID);
} }
private void doMaybeInitMainWallet(boolean sync, int numAttempts) { private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
if (isShutDownStarted) return; if (isShutDownStarted) return;
// open or create wallet main wallet // open or create wallet main wallet
@ -1327,6 +1336,7 @@ public class XmrWalletService {
long date = localDateTime.toEpochSecond(ZoneOffset.UTC); long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
user.setWalletCreationDate(date); user.setWalletCreationDate(date);
} }
walletHeight.set(wallet.getHeight());
isClosingWallet = false; isClosingWallet = false;
} }
@ -1335,6 +1345,7 @@ public class XmrWalletService {
log.info("Monero wallet path={}", wallet.getPath()); log.info("Monero wallet path={}", wallet.getPath());
// sync main wallet if applicable // sync main wallet if applicable
// TODO: error handling and re-initialization is jenky, refactor
if (sync && numAttempts > 0) { if (sync && numAttempts > 0) {
try { try {
@ -1347,7 +1358,16 @@ public class XmrWalletService {
// sync main wallet // sync main wallet
log.info("Syncing main wallet"); log.info("Syncing main wallet");
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
syncWithProgress(); // blocking MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
syncWithProgress(true); // repeat sync to latest target height
} catch (Exception e) {
log.warn("Error syncing wallet with progress on startup: " + e.getMessage());
forceCloseMainWallet();
requestSwitchToNextBestConnection(sourceConnection);
maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again
return;
}
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
// poll wallet // poll wallet
@ -1410,7 +1430,7 @@ public class XmrWalletService {
if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) { if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) {
String warningMsg = "New Monero wallet detected. Resetting internal state."; String warningMsg = "New Monero wallet detected. Resetting internal state.";
if (!tradeManager.getOpenTrades().isEmpty()) warningMsg += "\n\nWARNING: Your open trades will settle to the payout address in the OLD wallet!"; // TODO: allow payout address to be updated in PaymentSentMessage, PaymentReceivedMessage, and DisputeOpenedMessage? if (!tradeManager.getOpenTrades().isEmpty()) warningMsg += "\n\nWARNING: Your open trades will settle to the payout address in the OLD wallet!"; // TODO: allow payout address to be updated in PaymentSentMessage, PaymentReceivedMessage, and DisputeOpenedMessage?
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg); HavenoUtils.setTopError(warningMsg);
// reset address entries // reset address entries
xmrAddressEntryList.clear(); xmrAddressEntryList.clear();
@ -1421,94 +1441,6 @@ public class XmrWalletService {
} }
} }
private void syncWithProgress() {
// start sync progress timeout
resetSyncProgressTimeout();
// show sync progress
updateSyncProgress(wallet.getHeight());
// test connection changing on startup before wallet synced
if (runReconnectTestOnStartup) {
UserThread.runAfter(() -> {
log.warn("Testing connection change on startup before wallet synced");
xmrConnectionService.setConnection("http://node.community.rino.io:18081"); // TODO: needs to be online
}, 1);
runReconnectTestOnStartup = false; // only run once
}
// get sync notifications from native wallet
if (wallet instanceof MoneroWalletFull) {
if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
wallet.sync(new MoneroWalletListener() {
@Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
updateSyncProgress(height);
}
});
wasWalletSynced = true;
return;
}
// poll wallet for progress
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
syncProgressLatch = new CountDownLatch(1);
syncProgressLooper = new TaskLooper(() -> {
if (wallet == null) return;
long height = 0;
try {
height = wallet.getHeight(); // can get read timeout while syncing
} catch (Exception e) {
if (!isShutDownStarted) e.printStackTrace();
return;
}
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
else {
syncProgressLooper.stop();
wasWalletSynced = true;
updateSyncProgress(height);
syncProgressLatch.countDown();
}
});
syncProgressLooper.start(1000);
HavenoUtils.awaitLatch(syncProgressLatch);
wallet.stopSyncing();
if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress");
}
private void updateSyncProgress(long height) {
UserThread.execute(() -> {
walletHeight.set(height);
resetSyncProgressTimeout();
// new wallet reports height 1 before synced
if (height == 1) {
downloadListener.progress(.0001, xmrConnectionService.getTargetHeight() - height, null); // >0% shows progress bar
return;
}
// set progress
long targetHeight = xmrConnectionService.getTargetHeight();
long blocksLeft = targetHeight - walletHeight.get();
if (syncStartHeight == null) syncStartHeight = walletHeight.get();
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, (double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress
downloadListener.progress(percent, blocksLeft, null);
});
}
private synchronized void resetSyncProgressTimeout() {
if (syncProgressTimeout != null) syncProgressTimeout.stop();
syncProgressTimeout = UserThread.runAfter(() -> {
if (isShutDownStarted || wasWalletSynced) return;
log.warn("Sync progress timeout called");
forceCloseMainWallet();
requestSwitchToNextBestConnection();
maybeInitMainWallet(true);
resetSyncProgressTimeout();
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
// must be connected to daemon // must be connected to daemon
@ -1529,7 +1461,7 @@ public class XmrWalletService {
return walletFull; return walletFull;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
if (walletFull != null) forceCloseMainWallet(); if (walletFull != null) forceCloseWallet(walletFull, config.getPath());
throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'"); throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'");
} }
} }
@ -1661,27 +1593,22 @@ public class XmrWalletService {
} }
private void onConnectionChanged(MoneroRpcConnection connection) { private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
// use current connection // use current connection
connection = xmrConnectionService.getConnection(); connection = xmrConnectionService.getConnection();
// check if ignored // check if ignored
if (wallet == null || isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); updatePollPeriod();
String newProxyUri = connection == null ? null : connection.getProxyUri();
log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri);
// force restart main wallet if connection changed before synced
if (!wasWalletSynced) {
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return;
log.warn("Force restarting main wallet because connection changed before inital sync");
forceRestartMainWallet();
return; return;
} }
// update connection // update connection
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
String newProxyUri = connection == null ? null : connection.getProxyUri();
log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri);
if (wallet instanceof MoneroWalletRpc) { if (wallet instanceof MoneroWalletRpc) {
if (StringUtils.equals(oldProxyUri, newProxyUri)) { if (StringUtils.equals(oldProxyUri, newProxyUri)) {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
@ -1689,7 +1616,7 @@ public class XmrWalletService {
log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet
closeMainWallet(true); closeMainWallet(true);
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS); doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
return; // wallet is re-initialized return; // wallet re-initializes off thread
} }
} else { } else {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
@ -1736,14 +1663,14 @@ public class XmrWalletService {
}); });
} }
// excute tasks in parallel // execute tasks in parallel
ThreadUtils.awaitTasks(tasks, Math.min(10, 1 + trades.size())); ThreadUtils.awaitTasks(tasks, Math.min(10, 1 + trades.size()));
log.info("Done changing all wallet passwords"); log.info("Done changing all wallet passwords");
} }
private void closeMainWallet(boolean save) { private void closeMainWallet(boolean save) {
stopPolling(); stopPolling();
synchronized (WALLET_LOCK) { synchronized (walletLock) {
try { try {
if (wallet != null) { if (wallet != null) {
isClosingWallet = true; isClosingWallet = true;
@ -1758,19 +1685,28 @@ public class XmrWalletService {
private void forceCloseMainWallet() { private void forceCloseMainWallet() {
stopPolling(); stopPolling();
if (wallet != null && !isClosingWallet) {
isClosingWallet = true; isClosingWallet = true;
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
wallet = null; wallet = null;
} }
}
private void forceRestartMainWallet() { public void forceRestartMainWallet() {
log.warn("Force restarting main wallet"); log.warn("Force restarting main wallet");
if (isClosingWallet) return;
forceCloseMainWallet(); forceCloseMainWallet();
maybeInitMainWallet(true); maybeInitMainWallet(true);
} }
public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
getWallet(); // re-open wallet
}
private void startPolling() { private void startPolling() {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
if (isShutDownStarted || isPolling()) return; if (isShutDownStarted || isPolling()) return;
updatePollPeriod(); updatePollPeriod();
pollLooper = new TaskLooper(() -> pollWallet()); pollLooper = new TaskLooper(() -> pollWallet());
@ -1791,15 +1727,15 @@ public class XmrWalletService {
public void updatePollPeriod() { public void updatePollPeriod() {
if (isShutDownStarted) return; if (isShutDownStarted) return;
setPollPeriod(getPollPeriod()); setPollPeriodMs(getPollPeriodMs());
} }
private long getPollPeriod() { private long getPollPeriodMs() {
return xmrConnectionService.getRefreshPeriodMs(); return xmrConnectionService.getRefreshPeriodMs();
} }
private void setPollPeriod(long pollPeriodMs) { private void setPollPeriodMs(long pollPeriodMs) {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
if (this.isShutDownStarted) return; if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
this.pollPeriodMs = pollPeriodMs; this.pollPeriodMs = pollPeriodMs;
@ -1834,32 +1770,36 @@ public class XmrWalletService {
log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight());
return; return;
} }
// switch to best connection if wallet is too far behind
if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) {
log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight());
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
}
// sync wallet if behind daemon // sync wallet if behind daemon
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations synchronized (walletLock) { // avoid long sync from blocking other operations
// TODO: local tests have timing failures unless sync called directly
if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) {
syncMainWallet(); syncMainWallet();
} else {
syncWithProgress();
}
} }
} }
// fetch transactions from pool and store to cache // fetch transactions from pool and store to cache
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
if (updateTxs) { if (updateTxs) {
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations synchronized (walletLock) { // avoid long fetch from blocking other operations
synchronized (HavenoUtils.getDaemonLock()) { synchronized (HavenoUtils.getDaemonLock()) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
lastPollTxsTimestamp = System.currentTimeMillis();
} catch (Exception e) { // fetch from pool can fail } catch (Exception e) { // fetch from pool can fail
if (!isShutDownStarted) { if (!isShutDownStarted) {
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging
// throttle error handling
if (System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
lastLogPollErrorTimestamp = System.currentTimeMillis(); lastLogPollErrorTimestamp = System.currentTimeMillis();
if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection);
} }
} }
} }
@ -1868,33 +1808,32 @@ public class XmrWalletService {
} }
} catch (Exception e) { } catch (Exception e) {
if (wallet == null || isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet();
if (isConnectionRefused) forceRestartMainWallet();
else if (isWalletConnectedToDaemon()) { else if (isWalletConnectedToDaemon()) {
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getXmrConnectionService().getConnection());
//e.printStackTrace(); //e.printStackTrace();
} }
} finally { } finally {
synchronized (pollLock) {
pollInProgress = false;
}
// cache wallet info last // cache wallet info last
synchronized (WALLET_LOCK) { synchronized (walletLock) {
if (wallet != null && !isShutDownStarted) { if (wallet != null && !isShutDownStarted) {
try { try {
cacheWalletInfo(); cacheWalletInfo();
requestSaveMainWallet();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
synchronized (pollLock) {
pollInProgress = false;
}
} }
} }
private MoneroSyncResult syncMainWallet() { private MoneroSyncResult syncMainWallet() {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
MoneroSyncResult result = syncWallet(wallet); MoneroSyncResult result = syncWallet(wallet);
walletHeight.set(wallet.getHeight()); walletHeight.set(wallet.getHeight());
return result; return result;
@ -1902,7 +1841,7 @@ public class XmrWalletService {
} }
public boolean isWalletConnectedToDaemon() { public boolean isWalletConnectedToDaemon() {
synchronized (WALLET_LOCK) { synchronized (walletLock) {
try { try {
if (wallet == null) return false; if (wallet == null) return false;
return wallet.isConnectedToDaemon(); return wallet.isConnectedToDaemon();
@ -1912,8 +1851,12 @@ public class XmrWalletService {
} }
} }
public boolean requestSwitchToNextBestConnection() { private boolean requestSwitchToNextBestConnection() {
return xmrConnectionService.requestSwitchToNextBestConnection(); return requestSwitchToNextBestConnection(null);
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
} }
private void onNewBlock(long height) { private void onNewBlock(long height) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

View File

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html --> <!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.9</string> <string>1.0.10</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.9</string> <string>1.0.10</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Haveno</string> <string>Haveno</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

@ -197,7 +197,7 @@ public class TxIdTextField extends AnchorPane {
try { try {
if (trade == null) { if (trade == null) {
tx = useCache ? xmrWalletService.getDaemonTxWithCache(txId) : xmrWalletService.getDaemonTx(txId); tx = useCache ? xmrWalletService.getDaemonTxWithCache(txId) : xmrWalletService.getDaemonTx(txId);
tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getConnectionService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getXmrConnectionService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet
} else { } else {
if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx(); if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx();
else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx(); else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx();

View File

@ -1201,16 +1201,16 @@ textfield */
} }
.text-area-no-border { .text-area-no-border {
-fx-border-color: -fx-control-inner-background; -fx-border-color: -bs-background-color;
} }
.text-area-no-border .content { .text-area-no-border .content {
-fx-background-color: -fx-control-inner-background; -fx-background-color: -bs-background-color;
} }
.text-area-no-border:focused { .text-area-no-border:focused {
-fx-focus-color: -fx-control-inner-background; -fx-focus-color: -bs-background-color;
-fx-faint-focus-color: -fx-control-inner-background; -fx-faint-focus-color: -bs-background-color;
} }
/******************************************************************************* /*******************************************************************************

View File

@ -72,6 +72,7 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.util.FormattingUtils; import haveno.core.util.FormattingUtils;
import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinFormatter;
import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.FxmlView;
import haveno.desktop.components.AutocompleteComboBox;
import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.components.paymentmethods.AchTransferForm; import haveno.desktop.components.paymentmethods.AchTransferForm;
import haveno.desktop.components.paymentmethods.AdvancedCashForm; import haveno.desktop.components.paymentmethods.AdvancedCashForm;
@ -144,7 +145,6 @@ import java.util.stream.Collectors;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@ -181,7 +181,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
private final AdvancedCashValidator advancedCashValidator; private final AdvancedCashValidator advancedCashValidator;
private final TransferwiseValidator transferwiseValidator; private final TransferwiseValidator transferwiseValidator;
private final CoinFormatter formatter; private final CoinFormatter formatter;
private ComboBox<PaymentMethod> paymentMethodComboBox; private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private PaymentMethodForm paymentMethodForm; private PaymentMethodForm paymentMethodForm;
private TitledGroupBg accountTitledGroupBg; private TitledGroupBg accountTitledGroupBg;
private Button saveNewAccountButton; private Button saveNewAccountButton;
@ -463,14 +463,16 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
removeAccountRows(); removeAccountRows();
addAccountButton.setDisable(true); addAccountButton.setDisable(true);
accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE);
paymentMethodComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); paymentMethodComboBox = FormBuilder.addAutocompleteComboBox(
paymentMethodComboBox.setVisibleRowCount(11); root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE
);
paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
paymentMethodComboBox.setPrefWidth(250); paymentMethodComboBox.setPrefWidth(250);
List<PaymentMethod> list = PaymentMethod.paymentMethods.stream() List<PaymentMethod> list = PaymentMethod.paymentMethods.stream()
.filter(PaymentMethod::isTraditional) .filter(PaymentMethod::isTraditional)
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
paymentMethodComboBox.setItems(FXCollections.observableArrayList(list)); paymentMethodComboBox.setAutocompleteItems(FXCollections.observableArrayList(list));
paymentMethodComboBox.setConverter(new StringConverter<>() { paymentMethodComboBox.setConverter(new StringConverter<>() {
@Override @Override
public String toString(PaymentMethod paymentMethod) { public String toString(PaymentMethod paymentMethod) {
@ -479,10 +481,15 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
@Override @Override
public PaymentMethod fromString(String s) { public PaymentMethod fromString(String s) {
if (s.isEmpty())
return null; return null;
return paymentMethodComboBox.getItems().stream()
.filter(item -> Res.get(item.getId()).equals(s))
.findAny().orElse(null);
} }
}); });
paymentMethodComboBox.setOnAction(e -> { paymentMethodComboBox.setOnChangeConfirmed(e -> {
if (paymentMethodForm != null) { if (paymentMethodForm != null) {
FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1);
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
@ -550,6 +557,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
} }
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) {
if (paymentMethod == null) return null;
final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
paymentAccount.init(); paymentAccount.init();
return getPaymentMethodForm(paymentMethod, paymentAccount); return getPaymentMethodForm(paymentMethod, paymentAccount);

View File

@ -65,6 +65,7 @@ import javafx.scene.control.Button;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils; import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -256,6 +257,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// create tx // create tx
MoneroTxWallet tx = null; MoneroTxWallet tx = null;
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrWalletService.getXmrConnectionService().getConnection();
try { try {
log.info("Creating withdraw tx"); log.info("Creating withdraw tx");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -270,7 +272,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (isNotEnoughMoney(e.getMessage())) throw e; if (isNotEnoughMoney(e.getMessage())) throw e;
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); if (xmrWalletService.getXmrConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection);
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }

View File

@ -594,7 +594,11 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
// show confirmation // show confirmation
showPayoutTxConfirmation(contract, showPayoutTxConfirmation(contract,
payoutTx, payoutTx,
() -> doClose(closeTicketButton, cancelButton)); () -> doClose(closeTicketButton, cancelButton),
() -> {
closeTicketButton.setDisable(false);
cancelButton.setDisable(false);
});
} else { } else {
doClose(closeTicketButton, cancelButton); doClose(closeTicketButton, cancelButton);
} }
@ -607,7 +611,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
}); });
} }
private void showPayoutTxConfirmation(Contract contract, MoneroTxWallet payoutTx, ResultHandler resultHandler) { private void showPayoutTxConfirmation(Contract contract, MoneroTxWallet payoutTx, ResultHandler resultHandler, ResultHandler cancelHandler) {
// get buyer and seller destinations (order not preserved) // get buyer and seller destinations (order not preserved)
String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); String buyerPayoutAddressString = contract.getBuyerPayoutAddressString();
@ -641,6 +645,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
.actionButtonText(Res.get("shared.yes")) .actionButtonText(Res.get("shared.yes"))
.onAction(() -> resultHandler.handleResult()) .onAction(() -> resultHandler.handleResult())
.closeButtonText(Res.get("shared.cancel")) .closeButtonText(Res.get("shared.cancel"))
.onClose(() -> cancelHandler.handleResult())
.show(); .show();
} else { } else {
// No payout will be made // No payout will be made
@ -649,6 +654,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
.actionButtonText(Res.get("shared.yes")) .actionButtonText(Res.get("shared.yes"))
.onAction(resultHandler::handleResult) .onAction(resultHandler::handleResult)
.closeButtonText(Res.get("shared.cancel")) .closeButtonText(Res.get("shared.cancel"))
.onClose(() -> cancelHandler.handleResult())
.show(); .show();
} }
} }

View File

@ -37,7 +37,6 @@ import haveno.core.offer.OfferUtil;
import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.support.SupportType; import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeAlreadyOpenException;
import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeList;
import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.ArbitrationManager;
@ -67,6 +66,7 @@ import haveno.network.p2p.P2PService;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@ -545,32 +545,38 @@ public class PendingTradesDataModel extends ActivatableDataModel {
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex()); sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} else if (useArbitration) { } else if (useArbitration) {
// Only if we have completed mediation we allow arbitration // Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager; disputeManager = arbitrationManager;
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex()); trade.exportMultisigHex();
sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} else { } else {
log.warn("Invalid dispute state {}", disputeState.name()); log.warn("Invalid dispute state {}", disputeState.name());
} }
} }
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) { private void sendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex, Optional<Dispute> optionalDispute = disputeManager.findDispute(dispute);
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> { boolean disputeClosed = optionalDispute.isPresent() && optionalDispute.get().isClosed();
if ((throwable instanceof DisputeAlreadyOpenException)) { if (disputeClosed) {
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); String msg = "We got a dispute already open for that trade and trading peer.\n" + "TradeId = " + dispute.getTradeId();
new Popup().warning(errorMessage) new Popup().warning(msg + "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"))
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex)) .onAction(() -> doSendDisputeOpenedMessage(dispute, disputeManager))
.closeButtonText(Res.get("shared.cancel")).show(); .closeButtonText(Res.get("shared.cancel")).show();
} else { } else {
new Popup().warning(errorMessage).show(); doSendDisputeOpenedMessage(dispute, disputeManager);
} }
}); }
private void doSendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
disputeManager.sendDisputeOpenedMessage(dispute,
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class),
(errorMessage, throwable) -> new Popup().warning(errorMessage).show());
} }
public boolean isReadyForTxBroadcast() { public boolean isReadyForTxBroadcast() {

View File

@ -190,15 +190,13 @@ public abstract class TradeStepView extends AnchorPane {
} }
trade.errorMessageProperty().addListener(errorMessageListener); trade.errorMessageProperty().addListener(errorMessageListener);
if (!isMediationClosedState()) {
tradeStepInfo.setOnAction(e -> { tradeStepInfo.setOnAction(e -> {
if (this.isTradePeriodOver()) { if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
openSupportTicket(); openSupportTicket();
} else { } else {
openChat(); openChat();
} }
}); });
}
// We get mailbox messages processed after we have bootstrapped. This will influence the states we // We get mailbox messages processed after we have bootstrapped. This will influence the states we
// handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show // handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show
@ -572,7 +570,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) { private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
if (isInArbitration()) { if (isInMediation()) {
if (isRefundRequestStartedByPeer()) { if (isRefundRequestStartedByPeer()) {
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
} else if (isRefundRequestSelfStarted()) { } else if (isRefundRequestSelfStarted()) {
@ -597,7 +595,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
} }
private boolean isInArbitration() { private boolean isInMediation() {
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted(); return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
} }
@ -613,6 +611,10 @@ public abstract class TradeStepView extends AnchorPane {
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED; return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
} }
private boolean isArbitrationOpenedState() {
return trade.getDisputeState().isOpen();
}
private boolean isTradePeriodOver() { private boolean isTradePeriodOver() {
return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get(); return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get();
} }
@ -741,7 +743,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) { if (!trade.getDisputeState().isOpen()) {
switch (tradePeriodState) { switch (tradePeriodState) {
case FIRST_HALF: case FIRST_HALF:
// just for dev testing. not possible to go back in time ;-) // just for dev testing. not possible to go back in time ;-)

View File

@ -1485,7 +1485,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
@Override @Override
public void onCloseDisputeFromChatWindow(Dispute dispute) { public void onCloseDisputeFromChatWindow(Dispute dispute) {
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) { if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState().isOpen()) {
handleOnProcessDispute(dispute); handleOnProcessDispute(dispute);
} else { } else {
closeDisputeFromButton(); closeDisputeFromButton();

View File

@ -131,7 +131,7 @@
-bs-red-soft: derive(-bs-rd-error-red, 60%); -bs-red-soft: derive(-bs-rd-error-red, 60%);
-bs-progress-bar-track: #272728; -bs-progress-bar-track: #272728;
-bs-chart-tick: rgba(255, 255, 255, 0.7); -bs-chart-tick: rgba(255, 255, 255, 0.7);
-bs-chart-lines: rgba(0, 0, 0, 0.3); -bs-chart-lines: -bs-color-gray-2;
-bs-white: white; -bs-white: white;
-bs-prompt-text: -bs-color-gray-6; -bs-prompt-text: -bs-color-gray-6;
-bs-decimals: #db6300; -bs-decimals: #db6300;

View File

@ -1405,6 +1405,24 @@ public class FormBuilder {
return comboBox; return comboBox;
} }
public static <T> AutocompleteComboBox<T> addAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) {
var comboBox = new AutocompleteComboBox<T>();
comboBox.setLabelFloat(true);
comboBox.setPromptText(title);
comboBox.setMaxWidth(Double.MAX_VALUE);
// Default ComboBox does not show promptText after clear selection.
// https://stackoverflow.com/questions/50569330/how-to-reset-combobox-and-display-prompttext?noredirect=1&lq=1
comboBox.setButtonCell(getComboBoxButtonCell(title, comboBox));
GridPane.setRowIndex(comboBox, rowIndex);
GridPane.setColumnIndex(comboBox, 0);
GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0));
gridPane.getChildren().add(comboBox);
return comboBox;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Label + AutocompleteComboBox // Label + AutocompleteComboBox
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -701,7 +701,7 @@ public class GUIUtil {
} }
public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) { public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) {
XmrConnectionService xmrConnectionService = xmrWalletService.getConnectionService(); XmrConnectionService xmrConnectionService = xmrWalletService.getXmrConnectionService();
if (!xmrConnectionService.hasSufficientPeersForBroadcast()) { if (!xmrConnectionService.hasSufficientPeersForBroadcast()) {
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show(); new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show();
return false; return false;

View File

@ -79,17 +79,60 @@ Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as
## Add seed nodes ## Add seed nodes
### Seed nodes without Proof of Work (PoW)
> [!note]
> Using PoW is suggested. See next section for PoW setup.
For each seed node: For each seed node:
1. [Build the Haveno repository](#fork-and-build-haveno). 1. [Build the Haveno repository](#fork-and-build-haveno).
2. [Start a local Monero node](#start-a-local-monero-node). 2. [Start a local Monero node](#start-a-local-monero-node).
3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed. 3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`). 4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`).
5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and coppied haveno-seednode2.service to your systemd folder. 5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder.
6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given. 6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given.
7. Add the `.onion` address to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly. 7. Add the `.onion` address to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly.
8. Update all seed nodes, arbitrators, and user applications for the change to take effect. 8. Update all seed nodes, arbitrators, and user applications for the change to take effect.
### Seed nodes with Proof of Work (PoW)
> [!note]
> These instructions were written for Ubuntu with an Intel/AMD 64-bit CPU so changes may be needed for your distribution.
### Install Tor
Source: [Tor Project Support](https://support.torproject.org/apt/)
1. Verify architecture `sudo dpkg --print-architecture`.
2. Create sources.list file `sudo nano /etc/apt/sources.list.d/tor.list`.
3. Paste `deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org <DISTRIBUTION> main`.
4. Paste `deb-src [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org <DISTRIBUTION> main`.
> [!note]
> Replace `<DISTRIBUTION>` with your system codename such as "jammy" for Ubuntu 22.04.
5. Press Ctrl+X, then "y", then the enter key.
6. Add the gpg key used to sign the packages `sudo wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null`.
7. Update repositories `sudo apt update`.
8. Install tor and tor debian keyring `sudo apt install tor deb.torproject.org-keyring`.
9. Replace torrc `sudo mv /etc/tor/torrc /etc/tor/torrc.default` then `sudo cp seednode/torrc /etc/tor/torrc`.
10. Stop tor `sudo systemctl stop tor`.
For each seed node:
1. [Build the Haveno repository](#fork-and-build-haveno).
2. [Start a local Monero node](#start-a-local-monero-node).
3. Run `sudo cat /var/lib/tor/haveno_seednode/hostname` and note down the .onion for the next step & step 10.
4. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
5. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`).
6. Add user to tor group `sudo usermod -aG debian-tor <user>`.
> [!note]
> Replace `<user>` above with the user that will be running the seed node (step 6 above & step 4)
7. Disconnect and reconnect SSH session or logout and back in.
8. Run `sudo systemctl start tor`.
9. Run `sudo systemctl start haveno-seednode` to start the seednode and also run `sudo systemctl start haveno-seednode2` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder.
10. Add the `.onion` address from step 3 to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:2002)`. Be careful to record full addresses correctly.
11. Update all seed nodes, arbitrators, and user applications for the change to take effect.
Customize and deploy haveno-seednode.service to run a seed node as a system service. Customize and deploy haveno-seednode.service to run a seed node as a system service.
Each seed node requires a locally running Monero node. You can use the default port or configure it manually with `--xmrNode`, `--xmrNodeUsername`, and `--xmrNodePassword`. Each seed node requires a locally running Monero node. You can use the default port or configure it manually with `--xmrNode`, `--xmrNodeUsername`, and `--xmrNodePassword`.

View File

@ -28,8 +28,10 @@ import haveno.network.p2p.network.LocalhostNetworkNode;
import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.NetworkNode;
import haveno.network.p2p.network.NewTor; import haveno.network.p2p.network.NewTor;
import haveno.network.p2p.network.RunningTor; import haveno.network.p2p.network.RunningTor;
import haveno.network.p2p.network.DirectBindTor;
import haveno.network.p2p.network.TorMode; import haveno.network.p2p.network.TorMode;
import haveno.network.p2p.network.TorNetworkNode; import haveno.network.p2p.network.TorNetworkNodeDirectBind;
import haveno.network.p2p.network.TorNetworkNodeNetlayer;
import java.io.File; import java.io.File;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
@Named(Config.MAX_CONNECTIONS) int maxConnections, @Named(Config.MAX_CONNECTIONS) int maxConnections,
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P, @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
@Named(Config.NODE_PORT) int port, @Named(Config.NODE_PORT) int port,
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
@Named(Config.TOR_DIR) File torDir, @Named(Config.TOR_DIR) File torDir,
@Nullable @Named(Config.TORRC_FILE) File torrcFile, @Nullable @Named(Config.TORRC_FILE) File torrcFile,
@Named(Config.TORRC_OPTIONS) String torrcOptions, @Named(Config.TORRC_OPTIONS) String torrcOptions,
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
torrcOptions, torrcOptions,
controlHost, controlHost,
controlPort, controlPort,
hiddenServiceAddress,
password, password,
cookieFile, cookieFile,
useSafeCookieAuthentication); useSafeCookieAuthentication);
networkNode = new TorNetworkNode(port, networkProtoResolver, streamIsolation, torMode, banFilter, maxConnections, controlHost); if (torMode instanceof NewTor || torMode instanceof RunningTor) {
networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost);
} else {
networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress);
}
} }
} }
@ -75,12 +83,17 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
String torrcOptions, String torrcOptions,
String controlHost, String controlHost,
int controlPort, int controlPort,
String hiddenServiceAddress,
String password, String password,
@Nullable File cookieFile, @Nullable File cookieFile,
boolean useSafeCookieAuthentication) { boolean useSafeCookieAuthentication) {
return controlPort != Config.UNSPECIFIED_PORT ? if (!hiddenServiceAddress.equals("")) {
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) : return new DirectBindTor();
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider); } else if (controlPort != Config.UNSPECIFIED_PORT) {
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
} else {
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
}
} }
@Override @Override

View File

@ -26,6 +26,7 @@ import haveno.common.config.Config;
import static haveno.common.config.Config.BAN_LIST; import static haveno.common.config.Config.BAN_LIST;
import static haveno.common.config.Config.MAX_CONNECTIONS; import static haveno.common.config.Config.MAX_CONNECTIONS;
import static haveno.common.config.Config.NODE_PORT; import static haveno.common.config.Config.NODE_PORT;
import static haveno.common.config.Config.HIDDEN_SERVICE_ADDRESS;
import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES; import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES;
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS; import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS; import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
@ -87,6 +88,7 @@ public class P2PModule extends AppModule {
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir); bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);
bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort); bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort);
bind(String.class).annotatedWith(named(HIDDEN_SERVICE_ADDRESS)).toInstance(config.hiddenServiceAddress);
bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections); bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections);

View File

@ -171,9 +171,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private final Capabilities capabilities = new Capabilities(); private final Capabilities capabilities = new Capabilities();
// throttle logs of reported invalid requests // throttle logs of reported invalid requests
private static long lastLoggedInvalidRequestReport = 0; private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds
private static int unloggedInvalidRequestReports = 0; private static long lastLoggedInvalidRequestReportTs = 0;
private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s private static int numUnloggedInvalidRequestReports = 0;
private static long lastLoggedWarningTs = 0;
private static int numUnloggedWarnings = 0;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
@ -218,8 +220,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (peersNodeAddress != null) { if (peersNodeAddress != null) {
setPeersNodeAddress(peersNodeAddress); setPeersNodeAddress(peersNodeAddress);
if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) { if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) {
log.warn("We created an outbound connection with a banned peer"); reportInvalidRequest(RuleViolation.PEER_BANNED, "We created an outbound connection with a banned peer");
reportInvalidRequest(RuleViolation.PEER_BANNED);
} }
} }
ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID); ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID);
@ -249,8 +250,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (banFilter != null && if (banFilter != null &&
peersNodeAddressOptional.isPresent() && peersNodeAddressOptional.isPresent() &&
banFilter.isPeerBanned(peersNodeAddressOptional.get())) { banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
log.warn("We tried to send a message to a banned peer. message={}", networkEnvelope.getClass().getSimpleName()); String errorMessage = "We tried to send a message to a banned peer. message=" + networkEnvelope.getClass().getSimpleName();
reportInvalidRequest(RuleViolation.PEER_BANNED); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return; return;
} }
@ -419,7 +420,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (networkEnvelope instanceof SendersNodeAddressMessage) { if (networkEnvelope instanceof SendersNodeAddressMessage) {
boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope); boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope);
if (!isValid) { if (!isValid) {
log.warn("Received an invalid {} at processing BundleOfEnvelopes", networkEnvelope.getClass().getSimpleName()); throttleWarn("Received an invalid " + networkEnvelope.getClass().getSimpleName() + " at processing BundleOfEnvelopes");
continue; continue;
} }
} }
@ -610,20 +611,20 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
* Runs in same thread as Connection * Runs in same thread as Connection
*/ */
public boolean reportInvalidRequest(RuleViolation ruleViolation) { public boolean reportInvalidRequest(RuleViolation ruleViolation, String errorMessage) {
return Connection.reportInvalidRequest(this, ruleViolation); return Connection.reportInvalidRequest(this, ruleViolation, errorMessage);
} }
private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation) { private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation, String errorMessage) {
// determine if report should be logged to avoid spamming the logs // determine if report should be logged to avoid spamming the logs
boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReport > LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS; boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReportTs > LOG_THROTTLE_INTERVAL_MS;
// count the number of unlogged reports since last log entry // count the number of unlogged reports since last log entry
if (!logReport) unloggedInvalidRequestReports++; if (!logReport) numUnloggedInvalidRequestReports++;
// handle report // handle report
if (logReport) log.info("We got reported the ruleViolation {} at connection with address {} and uid {}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid()); if (logReport) log.warn("We got reported the ruleViolation {} at connection with address={}, uid={}, errorMessage={}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid(), errorMessage);
int numRuleViolations; int numRuleViolations;
numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0); numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
numRuleViolations++; numRuleViolations++;
@ -654,9 +655,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) {
if (logReport) { if (logReport) {
if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports); if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports);
unloggedInvalidRequestReports = 0; numUnloggedInvalidRequestReports = 0;
lastLoggedInvalidRequestReport = System.currentTimeMillis(); lastLoggedInvalidRequestReportTs = System.currentTimeMillis();
} }
} }
@ -673,25 +674,22 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
else else
closeConnectionReason = CloseConnectionReason.RESET; closeConnectionReason = CloseConnectionReason.RESET;
log.info("SocketException (expected if connection lost). closeConnectionReason={}; connection={}", closeConnectionReason, this); throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this);
} else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { } else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) {
closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT; closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT;
log.info("Shut down caused by exception {} on connection={}", e, this); throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
} else if (e instanceof EOFException) { } else if (e instanceof EOFException) {
closeConnectionReason = CloseConnectionReason.TERMINATED; closeConnectionReason = CloseConnectionReason.TERMINATED;
log.warn("Shut down caused by exception {} on connection={}", e, this); throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
} else if (e instanceof OptionalDataException || e instanceof StreamCorruptedException) { } else if (e instanceof OptionalDataException || e instanceof StreamCorruptedException) {
closeConnectionReason = CloseConnectionReason.CORRUPTED_DATA; closeConnectionReason = CloseConnectionReason.CORRUPTED_DATA;
log.warn("Shut down caused by exception {} on connection={}", e, this); throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
} else { } else {
// TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException // TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException
closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION; closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION;
log.warn("Unknown reason for exception at socket: {}\n\t" + throttleWarn("Unknown reason for exception at socket: " + socket.toString() + "\n\t" +
"peer={}\n\t" + "peer=" + this.peersNodeAddressOptional + "\n\t" +
"Exception={}", "Exception=" + e.toString());
socket.toString(),
this.peersNodeAddressOptional,
e.toString());
} }
shutDown(closeConnectionReason); shutDown(closeConnectionReason);
} }
@ -712,8 +710,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
} }
if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) { if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) {
log.warn("We got a message from a banned peer. message={}", sendersNodeAddressMessage.getClass().getSimpleName()); String errorMessage = "We got a message from a banned peer. message=" + sendersNodeAddressMessage.getClass().getSimpleName();
reportInvalidRequest(RuleViolation.PEER_BANNED); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return false; return false;
} }
@ -745,7 +743,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
try { try {
if (socket != null && if (socket != null &&
socket.isClosed()) { socket.isClosed()) {
log.warn("Socket is null or closed socket={}", socket); throttleWarn("Socket is null or closed socket=" + socket);
shutDown(CloseConnectionReason.SOCKET_CLOSED); shutDown(CloseConnectionReason.SOCKET_CLOSED);
return; return;
} }
@ -757,7 +755,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (socket != null && if (socket != null &&
socket.isClosed()) { socket.isClosed()) {
log.warn("Socket is null or closed socket={}", socket); throttleWarn("Socket is null or closed socket=" + socket);
shutDown(CloseConnectionReason.SOCKET_CLOSED); shutDown(CloseConnectionReason.SOCKET_CLOSED);
return; return;
} }
@ -767,9 +765,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
return; return;
} }
if (protoInputStream.read() == -1) { if (protoInputStream.read() == -1) {
log.warn("proto is null because protoInputStream.read()=-1 (EOF). That is expected if client got stopped without proper shutdown."); throttleWarn("proto is null because protoInputStream.read()=-1 (EOF). That is expected if client got stopped without proper shutdown.");
} else { } else {
log.warn("proto is null. protoInputStream.read()=" + protoInputStream.read()); throttleWarn("proto is null. protoInputStream.read()=" + protoInputStream.read());
} }
shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV); shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV);
return; return;
@ -778,8 +776,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (banFilter != null && if (banFilter != null &&
peersNodeAddressOptional.isPresent() && peersNodeAddressOptional.isPresent() &&
banFilter.isPeerBanned(peersNodeAddressOptional.get())) { banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
log.warn("We got a message from a banned peer. proto={}", Utilities.toTruncatedString(proto)); String errorMessage = "We got a message from a banned peer. proto=" + Utilities.toTruncatedString(proto);
reportInvalidRequest(RuleViolation.PEER_BANNED); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return; return;
} }
@ -814,30 +812,28 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage && if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage &&
!((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) { !((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) {
log.warn("PersistableNetworkPayload.verifyHashSize failed. hashSize={}; object={}", String errorMessage = "PersistableNetworkPayload.verifyHashSize failed. hashSize=" +
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length, ((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length + "; object=" +
Utilities.toTruncatedString(proto)); Utilities.toTruncatedString(proto);
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED)) if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
return; return;
} }
if (exceeds) { if (exceeds) {
log.warn("size > MAX_MSG_SIZE. size={}; object={}", size, Utilities.toTruncatedString(proto)); String errorMessage = "size > MAX_MSG_SIZE. size=" + size + "; object=" + Utilities.toTruncatedString(proto);
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
return; return;
} }
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED)) if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED, "Violates throttle limit"))
return; return;
// Check P2P network ID // Check P2P network ID
String errorMessage = "RuleViolation.WRONG_NETWORK_ID. version of message=" + proto.getMessageVersion() +
", app version=" + Version.getP2PMessageVersion() +
", proto.toTruncatedString=" + Utilities.toTruncatedString(proto.toString());
if (!proto.getMessageVersion().equals(Version.getP2PMessageVersion()) if (!proto.getMessageVersion().equals(Version.getP2PMessageVersion())
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) { && reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID, errorMessage)) {
log.warn("RuleViolation.WRONG_NETWORK_ID. version of message={}, app version={}, " +
"proto.toTruncatedString={}", proto.getMessageVersion(),
Version.getP2PMessageVersion(),
Utilities.toTruncatedString(proto.toString()));
return; return;
} }
@ -879,12 +875,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID); ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID);
} }
} catch (InvalidClassException e) { } catch (InvalidClassException e) {
log.error(e.getMessage()); reportInvalidRequest(RuleViolation.INVALID_CLASS, e.getMessage());
e.printStackTrace();
reportInvalidRequest(RuleViolation.INVALID_CLASS);
} catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) { } catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) {
log.error(e.getMessage()); reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE, e.getMessage());
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
} catch (Throwable t) { } catch (Throwable t) {
handleException(t); handleException(t);
} }
@ -943,4 +936,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope); NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope);
return nodeAddress == null ? "null" : nodeAddress.getFullAddress(); return nodeAddress == null ? "null" : nodeAddress.getFullAddress();
} }
private synchronized void throttleWarn(String msg) {
boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS;
if (logWarning) {
log.warn(msg);
if (numUnloggedWarnings > 0) log.warn("We received {} other log warnings since the last log entry", numUnloggedWarnings);
numUnloggedWarnings = 0;
lastLoggedWarningTs = System.currentTimeMillis();
} else {
numUnloggedWarnings++;
}
}
} }

View File

@ -0,0 +1,22 @@
package haveno.network.p2p.network;
import lombok.extern.slf4j.Slf4j;
import org.berndpruenster.netlayer.tor.Tor;
@Slf4j
public class DirectBindTor extends TorMode {
public DirectBindTor() {
super(null);
}
@Override
public Tor getTor() {
return null;
}
@Override
public String getHiddenServiceDirectory() {
return null;
}
}

View File

@ -18,50 +18,26 @@
package haveno.network.p2p.network; package haveno.network.p2p.network;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import haveno.network.utils.Utils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.network.NetworkProtoResolver;
import haveno.common.util.SingleThreadExecutorUtils; import haveno.common.util.SingleThreadExecutorUtils;
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.TorSocket;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import java.security.SecureRandom;
import java.net.Socket; import java.net.Socket;
import java.io.IOException; import java.io.IOException;
import java.util.Base64;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j @Slf4j
public class TorNetworkNode extends NetworkNode { public abstract class TorNetworkNode extends NetworkNode {
private static final long SHUT_DOWN_TIMEOUT = 2;
private final String torControlHost; protected final ExecutorService executor;
private HiddenServiceSocket hiddenServiceSocket;
private Timer shutDownTimeoutTimer;
private Tor tor;
private TorMode torMode;
private boolean streamIsolation;
private Socks5Proxy socksProxy;
private boolean shutDownInProgress;
private boolean shutDownComplete;
private final ExecutorService executor;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
public TorNetworkNode(int servicePort, public TorNetworkNode(int servicePort,
NetworkProtoResolver networkProtoResolver, NetworkProtoResolver networkProtoResolver,
boolean useStreamIsolation,
TorMode torMode,
@Nullable BanFilter banFilter, @Nullable BanFilter banFilter,
int maxConnections, String torControlHost) { int maxConnections) {
super(servicePort, networkProtoResolver, banFilter, maxConnections); super(servicePort, networkProtoResolver, banFilter, maxConnections);
this.torMode = torMode;
this.streamIsolation = useStreamIsolation;
this.torControlHost = torControlHost;
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor"); executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
} }
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
@Override @Override
public void start(@Nullable SetupListener setupListener) { public void start(@Nullable SetupListener setupListener) {
torMode.doRollingBackup();
if (setupListener != null) if (setupListener != null)
addSetupListener(setupListener); addSetupListener(setupListener);
createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort); createTorAndHiddenService();
}
@Override
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
// If streamId is null stream isolation gets deactivated.
// Hidden services use stream isolation by default, so we pass null.
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
}
public Socks5Proxy getSocksProxy() {
try {
String stream = null;
if (streamIsolation) {
byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash
new SecureRandom().nextBytes(bytes);
stream = Base64.getEncoder().encodeToString(bytes);
}
if (socksProxy == null || streamIsolation) {
tor = Tor.getDefault();
socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null;
}
return socksProxy;
} catch (Throwable t) {
log.error("Error at getSocksProxy", t);
return null;
}
} }
public void shutDown(@Nullable Runnable shutDownCompleteHandler) { public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
log.info("TorNetworkNode shutdown started"); super.shutDown(shutDownCompleteHandler);
if (shutDownComplete) {
log.info("TorNetworkNode shutdown already completed");
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
return;
}
if (shutDownInProgress) {
log.warn("Ignoring request to shut down because shut down is in progress");
return;
}
shutDownInProgress = true;
shutDownTimeoutTimer = UserThread.runAfter(() -> {
log.error("A timeout occurred at shutDown");
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
executor.shutdownNow();
}, SHUT_DOWN_TIMEOUT);
super.shutDown(() -> {
try {
tor = Tor.getDefault();
if (tor != null) {
tor.shutdown();
tor = null;
log.info("Tor shutdown completed");
}
executor.shutdownNow();
} catch (Throwable e) {
log.error("Shutdown torNetworkNode failed with exception", e);
} finally {
shutDownTimeoutTimer.stop();
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
}
});
} }
/////////////////////////////////////////////////////////////////////////////////////////// public abstract Socks5Proxy getSocksProxy();
// Create tor and hidden service
///////////////////////////////////////////////////////////////////////////////////////////
private void createTorAndHiddenService(int localPort, int servicePort) { protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;
executor.submit(() -> {
try { protected abstract void createTorAndHiddenService();
Tor.setDefault(torMode.getTor());
long ts = System.currentTimeMillis();
hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort);
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort()));
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
hiddenServiceSocket.addReadyListener(socket -> {
log.info("\n################################################################\n" +
"Tor hidden service published after {} ms. Socket={}\n" +
"################################################################",
System.currentTimeMillis() - ts, socket);
UserThread.execute(() -> {
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":"
+ hiddenServiceSocket.getHiddenServicePort()));
startServer(socket);
setupListeners.forEach(SetupListener::onHiddenServicePublished);
});
return null;
});
} catch (TorCtlException e) {
log.error("Starting tor node failed", e);
if (e.getCause() instanceof IOException) {
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} else {
UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges));
log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges.");
shutDown(null);
}
} catch (IOException e) {
log.error("Could not connect to running Tor", e);
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} catch (Throwable ignore) {
}
return null;
});
}
} }

View File

@ -0,0 +1,114 @@
package haveno.network.p2p.network;
import haveno.common.util.Hex;
import haveno.network.p2p.NodeAddress;
import haveno.common.UserThread;
import haveno.common.proto.network.NetworkProtoResolver;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import java.net.Socket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class TorNetworkNodeDirectBind extends TorNetworkNode {
private static final int TOR_DATA_PORT = 9050; // TODO: config option?
private final String serviceAddress;
public TorNetworkNodeDirectBind(int servicePort,
NetworkProtoResolver networkProtoResolver,
@Nullable BanFilter banFilter,
int maxConnections,
String hiddenServiceAddress) {
super(servicePort, networkProtoResolver, banFilter, maxConnections);
this.serviceAddress = hiddenServiceAddress;
}
@Override
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
super.shutDown(() -> {
log.info("TorNetworkNodeDirectBind shutdown completed");
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
});
}
@Override
public Socks5Proxy getSocksProxy() {
Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
proxy.resolveAddrLocally(false);
return proxy;
}
@Override
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
// https://datatracker.ietf.org/doc/html/rfc1928 SOCKS5 Protocol
try {
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
sock.getOutputStream().write(Hex.decode("050100"));
String response = Hex.encode(sock.getInputStream().readNBytes(2));
if (!response.equalsIgnoreCase("0500")) {
return null;
}
String connect_details = "050100033E" + Hex.encode(peerNodeAddress.getHostName().getBytes(StandardCharsets.UTF_8));
StringBuilder connect_port = new StringBuilder(Integer.toHexString(peerNodeAddress.getPort()));
while (connect_port.length() < 4) connect_port.insert(0, "0");
connect_details = connect_details + connect_port;
sock.getOutputStream().write(Hex.decode(connect_details));
response = Hex.encode(sock.getInputStream().readNBytes(10));
if (response.substring(0, 2).equalsIgnoreCase("05") && response.substring(2, 4).equalsIgnoreCase("00")) {
return sock; // success
}
if (response.substring(2, 4).equalsIgnoreCase("04")) {
log.warn("Host unreachable: {}", peerNodeAddress);
} else {
log.warn("SOCKS error code received {} expected 00", response.substring(2, 4));
}
if (!response.substring(0, 2).equalsIgnoreCase("05")) {
log.warn("unexpected response, this isn't a SOCKS5 proxy?: {} {}", response, response.substring(0, 2));
}
} catch (Exception e) {
log.warn(e.toString());
}
throw new IOException("createSocket failed");
}
@Override
protected void createTorAndHiddenService() {
executor.submit(() -> {
try {
// listener for incoming messages at the hidden service
ServerSocket socket = new ServerSocket(servicePort);
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
log.info("\n################################################################\n" +
"Bound to Tor hidden service: {} Port: {}\n" +
"################################################################",
serviceAddress, servicePort);
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
UserThread.runAfter(() -> {
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
startServer(socket);
setupListeners.forEach(SetupListener::onHiddenServicePublished);
}, 3);
return null;
} catch (IOException e) {
log.error("Could not connect to external Tor", e);
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
}
return null;
});
}
}

View File

@ -0,0 +1,174 @@
package haveno.network.p2p.network;
import haveno.common.Timer;
import haveno.network.p2p.NodeAddress;
import haveno.common.UserThread;
import haveno.common.proto.network.NetworkProtoResolver;
import haveno.network.utils.Utils;
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.TorSocket;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import java.security.SecureRandom;
import java.net.Socket;
import java.io.IOException;
import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class TorNetworkNodeNetlayer extends TorNetworkNode {
private static final long SHUT_DOWN_TIMEOUT = 2;
private HiddenServiceSocket hiddenServiceSocket;
private boolean streamIsolation;
private Socks5Proxy socksProxy;
protected TorMode torMode;
private Tor tor;
private final String torControlHost;
private Timer shutDownTimeoutTimer;
private boolean shutDownInProgress;
private boolean shutDownComplete;
public TorNetworkNodeNetlayer(int servicePort,
NetworkProtoResolver networkProtoResolver,
TorMode torMode,
@Nullable BanFilter banFilter,
int maxConnections,
boolean useStreamIsolation,
String torControlHost) {
super(servicePort, networkProtoResolver, banFilter, maxConnections);
this.torControlHost = torControlHost;
this.streamIsolation = useStreamIsolation;
this.torMode = torMode;
}
@Override
public void start(@Nullable SetupListener setupListener) {
torMode.doRollingBackup();
super.start(setupListener);
}
@Override
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
log.info("TorNetworkNodeNetlayer shutdown started");
if (shutDownComplete) {
log.info("TorNetworkNodeNetlayer shutdown already completed");
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
return;
}
if (shutDownInProgress) {
log.warn("Ignoring request to shut down because shut down is in progress");
return;
}
shutDownInProgress = true;
shutDownTimeoutTimer = UserThread.runAfter(() -> {
log.error("A timeout occurred at shutDown");
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
executor.shutdownNow();
}, SHUT_DOWN_TIMEOUT);
super.shutDown(() -> {
try {
tor = Tor.getDefault();
if (tor != null) {
tor.shutdown();
tor = null;
log.info("Tor shutdown completed");
}
executor.shutdownNow();
} catch (Throwable e) {
log.error("Shutdown TorNetworkNodeNetlayer failed with exception", e);
} finally {
shutDownTimeoutTimer.stop();
shutDownComplete = true;
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
}
});
}
@Override
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
// If streamId is null stream isolation gets deactivated.
// Hidden services use stream isolation by default, so we pass null.
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
}
@Override
public Socks5Proxy getSocksProxy() {
try {
String stream = null;
if (streamIsolation) {
byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash
new SecureRandom().nextBytes(bytes);
stream = Base64.getEncoder().encodeToString(bytes);
}
if (socksProxy == null || streamIsolation) {
tor = Tor.getDefault();
socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null;
}
return socksProxy;
} catch (Throwable t) {
log.error("Error at getSocksProxy", t);
return null;
}
}
@Override
protected void createTorAndHiddenService() {
int localPort = Utils.findFreeSystemPort();
executor.submit(() -> {
try {
Tor.setDefault(torMode.getTor());
long ts = System.currentTimeMillis();
hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort);
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort()));
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
hiddenServiceSocket.addReadyListener(socket -> {
log.info("\n################################################################\n" +
"Tor hidden service published after {} ms. Socket={}\n" +
"################################################################",
System.currentTimeMillis() - ts, socket);
UserThread.execute(() -> {
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":"
+ hiddenServiceSocket.getHiddenServicePort()));
startServer(socket);
setupListeners.forEach(SetupListener::onHiddenServicePublished);
});
return null;
});
} catch (TorCtlException e) {
log.error("Starting tor node failed", e);
if (e.getCause() instanceof IOException) {
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} else {
UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges));
log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges.");
shutDown(null);
}
} catch (IOException e) {
log.error("Could not connect to running Tor", e);
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
} catch (Throwable ignore) {
}
return null;
});
}
}

View File

@ -381,7 +381,7 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost
// If a node is trying to send too many list we treat it as rule violation. // If a node is trying to send too many list we treat it as rule violation.
// Reported list include the connected list. We use the max value and give some extra headroom. // Reported list include the connected list. We use the max value and give some extra headroom.
// Will trigger a shutdown after 2nd time sending too much // Will trigger a shutdown after 2nd time sending too much
connection.reportInvalidRequest(RuleViolation.TOO_MANY_REPORTED_PEERS_SENT); connection.reportInvalidRequest(RuleViolation.TOO_MANY_REPORTED_PEERS_SENT, "Too many reported peers sent");
} }
} }

View File

@ -50,8 +50,13 @@ public class TorNetworkNodeTest {
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException { public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
latch = new CountDownLatch(1); latch = new CountDownLatch(1);
int port = 9001; int port = 9001;
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false, TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1"); TestUtils.getNetworkProtoResolver(),
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
null,
12,
false,
"127.0.0.1");
node1.start(new SetupListener() { node1.start(new SetupListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
@ -77,8 +82,13 @@ public class TorNetworkNodeTest {
latch = new CountDownLatch(1); latch = new CountDownLatch(1);
int port2 = 9002; int port2 = 9002;
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false, TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1"); TestUtils.getNetworkProtoResolver(),
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
null,
12,
false,
"127.0.0.1");
node2.start(new SetupListener() { node2.start(new SetupListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
@ -135,8 +145,13 @@ public class TorNetworkNodeTest {
public void testTorNodeAfterBothReady() throws InterruptedException, IOException { public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
latch = new CountDownLatch(2); latch = new CountDownLatch(2);
int port = 9001; int port = 9001;
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false, TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1"); TestUtils.getNetworkProtoResolver(),
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
null,
12,
false,
"127.0.0.1");
node1.start(new SetupListener() { node1.start(new SetupListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
@ -161,8 +176,12 @@ public class TorNetworkNodeTest {
}); });
int port2 = 9002; int port2 = 9002;
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false, TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(),
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1"); new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
null,
12,
false,
"127.0.0.1");
node2.start(new SetupListener() { node2.start(new SetupListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {

View File

@ -10,6 +10,8 @@ SyslogIdentifier=Haveno-Seednode
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
--useLocalhostForP2P=false\ --useLocalhostForP2P=false\
--useDevPrivilegeKeys=false\ --useDevPrivilegeKeys=false\
# Uncomment the following line to use external tor
# --hiddenServiceAddress=example.onion\
--nodePort=2002\ --nodePort=2002\
--appName=haveno-XMR_STAGENET_Seed_2002\ --appName=haveno-XMR_STAGENET_Seed_2002\
# --logLevel=trace\ # --logLevel=trace\

View File

@ -10,8 +10,10 @@ SyslogIdentifier=Haveno-Seednode2
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
--useLocalhostForP2P=false\ --useLocalhostForP2P=false\
--useDevPrivilegeKeys=false\ --useDevPrivilegeKeys=false\
--nodePort=3003\ # Uncomment the following line to use external tor
--appName=haveno-XMR_STAGENET_Seed_3003\ # --hiddenServiceAddress=example.onion\
--nodePort=2003\
--appName=haveno-XMR_STAGENET_Seed_2003\
# --logLevel=trace\ # --logLevel=trace\
--xmrNode=http://[::1]:38088\ --xmrNode=http://[::1]:38088\
--xmrNodeUsername=admin\ --xmrNodeUsername=admin\

View File

@ -2,10 +2,10 @@
## 1. Enable persistent storage and admin password before starting tails ## 1. Enable persistent storage and admin password before starting tails
## 2. Get your haveno deb file in persistent storage, currently most people use haveno-reto (amd64 version for tails) ## 2. Get your haveno deb file in persistent storage (amd64 version for tails)
## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh``` ## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh```
## 4. As amnesia run ```source ~/.bashrc``` ## 4. As amnesia run ```source ~/.bashrc```
## 5. Start haveno using ```haveno-tails``` ## 5. Start haveno using ```haveno-tails```
## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno-reto ## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno

View File

@ -11,9 +11,11 @@ SyslogIdentifier=Haveno-Seednode
ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
--useLocalhostForP2P=false\ --useLocalhostForP2P=false\
--useDevPrivilegeKeys=false\ --useDevPrivilegeKeys=false\
# Uncomment the following line to use external tor
# --hiddenServiceAddress=example.onion\
--nodePort=2002\ --nodePort=2002\
--appName=haveno-XMR_STAGENET_Seed_2002 --appName=haveno-XMR_STAGENET_Seed_2002\
--xmrNode=[::1]:38088 --xmrNode=http://[::1]:38088
ExecStop=/bin/kill ${MAINPID} ; sleep 5 ExecStop=/bin/kill ${MAINPID} ; sleep 5
Restart=always Restart=always

View File

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p { public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30; private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.0.9"; private static final String VERSION = "1.0.10";
private SeedNode seedNode; private SeedNode seedNode;
private Timer checkConnectionLossTime; private Timer checkConnectionLossTime;

View File

@ -1,18 +1,155 @@
RunAsDaemon 1 ## Configuration file for Haveno Seednode
SOCKSPort 9050 ##
ControlPort 9051 ## To start/reload/etc this instance, run "systemctl start tor" (or reload, or..).
Log notice syslog ## This instance will run as user debian-tor; its data directory is /var/lib/tor.
##
## This file is configured via:
## /usr/share/tor/tor-service-defaults-torrc
##
## See 'man tor', for more options you can use in this file.
CookieAuthentication 0 ## Tor opens a socks proxy on port 9050 by default -- even if you don't
CookieAuthFileGroupReadable 1 ## configure one below. Set "SocksPort 0" if you plan to run Tor only
DataDirectoryGroupReadable 1 ## as a relay, and not make any local application connections yourself.
#SocksPort 9050 # Default: Bind to localhost:9050 for local connections.
# ### SocksPort flag: OnionTrafficOnly ###
## Tell the tor client to only connect to .onion addresses in response to SOCKS5 requests on this connection.
## This is equivalent to NoDNSRequest, NoIPv4Traffic, NoIPv6Traffic.
# ### SocksPort flag: ExtendedErrors ###
## Return extended error code in the SOCKS reply. So far, the possible errors are:
# X'F0' Onion Service Descriptor Can Not be Found
# X'F1' Onion Service Descriptor Is Invalid
# X'F2' Onion Service Introduction Failed
# X'F3' Onion Service Rendezvous Failed
# X'F4' Onion Service Missing Client Authorization
# X'F5' Onion Service Wrong Client Authorization
# X'F6' Onion Service Invalid Address
# X'F7' Onion Service Introduction Timed Out
SocksPort 9050 OnionTrafficOnly ExtendedErrors
SafeSocks 0 ## Entry policies to allow/deny SOCKS requests based on IP address.
HiddenServiceStatistics 0 ## First entry that matches wins. If no SocksPolicy is set, we accept
## all (and only) requests that reach a SocksPort. Untrusted users who
## can access your SocksPort may be able to learn about the connections
## you make.
SocksPolicy accept 127.0.0.1
SocksPolicy accept6 [::1]
SocksPolicy reject *
## Tor will reject application connections that use unsafe variants of the socks protocol
##ones that only provide an IP address, meaning the application is doing a DNS resolve first.
## Specifically, these are socks4 and socks5 when not doing remote DNS. (Default: 0)
#SafeSocks 1
## Tor will make a notice-level log entry for each connection to the Socks port indicating
## whether the request used a safe socks protocol or an unsafe one (see above entry on SafeSocks).
## This helps to determine whether an application using Tor is possibly leaking DNS requests. (Default: 0)
TestSocks 1
## Logs go to stdout at level "notice" unless redirected by something
## else, like one of the below lines. You can have as many Log lines as
## you want.
##
## We advise using "notice" in most cases, since anything more verbose
## may provide sensitive information to an attacker who obtains the logs.
##
## Send all messages of level 'notice' or higher to /var/log/tor/notices.log
#Log notice file /var/log/tor/notices.log
## Send every possible message to /var/log/tor/debug.log
#Log debug file /var/log/tor/debug.log
## Use the system log instead of Tor's logfiles (This is default)
#Log notice syslog
## To send all messages to stderr:
#Log debug stderr
# Try to write to disk less frequently than we would otherwise. This is useful when running on flash memory.
AvoidDiskWrites 1 AvoidDiskWrites 1
#MaxClientCircuitsPending 64 ## TODO: This option has no effect. Bisq/Haveno is tor client &/or hidden service. 'man torrc':
#KeepalivePeriod 2 ## Relays and bridges only. When this option is enabled, a Tor relay writes obfuscated statistics on its
#CircuitBuildTimeout 5 ## role as hidden-service directory, introduction point, or rendezvous point to disk every 24 hours.
#NewCircuitPeriod 15 ## If ExtraInfoStatistics is enabled, it will be published as part of the extra-info document. (Default: 1)
#NumEntryGuards 8 HiddenServiceStatistics 0
## NOTE: In order to use the ControlPort, the <user> must belong to the tor group.
## sudo usermod -aG debian-tor <user>
##
## The port on which Tor will listen for local connections from Tor
## controller applications, as documented in control-spec.txt.
#ControlPort 9051
## If you enable the controlport, be sure to enable one of these
## authentication methods, to prevent attackers from accessing it.
##
## Compute the hash of a password with "tor --hash-password password".
#HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
CookieAuthentication 0 # (Default: 1)
## MetricsPort provides an interface to the underlying Tor relay metrics.
## Exposing publicly is dangerous, set a very strict access policy.
## Retrieve the metrics with: curl http://127.0.0.1:9035/metrics
MetricsPort 127.0.0.1:9035
MetricsPortPolicy accept 127.0.0.1
MetricsPortPolicy accept [::1]
############### This section is just for location-hidden services ###
## Once you have configured a hidden service, you can look at the
## contents of the file ".../hidden_service/hostname" for the address
## to tell people. e.g.: 'sudo cat /var/lib/tor/haveno_seednode/hostname'
##
## HiddenServicePort x y:z says to redirect requests on port x to the
## address y:z.
##
## If you plan to keep your service available for a long time, you might want to make a backup copy
## of the private_key file or complete folder /var/lib/tor/hidden_service somewhere.
#### Haveno seednode incoming anonymity connections ###
HiddenServiceDir /var/lib/tor/haveno_seednode
HiddenServicePort 2002 127.0.0.1:2002
HiddenServicePort 2002 [::1]:2002
## NOTE: HiddenService options are per onion service
## https://community.torproject.org/onion-services/advanced/dos/
##
## Rate limiting at the Introduction Points
## Intropoint protections prevents onion service DoS from becoming a DoS for the entire machine and its guard.
HiddenServiceEnableIntroDoSDefense 1
#HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25)
#HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200)
# Number of introduction points the hidden service will have. You cant have more than 20.
#HiddenServiceNumIntroductionPoints 3 # (Default: 3)
## https://tpo.pages.torproject.net/onion-services/ecosystem/technology/pow/#configuring-an-onion-service-with-the-pow-protection
## Proof of Work (PoW) before establishing Rendezvous Circuits
## The lower the queue and burst rates, the higher the puzzle effort tends to be for users.
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 200 # (Default: 250)
HiddenServicePoWQueueBurst 1000 # (Default: 2500)
## Stream limits in the established Rendezvous Circuits
## The maximum number of simultaneous streams (connections) per rendezvous circuit. The max value allowed is 65535. (0 = unlimited)
HiddenServiceMaxStreams 25
#HiddenServiceMaxStreamsCloseCircuit 1
#### Haveno seednode2 incoming anonymity connections ###
HiddenServiceDir /var/lib/tor/haveno_seednode2
HiddenServicePort 2003 127.0.0.1:2003
HiddenServicePort 2003 [::1]:2003
HiddenServiceEnableIntroDoSDefense 1
#HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25)
#HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200)
#HiddenServiceNumIntroductionPoints 3 # (Default: 3)
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 200 # (Default: 250)
HiddenServicePoWQueueBurst 1000 # (Default: 2500)
HiddenServiceMaxStreams 25
#HiddenServiceMaxStreamsCloseCircuit 1
#####################################################################
LongLivedPorts 2002,2003
## Default: 21, 22, 706, 1863, 5050, 5190, 5222, 5223, 6523, 6667, 6697, 8300