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
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.

View File

@ -317,9 +317,6 @@ configure(project(':common')) {
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-graphics:$javafxVersion:$os"
}
@ -610,7 +607,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.9-SNAPSHOT'
version = '1.0.10-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

View File

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// 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.

View File

@ -77,6 +77,7 @@ public class Config {
public static final String SEED_NODES = "seedNodes";
public static final String BAN_LIST = "banList";
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 MAX_CONNECTIONS = "maxConnections";
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
@ -151,6 +152,7 @@ public class Config {
public final File appDataDir;
public final int walletRpcBindPort;
public final int nodePort;
public final String hiddenServiceAddress;
public final int maxMemory;
public final String logLevel;
public final List<String> bannedXmrNodes;
@ -286,6 +288,12 @@ public class Config {
.ofType(Integer.class)
.defaultsTo(9999);
ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
.withRequiredArg()
@ -670,6 +678,7 @@ public class Config {
this.helpRequested = options.has(helpOpt);
this.configFile = configFile;
this.nodePort = options.valueOf(nodePortOpt);
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
this.maxMemory = options.valueOf(maxMemoryOpt);
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
// 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();
}, trade.getId());
}
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
synchronized (trade) {
synchronized (trade.getLock()) {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
@ -163,7 +163,7 @@ public class CoreDisputesService {
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (trade) {
synchronized (trade.getLock()) {
try {
// create dispute result
@ -275,11 +275,13 @@ public class CoreDisputesService {
disputeResult.summaryNotesProperty().get()
);
synchronized (dispute.getChatMessages()) {
if (reason == DisputeResult.Reason.OPTION_TRADE &&
dispute.getChatMessages().size() > 1 &&
dispute.getChatMessages().get(1).isSystemMessage()) {
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
}
}
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);

View File

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

View File

@ -101,6 +101,7 @@ public final class XmrConnectionService {
private Long lastLogPollErrorTimestamp;
private Long syncStartHeight = null;
private TaskLooper daemonPollLooper;
private long lastRefreshPeriodMs = 0;
@Getter
private boolean isShutDownStarted;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
@ -273,7 +274,11 @@ public final class XmrConnectionService {
}
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
if (isShutDownStarted) {
@ -281,9 +286,15 @@ public final class XmrConnectionService {
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
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;
}
@ -343,7 +354,11 @@ public final class XmrConnectionService {
}
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() {
@ -413,12 +428,16 @@ public final class XmrConnectionService {
return connection != null && HavenoUtils.isLocalHost(connection.getUri());
}
private long getDefaultRefreshPeriodMs() {
private long getDefaultRefreshPeriodMs(boolean internal) {
MoneroRpcConnection connection = getConnection();
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
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
else return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
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)) {
return REFRESH_PERIOD_ONION_MS;
} else {
@ -638,7 +657,7 @@ public final class XmrConnectionService {
// update polling
doPollDaemon();
if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
// notify listeners in parallel
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) {
if (daemonPollLooper != null) daemonPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemon());
daemonPollLooper.start(getRefreshPeriodMs());
daemonPollLooper.start(getInternalRefreshPeriodMs());
}
}
@ -715,6 +734,13 @@ public final class XmrConnectionService {
// connected to daemon
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
UserThread.execute(() -> {

View File

@ -133,6 +133,22 @@ public class WalletAppSetup {
String result;
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
double walletDownloadPercentageD = (double) walletDownloadPercentage;
xmrWalletSyncProgress.set(walletDownloadPercentageD);
@ -144,29 +160,15 @@ public class WalletAppSetup {
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
getXmrSplashSyncIconId().set("image-connection-synced");
downloadCompleteHandler.run();
} else if (walletDownloadPercentageD > 0) {
} else if (walletDownloadPercentageD >= 0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
getXmrSplashSyncIconId().set(""); // clear synced icon
} 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 feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
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 {

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
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
// message arrived.
synchronized (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
// 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.user.Preferences;
import haveno.core.user.User;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javafx.collections.SetChangeListener;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@ -218,7 +215,7 @@ public class OfferFilterService {
return result;
}
public boolean hasValidSignature(Offer offer) {
private boolean hasValidSignature(Offer offer) {
// get accepted arbitrator by address
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?
} else {
// otherwise log warning that arbitrator is unregistered
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);
// // otherwise log warning that arbitrator is unregistered
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
// 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());
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap();
@ -431,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
reserveTxKeyImages);
}
@Override

View File

@ -110,6 +110,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javax.annotation.Nullable;
import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer;
@ -688,7 +689,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
addOpenOffer(editedOpenOffer);
if (editedOpenOffer.isAvailable())
republishOffer(editedOpenOffer);
maybeRepublishOffer(editedOpenOffer);
offersToBeEdited.remove(openOffer.getId());
requestPersistence();
@ -863,8 +864,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> {
synchronized (processOffersLock) {
List<String> errorMessages = new ArrayList<String>();
synchronized (processOffersLock) {
List<OpenOffer> openOffers = getOpenOffers();
for (OpenOffer pendingOffer : openOffers) {
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
@ -887,13 +888,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
});
HavenoUtils.awaitLatch(latch);
}
}
requestPersistence();
if (errorMessages.isEmpty()) {
if (resultHandler != null) resultHandler.handleResult(null);
} else {
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
}
}
}, THREAD_ID);
}
@ -962,9 +963,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
} else {
// handle sufficient balance
// sign and post offer if enough funds
boolean hasFundsReserved = openOffer.getReserveTxHash() != null;
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
if (hasSufficientBalance) {
if (hasFundsReserved || hasSufficientBalance) {
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
return;
} else if (openOffer.getScheduledTxHashes() == null) {
@ -1083,11 +1085,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded();
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
synchronized (HavenoUtils.getWalletFunctionLock()) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
@ -1100,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} catch (Exception e) {
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());
xmrWalletService.handleWalletError(e, sourceConnection);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
@ -1714,11 +1717,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
synchronized (openOffers) {
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
// 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
republishOffer(openOffer, () -> processListForRepublishOffers(list));
maybeRepublishOffer(openOffer, () -> processListForRepublishOffers(list));
/* republishOffer(openOffer,
() -> UserThread.runAfter(() -> processListForRepublishOffers(list),
@ -1730,13 +1733,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
private void republishOffer(OpenOffer openOffer) {
republishOffer(openOffer, null);
private void maybeRepublishOffer(OpenOffer openOffer) {
maybeRepublishOffer(openOffer, null);
}
private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
ThreadUtils.execute(() -> {
// skip if prevented from publishing
if (preventedFromPublishing(openOffer)) {
if (completeHandler != null) completeHandler.run();
return;
}
// determine if offer is valid
boolean isValid = true;
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());
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());
isValid = false;
}
@ -1811,6 +1820,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID);
}
private boolean preventedFromPublishing(OpenOffer openOffer) {
return openOffer.isDeactivated() || openOffer.isCanceled();
}
private void startPeriodicRepublishOffersTimer() {
stopped = false;
if (periodicRepublishOffersTimer == null) {
@ -1843,8 +1856,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
final OpenOffer openOffer = openOffersList.get(i);
UserThread.runAfterRandomDelay(() -> {
// we need to check if in the meantime the offer has been removed
if (openOffers.contains(openOffer) && openOffer.isAvailable())
refreshOffer(openOffer, 0, 1);
boolean contained = false;
synchronized (openOffers) {
contained = openOffers.contains(openOffer);
}
if (contained) maybeRefreshOffer(openOffer, 0, 1);
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
}
} else {
@ -1857,13 +1873,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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(),
() -> log.debug("Successful refreshed TTL for offer"),
(errorMessage) -> {
log.warn(errorMessage);
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.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
@ -59,11 +59,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
}
// verify monero connection
model.getXmrWalletService().getConnectionService().verifyConnection();
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
// create reserve tx
MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// reset protocol timeout
verifyPending();
@ -82,14 +82,16 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = model.getXmrWalletService().getXmrConnectionService().getConnection();
try {
//if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) {
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;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -102,11 +104,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
// reset state with wallet lock
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
offer.getOfferPayload().setReserveTxKeyImages(null);
}
throw e;
}
@ -132,7 +131,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
}
}
public void verifyPending() {
if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
private boolean isPending() {
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) {
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {} with tradeId {} and uid {}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
// ack message on chat message received when dispute is opened and closed
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 (dispute.isClosed()) trade.pollWalletNormallyForMs(30000); // sync to check for payout
else trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) {
if (dispute.isClosed()) dispute.reOpen();
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
} else if (dispute.isClosed()) {
trade.pollWalletNormallyForMs(30000); // sync to check for payout
}
}
}
}
}
}
} else {
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
log.warn("Received AckMessage with error state for {} with tradeId={}, sender={}, errorMessage={}",
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()

View File

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

View File

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

View File

@ -157,6 +157,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeListService.requestPersistence();
}
protected void requestPersistence(Trade trade) {
trade.requestPersistence();
disputeListService.requestPersistence();
}
@Override
public NodeAddress getPeerNodeAddress(ChatMessage message) {
Optional<Dispute> disputeOptional = findDispute(message);
@ -182,11 +187,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
@Override
public List<ChatMessage> getAllChatMessages(String tradeId) {
synchronized (getDisputeList().getObservableList()) {
return getDisputeList().stream()
.filter(dispute -> dispute.getTradeId().equals(tradeId))
.flatMap(dispute -> dispute.getChatMessages().stream())
.collect(Collectors.toList());
}
}
@Override
public boolean channelOpen(ChatMessage message) {
@ -234,7 +241,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
public ObservableList<Dispute> getDisputesAsObservableList() {
synchronized(disputeListService.getDisputeList()) {
synchronized(disputeListService.getDisputeList().getObservableList()) {
return disputeListService.getObservableList();
}
}
@ -244,7 +251,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
protected T getDisputeList() {
synchronized(disputeListService.getDisputeList()) {
synchronized(disputeListService.getDisputeList().getObservableList()) {
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
public void sendDisputeOpenedMessage(Dispute dispute,
boolean reOpen,
String updatedMultisigHex,
ResultHandler resultHandler,
FaultHandler faultHandler) {
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
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;
}
@ -343,7 +356,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return;
}
synchronized (disputeList) {
synchronized (disputeList.getObservableList()) {
if (disputeList.contains(dispute)) {
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
log.warn(msg);
@ -352,7 +365,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
if (!storedDisputeOptional.isPresent() || reOpen) {
// add or re-open dispute
if (reOpen) {
dispute = storedDisputeOptional.get();
} else {
disputeList.add(dispute);
}
String disputeInfo = getDisputeInfo(dispute);
String sysMsg = dispute.isSupportTicket() ?
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
@ -367,17 +389,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
p2PService.getAddress());
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
if (!reOpen) {
disputeList.add(dispute);
}
// create dispute opened message
trade.exportMultisigHex();
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType(),
updatedMultisigHex,
trade.getSelf().getUpdatedMultisigHex(),
trade.getArbitrator().getPaymentSentMessage());
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
"chatMessage.uid={}",
@ -387,6 +407,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
// send dispute opened message
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
dispute.getAgentPubKeyRing(),
disputeOpenedMessage,
@ -420,7 +441,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
requestPersistence();
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
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage);
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
requestPersistence();
faultHandler.handleFault("Sending dispute message failed: " +
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
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
Dispute dispute = message.getDispute();
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
Dispute msgDispute = message.getDispute();
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
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;
}
// 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(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
String errorMessage = null;
PubKeyRing senderPubKeyRing = null;
try {
@ -503,14 +538,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
// 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();
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");
// update sender node address
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()) {
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
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
// add dispute
// add or re-open dispute
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
if (!disputeList.contains(msgDispute)) {
if (!storedDisputeOptional.isPresent() || reOpen) {
// update trade state
if (reOpen) {
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
} else {
disputeList.add(dispute);
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
}
// send dispute opened message to peer if arbitrator
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
// reset buyer and seller unsigned payout tx hex
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();
errorMessage = null;
} else {
@ -548,7 +602,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// add chat message with mediation info if applicable
addMediationResultMessage(dispute);
} 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) {
@ -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
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
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);
}
@ -575,7 +629,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
Contract contractFromOpener,
PubKeyRing pubKeyRing,
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
// 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
@ -597,6 +651,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return;
}
// create mirrored dispute
Dispute dispute = new Dispute(new Date().getTime(),
disputeFromOpener.getTradeId(),
pubKeyRing.hashCode(),
@ -622,10 +677,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
// skip if dispute already open
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
// Valid case if both have opened a dispute and agent was not online.
if (storedDisputeOptional.isPresent()) {
if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) {
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
return;
}
@ -647,9 +701,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
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) {
disputeList.add(dispute);
}
}
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
@ -658,10 +719,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return;
}
// We mirrored dispute already!
Contract contract = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
// create dispute opened message with peer dispute
TradePeer peer = trade.getTradePeer(pubKeyRing);
PubKeyRing peersPubKeyRing = peer.getPubKeyRing();
NodeAddress peersNodeAddress = peer.getNodeAddress();
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),
UUID.randomUUID().toString(),
@ -749,7 +810,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
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());
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
@ -792,7 +853,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.getChatMessage().setArrived(true);
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.pollWalletNormallyForMs(30000);
requestPersistence();
requestPersistence(trade);
resultHandler.handleResult();
}
@ -811,7 +872,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResult.getChatMessage().setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence();
requestPersistence(trade);
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
disputeResult.getChatMessage().setSendMessageError(errorMessage);
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence();
requestPersistence(trade);
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
}
}
);
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence();
requestPersistence(trade);
} catch (Exception e) {
faultHandler.handleFault(e.getMessage(), e);
}
@ -900,11 +961,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update trade state
if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
trade.updatePayout(payoutTx);
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
trade.requestPersistence();
return payoutTx;
} catch (Exception e) {
trade.syncAndPollWallet();
@ -937,21 +998,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
}
private Optional<Dispute> findDispute(Dispute dispute) {
public Optional<Dispute> findDispute(Dispute dispute) {
return findDispute(dispute.getTradeId(), dispute.getTraderId());
}
protected Optional<Dispute> findDispute(DisputeResult disputeResult) {
public Optional<Dispute> findDispute(DisputeResult disputeResult) {
ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null");
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
}
private Optional<Dispute> findDispute(ChatMessage message) {
public Optional<Dispute> findDispute(ChatMessage message) {
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();
if (disputeList == 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.MessageListener;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
@ -87,9 +88,11 @@ import monero.wallet.model.MoneroTxWallet;
import java.io.IOException;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
@ -168,7 +171,28 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
@Override
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
@ -216,7 +240,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
ThreadUtils.execute(() -> {
ChatMessage chatMessage = null;
Dispute dispute = null;
synchronized (trade) {
synchronized (trade.getLock()) {
try {
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
chatMessage = disputeResult.getChatMessage();
@ -252,7 +276,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// save dispute closed message for reprocessing
trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage);
requestPersistence();
requestPersistence(trade);
// verify arbitrator does not receive DisputeClosedMessage
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
@ -263,11 +287,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// set dispute state
cleanupRetryMap(uid);
synchronized (dispute.getChatMessages()) {
if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage);
} else {
log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId());
}
}
dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) {
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()) {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} 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);
}
}
@ -326,17 +352,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// 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.
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
requestPersistence();
requestPersistence(trade);
} catch (Exception e) {
log.warn("Error processing dispute closed message: " + e.getMessage());
log.warn("Error processing dispute closed message: {}", e.getMessage());
e.printStackTrace();
requestPersistence();
requestPersistence(trade);
// 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.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());
requestPersistence();
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
requestPersistence(trade);
throw e;
}
@ -356,7 +386,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
// 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()) {
@ -442,12 +472,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// sign arbitrator-signed payout tx
if (trade.getPayoutTxHex() == null) {
try {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex();
disputeTxSet.setMultisigTxHex(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
// 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
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
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());
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 (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
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.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
requestPersistence(trade);
return disputeTxSet;
}

View File

@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.Sig;
import haveno.common.util.Utilities;
import haveno.core.api.XmrConnectionService;
import haveno.core.app.HavenoSetup;
import haveno.core.offer.OfferPayload;
import haveno.core.offer.OpenOfferManager;
@ -106,6 +107,7 @@ public class HavenoUtils {
public static HavenoSetup havenoSetup;
public static ArbitrationManager arbitrationManager;
public static XmrWalletService xmrWalletService;
public static XmrConnectionService xmrConnectionService;
public static OpenOfferManager openOfferManager;
public static boolean isSeedNode() {
@ -502,4 +504,20 @@ public class HavenoUtils {
else if (Config.baseCurrencyNetwork().isStagenet()) return 38081;
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.taskrunner.Model;
import haveno.common.util.Utilities;
import haveno.core.api.XmrConnectionService;
import haveno.core.monetary.Price;
import haveno.core.monetary.Volume;
import haveno.core.network.MessageState;
@ -69,6 +68,7 @@ import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.statistics.TradeStatistics3;
import haveno.core.util.VolumeUtil;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletBase;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.NodeAddress;
@ -76,14 +76,12 @@ import haveno.network.p2p.P2PService;
import haveno.network.p2p.network.TorNetworkNode;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@ -135,19 +133,18 @@ import static com.google.common.base.Preconditions.checkNotNull;
* stored in the task model.
*/
@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 long SHUTDOWN_TIMEOUT_MS = 60000;
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 EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
private final Object walletLock = new Object();
private final Object pollLock = new Object();
private final LongProperty walletHeight = new SimpleLongProperty(0);
private MoneroWallet wallet;
private boolean wasWalletSynced;
protected final Object pollLock = new Object();
protected static final Object importMultisigLock = new Object();
private boolean pollInProgress;
private boolean restartInProgress;
private Subscription protocolErrorStateSubscription;
@ -321,7 +318,11 @@ public abstract class Trade implements Tradable, Model {
}
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() {
@ -409,9 +410,6 @@ public abstract class Trade implements Tradable, Model {
// Immutable
@Getter
transient final private XmrWalletService xmrWalletService;
@Getter
transient final private XmrConnectionService xmrConnectionService;
transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0);
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
@ -437,10 +435,6 @@ public abstract class Trade implements Tradable, Model {
@Getter
transient private boolean isInitialized;
transient private boolean isFullyInitialized;
@Getter
transient private boolean isShutDownStarted;
@Getter
transient private boolean isShutDown;
// Added in v1.2.0
transient private ObjectProperty<BigInteger> tradeAmountProperty;
@ -507,11 +501,12 @@ public abstract class Trade implements Tradable, Model {
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
super();
this.offer = offer;
this.amount = tradeAmount.longValueExact();
this.price = tradePrice;
this.xmrWalletService = xmrWalletService;
this.xmrConnectionService = xmrWalletService.getConnectionService();
this.xmrConnectionService = xmrWalletService.getXmrConnectionService();
this.processModel = processModel;
this.uid = uid;
this.takeOfferDate = new Date().getTime();
@ -846,8 +841,8 @@ public abstract class Trade implements Tradable, Model {
}
}
public boolean requestSwitchToNextBestConnection() {
if (xmrConnectionService.requestSwitchToNextBestConnection()) {
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
return true;
}
@ -891,10 +886,6 @@ public abstract class Trade implements Tradable, Model {
}).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?
private boolean isInvalidImportError(String errMsg) {
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) {
synchronized (walletLock) {
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() {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
synchronized (importMultisigLock) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
doImportMultisigHex();
break;
@ -1074,15 +1077,15 @@ public abstract class Trade implements Tradable, Model {
throw e;
} catch (Exception e) {
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 (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
}
}
}
}
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());
}
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) {
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
@ -1175,14 +1184,15 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
return doCreatePayoutTx();
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
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 (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
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));
// update state
saveWallet();
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(payoutTxFeeSplit);
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
getSeller().setPayoutTxFee(payoutTxFeeSplit);
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
return payoutTx;
}
@ -1234,6 +1242,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
return createTx(txConfig);
@ -1242,8 +1251,8 @@ public abstract class Trade implements Tradable, Model {
} catch (Exception e) {
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());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
@ -1263,6 +1272,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
doProcessPayoutTx(payoutTxHex, sign, publish);
break;
@ -1270,9 +1280,12 @@ public abstract class Trade implements Tradable, Model {
throw e;
} catch (Exception e) {
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 (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
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
for (int i = 0; i < 20; i++) {
synchronized (this) {
synchronized (getLock()) {
HavenoUtils.waitFor(10);
}
}
// shut down trade threads
synchronized (this) {
synchronized (getLock()) {
isInitialized = false;
isShutDown = true;
List<Runnable> shutDownThreads = new ArrayList<>();
@ -2299,7 +2312,7 @@ public abstract class Trade implements Tradable, Model {
private void doPublishTradeStatistics() {
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
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()) {
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
@ -2337,7 +2350,10 @@ public abstract class Trade implements Tradable, Model {
// check if ignored
if (isShutDownStarted) 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)
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) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
synchronized (walletLock) {
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 (isWalletBehind()) {
@ -2406,6 +2424,7 @@ public abstract class Trade implements Tradable, Model {
syncWalletIfBehind();
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
}
}
// apply tor after wallet synced depending on configuration
if (!wasWalletSynced) {
@ -2417,7 +2436,7 @@ public abstract class Trade implements Tradable, Model {
if (pollWallet) pollWallet();
} catch (Exception e) {
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId());
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
throw e;
}
}
@ -2546,11 +2565,12 @@ public abstract class Trade implements Tradable, Model {
// rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
wallet.rescanSpent();
} catch (Exception e) {
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) {
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
if (isConnectionRefused) forceRestartTradeWallet();
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet();
else {
boolean isWalletConnected = isWalletConnectedToDaemon();
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();
}
}
@ -2613,9 +2632,15 @@ public abstract class Trade implements Tradable, Model {
}
private void syncWalletIfBehind() {
if (isWalletBehind()) {
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);
} else {
syncWithProgress();
}
walletHeight.set(wallet.getHeight());
}
}
@ -2660,7 +2685,8 @@ public abstract class Trade implements Tradable, Model {
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
wallet.rescanBlockchain();
} 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;
} finally {
@ -2790,7 +2816,7 @@ public abstract class Trade implements Tradable, Model {
if (!isInitialized || isShutDownStarted) return;
if (isWalletConnectedToDaemon()) {
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());

View File

@ -45,7 +45,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
latchTrade();
this.errorMessageHandler = errorMessageHandler;
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) {
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@ public class SellerProtocol extends DisputeProtocol {
// re-send payment received message if payout not published
ThreadUtils.execute(() -> {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
synchronized (trade) {
synchronized (trade.getLock()) {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
latchTrade();
@ -117,7 +117,7 @@ public class SellerProtocol extends DisputeProtocol {
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("SellerProtocol.onPaymentReceived()");
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
latchTrade();
this.errorMessageHandler = errorMessageHandler;
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);
// initialize trade
synchronized (trade) {
synchronized (trade.getLock()) {
trade.initialize(processModel.getProvider());
// process mailbox messages
@ -261,7 +261,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
ThreadUtils.execute(() -> {
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
depositsConfirmedTasksCalled = true;
synchronized (trade) {
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
latchTrade();
expect(new Condition(trade))
@ -282,7 +282,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
// 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())) {
@ -299,7 +299,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
// check trade
if (trade.hasFailed()) {
@ -335,7 +335,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
// check trade
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());
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
// check trade
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());
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
Validator.checkTradeId(processModel.getOfferId(), response);
latchTrade();
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());
if (!trade.isInitialized() || trade.isShutDown()) return;
ThreadUtils.execute(() -> {
synchronized (trade) {
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return;
latchTrade();
this.errorMessageHandler = null;
@ -493,7 +493,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// a mailbox message with PaymentSentMessage.
// 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.
synchronized (trade) {
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return;
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());
@ -542,7 +542,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
return;
}
synchronized (trade) {
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return;
latchTrade();
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) {
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) {
sendAckMessage(ackReceiver, message, false, errorMessage);

View File

@ -87,6 +87,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
trade.requestPersistence();
}
complete();
@ -163,7 +164,8 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// convert info to csv
Integer length = null;
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));
@ -203,5 +205,3 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
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.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
@ -77,7 +77,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs
MoneroTxWallet depositTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout
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 {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
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 (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
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 (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence();
// update state
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();
}
processModel.getTradeManager().requestPersistence();
trade.requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View File

@ -76,8 +76,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
// export multisig hex once
if (trade.getSelf().getUpdatedMultisigHex() == null) {
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
processModel.getTradeManager().requestPersistence();
trade.exportMultisigHex();
}
// 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.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
@ -49,7 +49,7 @@ public class TakerReserveTradeFunds extends TradeTask {
// create reserve tx
MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout
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 {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) {
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 (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}

View File

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

View File

@ -110,35 +110,54 @@ public class TradeStatisticsManager {
maybeDumpStatistics();
}
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> set) {
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> tradeStats) {
// collect trades before May 31, 2024
Set<TradeStatistics3> tradesBeforeMay31_24 = set.stream()
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-05-31T00:00:00Z")))
// collect trades before August 7, 2024
Set<TradeStatistics3> earlyTrades = tradeStats.stream()
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-08-07T00:00:00Z")))
.collect(Collectors.toSet());
// collect duplicated trades
Set<TradeStatistics3> duplicated = new HashSet<TradeStatistics3>();
Set<TradeStatistics3> deduplicated = new HashSet<TradeStatistics3>();
for (TradeStatistics3 tradeStatistics : tradesBeforeMay31_24) {
if (hasLenientDuplicate(tradeStatistics, deduplicated)) duplicated.add(tradeStatistics);
else deduplicated.add(tradeStatistics);
Set<TradeStatistics3> duplicates = new HashSet<TradeStatistics3>();
Set<TradeStatistics3> deduplicates = new HashSet<TradeStatistics3>();
Set<TradeStatistics3> usedAsDuplicate = new HashSet<TradeStatistics3>();
for (TradeStatistics3 tradeStatistic : earlyTrades) {
TradeStatistics3 fuzzyDuplicate = findFuzzyDuplicate(tradeStatistic, deduplicates, usedAsDuplicate);
if (fuzzyDuplicate == null) deduplicates.add(tradeStatistic);
else {
duplicates.add(tradeStatistic);
usedAsDuplicate.add(fuzzyDuplicate);
}
}
// remove duplicated trades
set.removeAll(duplicated);
tradeStats.removeAll(duplicates);
}
private boolean hasLenientDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set) {
return set.stream().anyMatch(e -> isLenientDuplicate(tradeStatistics, e));
private TradeStatistics3 findFuzzyDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set, Set<TradeStatistics3> excluded) {
return set.stream().filter(e -> !excluded.contains(e)).filter(e -> isFuzzyDuplicate(tradeStatistics, e)).findFirst().orElse(null);
}
private boolean isLenientDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) < 120000;
return isWithin2Minutes &&
tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency()) &&
tradeStatistics1.getAmount() == tradeStatistics2.getAmount() &&
tradeStatistics1.getPrice() == tradeStatistics2.getPrice();
private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false;
if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false;
if (tradeStatistics1.getPrice() != tradeStatistics2.getPrice()) return false;
return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2);
}
// 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() {
@ -206,13 +225,23 @@ public class TradeStatisticsManager {
TradeStatistics3 tradeStatistics3 = null;
try {
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false);
} catch (Exception e) {
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
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()));
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.",
trade.getShortId());
return;

View File

@ -44,6 +44,7 @@ import haveno.core.offer.OpenOfferManager;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
@ -124,7 +125,7 @@ public class Balances {
private void doUpdateBalances() {
synchronized (this) {
synchronized (XmrWalletService.WALLET_LOCK) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// get wallet balances
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 haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.file.FileUtil;
@ -43,7 +42,6 @@ import haveno.core.user.User;
import haveno.core.xmr.listeners.XmrBalanceListener;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.model.XmrAddressEntryList;
import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.MoneroWalletRpcManager;
import haveno.core.xmr.setup.WalletsSetup;
import java.io.File;
@ -55,26 +53,22 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.value.ChangeListener;
import lombok.Getter;
import monero.common.MoneroError;
import monero.common.MoneroRpcConnection;
import monero.common.MoneroRpcError;
@ -103,14 +97,13 @@ import monero.wallet.model.MoneroTxPriority;
import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet;
import monero.wallet.model.MoneroWalletConfig;
import monero.wallet.model.MoneroWalletListener;
import monero.wallet.model.MoneroWalletListenerI;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class XmrWalletService {
public class XmrWalletService extends XmrWalletBase {
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
// monero configuration
@ -134,15 +127,13 @@ public class XmrWalletService {
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
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 Preferences preferences;
private final CoreAccountService accountService;
private final XmrConnectionService xmrConnectionService;
private final XmrAddressEntryList xmrAddressEntryList;
private final WalletsSetup walletsSetup;
private final DownloadListener downloadListener = new DownloadListener();
private final LongProperty walletHeight = new SimpleLongProperty(0);
private final File walletDir;
private final File xmrWalletFile;
@ -153,24 +144,15 @@ public class XmrWalletService {
private ChangeListener<? super Number> walletInitListener;
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 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 boolean pollInProgress;
private Long pollPeriodMs;
private Long lastLogPollErrorTimestamp;
private long lastLogPollErrorTimestamp;
private long lastPollTxsTimestamp;
private final Object pollLock = new Object();
private Long cachedHeight;
private BigInteger cachedBalance;
@ -178,7 +160,6 @@ public class XmrWalletService {
private List<MoneroSubaddress> cachedSubaddresses;
private List<MoneroOutputWallet> cachedOutputs;
private List<MoneroTxWallet> cachedTxs;
private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
@SuppressWarnings("unused")
@Inject
@ -194,7 +175,6 @@ public class XmrWalletService {
this.user = user;
this.preferences = preferences;
this.accountService = accountService;
this.xmrConnectionService = xmrConnectionService;
this.walletsSetup = walletsSetup;
this.xmrAddressEntryList = xmrAddressEntryList;
this.walletDir = walletDir;
@ -202,6 +182,8 @@ public class XmrWalletService {
this.useNativeXmrWallet = useNativeXmrWallet;
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
HavenoUtils.xmrWalletService = this;
HavenoUtils.xmrConnectionService = xmrConnectionService;
this.xmrConnectionService = xmrConnectionService; // TODO: super's is null unless set here from injection
// set monero logging
if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
@ -316,10 +298,6 @@ public class XmrWalletService {
return xmrConnectionService.getDaemon();
}
public XmrConnectionService getConnectionService() {
return xmrConnectionService;
}
public boolean isProxyApplied() {
return isProxyApplied(wasWalletSynced);
}
@ -420,6 +398,10 @@ public class XmrWalletService {
}
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) {
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true);
} else {
@ -456,7 +438,7 @@ public class XmrWalletService {
}
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
MoneroTxWallet tx = wallet.createTx(txConfig);
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) {
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
//printTxs("XmrWalletService.createTx", tx);
@ -479,7 +469,7 @@ public class XmrWalletService {
* Freeze reserved outputs and thaw unreserved outputs.
*/
public void fixReservedOutputs() {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// collect reserved outputs
Set<String> reservedKeyImages = new HashSet<String>();
@ -498,7 +488,7 @@ public class XmrWalletService {
}
private void freezeReservedOutputs(Set<String> reservedKeyImages) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// ensure wallet is open
if (wallet == null) {
@ -522,7 +512,7 @@ public class XmrWalletService {
}
private void thawUnreservedOutputs(Set<String> reservedKeyImages) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// ensure wallet is open
if (wallet == null) {
@ -552,7 +542,7 @@ public class XmrWalletService {
*/
public void freezeOutputs(Collection<String> keyImages) {
if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// collect outputs to freeze
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
@ -574,7 +564,7 @@ public class XmrWalletService {
*/
public void thawOutputs(Collection<String> keyImages) {
if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// collect outputs to thaw
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
@ -627,7 +617,7 @@ public class XmrWalletService {
* @return the reserve tx
*/
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()) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis();
@ -648,7 +638,7 @@ public class XmrWalletService {
* @return MoneroTxWallet the multisig deposit tx
*/
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
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) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
MoneroWallet wallet = getWallet();
// create a list of subaddresses to attempt spending from in preferred order
@ -903,7 +893,7 @@ public class XmrWalletService {
Runnable shutDownTask = () -> {
// remove listeners
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
if (wallet != null) {
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
wallet.removeListener(listener);
@ -913,7 +903,7 @@ public class XmrWalletService {
}
// shut down threads
synchronized (this) {
synchronized (getLock()) {
List<Runnable> shutDownThreads = new ArrayList<>();
shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID));
ThreadUtils.awaitTasks(shutDownThreads);
@ -1285,9 +1275,19 @@ public class XmrWalletService {
else log.info(appliedMsg);
// listen for connection changes
xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> {
xmrConnectionService.addConnectionListener(connection -> {
if (wasWalletSynced && !isSyncingWithProgress) {
ThreadUtils.execute(() -> {
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
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
@ -1306,11 +1306,20 @@ public class XmrWalletService {
}
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) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
if (isShutDownStarted) return;
// open or create wallet main wallet
@ -1327,6 +1336,7 @@ public class XmrWalletService {
long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
user.setWalletCreationDate(date);
}
walletHeight.set(wallet.getHeight());
isClosingWallet = false;
}
@ -1335,6 +1345,7 @@ public class XmrWalletService {
log.info("Monero wallet path={}", wallet.getPath());
// sync main wallet if applicable
// TODO: error handling and re-initialization is jenky, refactor
if (sync && numAttempts > 0) {
try {
@ -1347,7 +1358,16 @@ public class XmrWalletService {
// sync main wallet
log.info("Syncing main wallet");
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");
// poll wallet
@ -1410,7 +1430,7 @@ public class XmrWalletService {
if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) {
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?
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
HavenoUtils.setTopError(warningMsg);
// reset address entries
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) {
// must be connected to daemon
@ -1529,7 +1461,7 @@ public class XmrWalletService {
return walletFull;
} catch (Exception e) {
e.printStackTrace();
if (walletFull != null) forceCloseMainWallet();
if (walletFull != null) forceCloseWallet(walletFull, config.getPath());
throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'");
}
}
@ -1661,27 +1593,22 @@ public class XmrWalletService {
}
private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
// use current connection
connection = xmrConnectionService.getConnection();
// check if ignored
if (wallet == null || isShutDownStarted) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
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);
// 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();
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
updatePollPeriod();
return;
}
// 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 (StringUtils.equals(oldProxyUri, newProxyUri)) {
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
closeMainWallet(true);
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
return; // wallet is re-initialized
return; // wallet re-initializes off thread
}
} else {
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()));
log.info("Done changing all wallet passwords");
}
private void closeMainWallet(boolean save) {
stopPolling();
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
try {
if (wallet != null) {
isClosingWallet = true;
@ -1758,19 +1685,28 @@ public class XmrWalletService {
private void forceCloseMainWallet() {
stopPolling();
if (wallet != null && !isClosingWallet) {
isClosingWallet = true;
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
wallet = null;
}
}
private void forceRestartMainWallet() {
public void forceRestartMainWallet() {
log.warn("Force restarting main wallet");
if (isClosingWallet) return;
forceCloseMainWallet();
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() {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
if (isShutDownStarted || isPolling()) return;
updatePollPeriod();
pollLooper = new TaskLooper(() -> pollWallet());
@ -1791,15 +1727,15 @@ public class XmrWalletService {
public void updatePollPeriod() {
if (isShutDownStarted) return;
setPollPeriod(getPollPeriod());
setPollPeriodMs(getPollPeriodMs());
}
private long getPollPeriod() {
private long getPollPeriodMs() {
return xmrConnectionService.getRefreshPeriodMs();
}
private void setPollPeriod(long pollPeriodMs) {
synchronized (WALLET_LOCK) {
private void setPollPeriodMs(long pollPeriodMs) {
synchronized (walletLock) {
if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
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());
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
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();
} else {
syncWithProgress();
}
}
}
// fetch transactions from pool and store to cache
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
if (updateTxs) {
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
synchronized (walletLock) { // avoid long fetch from blocking other operations
synchronized (HavenoUtils.getDaemonLock()) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
lastPollTxsTimestamp = System.currentTimeMillis();
} catch (Exception e) { // fetch from pool can fail
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());
lastLogPollErrorTimestamp = System.currentTimeMillis();
if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection);
}
}
}
@ -1868,33 +1808,32 @@ public class XmrWalletService {
}
} catch (Exception e) {
if (wallet == null || isShutDownStarted) return;
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
if (isConnectionRefused) forceRestartMainWallet();
if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet();
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();
}
} finally {
synchronized (pollLock) {
pollInProgress = false;
}
// cache wallet info last
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
if (wallet != null && !isShutDownStarted) {
try {
cacheWalletInfo();
requestSaveMainWallet();
} catch (Exception e) {
e.printStackTrace();
}
}
}
synchronized (pollLock) {
pollInProgress = false;
}
}
}
private MoneroSyncResult syncMainWallet() {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
MoneroSyncResult result = syncWallet(wallet);
walletHeight.set(wallet.getHeight());
return result;
@ -1902,7 +1841,7 @@ public class XmrWalletService {
}
public boolean isWalletConnectedToDaemon() {
synchronized (WALLET_LOCK) {
synchronized (walletLock) {
try {
if (wallet == null) return false;
return wallet.isConnectedToDaemon();
@ -1912,8 +1851,12 @@ public class XmrWalletService {
}
}
public boolean requestSwitchToNextBestConnection() {
return xmrConnectionService.requestSwitchToNextBestConnection();
private boolean requestSwitchToNextBestConnection() {
return requestSwitchToNextBestConnection(null);
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
}
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 -->
<key>CFBundleVersion</key>
<string>1.0.9</string>
<string>1.0.10</string>
<key>CFBundleShortVersionString</key>
<string>1.0.9</string>
<string>1.0.10</string>
<key>CFBundleExecutable</key>
<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 {
if (trade == null) {
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 {
if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx();
else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx();

View File

@ -1201,16 +1201,16 @@ textfield */
}
.text-area-no-border {
-fx-border-color: -fx-control-inner-background;
-fx-border-color: -bs-background-color;
}
.text-area-no-border .content {
-fx-background-color: -fx-control-inner-background;
-fx-background-color: -bs-background-color;
}
.text-area-no-border:focused {
-fx-focus-color: -fx-control-inner-background;
-fx-faint-focus-color: -fx-control-inner-background;
-fx-focus-color: -bs-background-color;
-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.coin.CoinFormatter;
import haveno.desktop.common.view.FxmlView;
import haveno.desktop.components.AutocompleteComboBox;
import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.components.paymentmethods.AchTransferForm;
import haveno.desktop.components.paymentmethods.AdvancedCashForm;
@ -144,7 +145,6 @@ import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.GridPane;
@ -181,7 +181,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
private final AdvancedCashValidator advancedCashValidator;
private final TransferwiseValidator transferwiseValidator;
private final CoinFormatter formatter;
private ComboBox<PaymentMethod> paymentMethodComboBox;
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private PaymentMethodForm paymentMethodForm;
private TitledGroupBg accountTitledGroupBg;
private Button saveNewAccountButton;
@ -463,14 +463,16 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
removeAccountRows();
addAccountButton.setDisable(true);
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.setVisibleRowCount(11);
paymentMethodComboBox = FormBuilder.addAutocompleteComboBox(
root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE
);
paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
paymentMethodComboBox.setPrefWidth(250);
List<PaymentMethod> list = PaymentMethod.paymentMethods.stream()
.filter(PaymentMethod::isTraditional)
.sorted()
.collect(Collectors.toList());
paymentMethodComboBox.setItems(FXCollections.observableArrayList(list));
paymentMethodComboBox.setAutocompleteItems(FXCollections.observableArrayList(list));
paymentMethodComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(PaymentMethod paymentMethod) {
@ -479,10 +481,15 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
@Override
public PaymentMethod fromString(String s) {
if (s.isEmpty())
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) {
FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1);
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
@ -550,6 +557,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
}
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) {
if (paymentMethod == null) return null;
final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
paymentAccount.init();
return getPaymentMethodForm(paymentMethod, paymentAccount);

View File

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

View File

@ -594,7 +594,11 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
// show confirmation
showPayoutTxConfirmation(contract,
payoutTx,
() -> doClose(closeTicketButton, cancelButton));
() -> doClose(closeTicketButton, cancelButton),
() -> {
closeTicketButton.setDisable(false);
cancelButton.setDisable(false);
});
} else {
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)
String buyerPayoutAddressString = contract.getBuyerPayoutAddressString();
@ -641,6 +645,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
.actionButtonText(Res.get("shared.yes"))
.onAction(() -> resultHandler.handleResult())
.closeButtonText(Res.get("shared.cancel"))
.onClose(() -> cancelHandler.handleResult())
.show();
} else {
// No payout will be made
@ -649,6 +654,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
.actionButtonText(Res.get("shared.yes"))
.onAction(resultHandler::handleResult)
.closeButtonText(Res.get("shared.cancel"))
.onClose(() -> cancelHandler.handleResult())
.show();
}
}

View File

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

View File

@ -190,15 +190,13 @@ public abstract class TradeStepView extends AnchorPane {
}
trade.errorMessageProperty().addListener(errorMessageListener);
if (!isMediationClosedState()) {
tradeStepInfo.setOnAction(e -> {
if (this.isTradePeriodOver()) {
if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
openSupportTicket();
} else {
openChat();
}
});
}
// 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
@ -572,7 +570,7 @@ public abstract class TradeStepView extends AnchorPane {
}
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
if (isInArbitration()) {
if (isInMediation()) {
if (isRefundRequestStartedByPeer()) {
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
} else if (isRefundRequestSelfStarted()) {
@ -597,7 +595,7 @@ public abstract class TradeStepView extends AnchorPane {
}
}
private boolean isInArbitration() {
private boolean isInMediation() {
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
}
@ -613,6 +611,10 @@ public abstract class TradeStepView extends AnchorPane {
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
}
private boolean isArbitrationOpenedState() {
return trade.getDisputeState().isOpen();
}
private boolean isTradePeriodOver() {
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) {
if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) {
if (!trade.getDisputeState().isOpen()) {
switch (tradePeriodState) {
case FIRST_HALF:
// 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
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);
} else {
closeDisputeFromButton();

View File

@ -131,7 +131,7 @@
-bs-red-soft: derive(-bs-rd-error-red, 60%);
-bs-progress-bar-track: #272728;
-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-prompt-text: -bs-color-gray-6;
-bs-decimals: #db6300;

View File

@ -1405,6 +1405,24 @@ public class FormBuilder {
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
///////////////////////////////////////////////////////////////////////////////////////////

View File

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

View File

@ -79,17 +79,60 @@ Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as
## 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:
1. [Build the Haveno repository](#fork-and-build-haveno).
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.
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.
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.
### 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.
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.NewTor;
import haveno.network.p2p.network.RunningTor;
import haveno.network.p2p.network.DirectBindTor;
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 javax.annotation.Nullable;
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
@Named(Config.MAX_CONNECTIONS) int maxConnections,
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
@Named(Config.NODE_PORT) int port,
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
@Named(Config.TOR_DIR) File torDir,
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
@Named(Config.TORRC_OPTIONS) String torrcOptions,
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
torrcOptions,
controlHost,
controlPort,
hiddenServiceAddress,
password,
cookieFile,
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 controlHost,
int controlPort,
String hiddenServiceAddress,
String password,
@Nullable File cookieFile,
boolean useSafeCookieAuthentication) {
return controlPort != Config.UNSPECIFIED_PORT ?
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) :
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
if (!hiddenServiceAddress.equals("")) {
return new DirectBindTor();
} else if (controlPort != Config.UNSPECIFIED_PORT) {
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
} else {
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
}
}
@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.MAX_CONNECTIONS;
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.SOCKS_5_PROXY_HTTP_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(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);

View File

@ -171,9 +171,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private final Capabilities capabilities = new Capabilities();
// throttle logs of reported invalid requests
private static long lastLoggedInvalidRequestReport = 0;
private static int unloggedInvalidRequestReports = 0;
private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s
private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds
private static long lastLoggedInvalidRequestReportTs = 0;
private static int numUnloggedInvalidRequestReports = 0;
private static long lastLoggedWarningTs = 0;
private static int numUnloggedWarnings = 0;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -218,8 +220,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (peersNodeAddress != null) {
setPeersNodeAddress(peersNodeAddress);
if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) {
log.warn("We created an outbound connection with a banned peer");
reportInvalidRequest(RuleViolation.PEER_BANNED);
reportInvalidRequest(RuleViolation.PEER_BANNED, "We created an outbound connection with a banned peer");
}
}
ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID);
@ -249,8 +250,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (banFilter != null &&
peersNodeAddressOptional.isPresent() &&
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
log.warn("We tried to send a message to a banned peer. message={}", networkEnvelope.getClass().getSimpleName());
reportInvalidRequest(RuleViolation.PEER_BANNED);
String errorMessage = "We tried to send a message to a banned peer. message=" + networkEnvelope.getClass().getSimpleName();
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return;
}
@ -419,7 +420,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (networkEnvelope instanceof SendersNodeAddressMessage) {
boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope);
if (!isValid) {
log.warn("Received an invalid {} at processing BundleOfEnvelopes", networkEnvelope.getClass().getSimpleName());
throttleWarn("Received an invalid " + networkEnvelope.getClass().getSimpleName() + " at processing BundleOfEnvelopes");
continue;
}
}
@ -610,20 +611,20 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
* Runs in same thread as Connection
*/
public boolean reportInvalidRequest(RuleViolation ruleViolation) {
return Connection.reportInvalidRequest(this, ruleViolation);
public boolean reportInvalidRequest(RuleViolation ruleViolation, String errorMessage) {
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
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
if (!logReport) unloggedInvalidRequestReports++;
if (!logReport) numUnloggedInvalidRequestReports++;
// 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;
numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
numRuleViolations++;
@ -654,9 +655,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) {
if (logReport) {
if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports);
unloggedInvalidRequestReports = 0;
lastLoggedInvalidRequestReport = System.currentTimeMillis();
if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports);
numUnloggedInvalidRequestReports = 0;
lastLoggedInvalidRequestReportTs = System.currentTimeMillis();
}
}
@ -673,25 +674,22 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
else
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) {
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) {
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) {
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 {
// TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException
closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION;
log.warn("Unknown reason for exception at socket: {}\n\t" +
"peer={}\n\t" +
"Exception={}",
socket.toString(),
this.peersNodeAddressOptional,
e.toString());
throttleWarn("Unknown reason for exception at socket: " + socket.toString() + "\n\t" +
"peer=" + this.peersNodeAddressOptional + "\n\t" +
"Exception=" + e.toString());
}
shutDown(closeConnectionReason);
}
@ -712,8 +710,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
}
if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) {
log.warn("We got a message from a banned peer. message={}", sendersNodeAddressMessage.getClass().getSimpleName());
reportInvalidRequest(RuleViolation.PEER_BANNED);
String errorMessage = "We got a message from a banned peer. message=" + sendersNodeAddressMessage.getClass().getSimpleName();
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return false;
}
@ -745,7 +743,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
try {
if (socket != null &&
socket.isClosed()) {
log.warn("Socket is null or closed socket={}", socket);
throttleWarn("Socket is null or closed socket=" + socket);
shutDown(CloseConnectionReason.SOCKET_CLOSED);
return;
}
@ -757,7 +755,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (socket != null &&
socket.isClosed()) {
log.warn("Socket is null or closed socket={}", socket);
throttleWarn("Socket is null or closed socket=" + socket);
shutDown(CloseConnectionReason.SOCKET_CLOSED);
return;
}
@ -767,9 +765,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
return;
}
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 {
log.warn("proto is null. protoInputStream.read()=" + protoInputStream.read());
throttleWarn("proto is null. protoInputStream.read()=" + protoInputStream.read());
}
shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV);
return;
@ -778,8 +776,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (banFilter != null &&
peersNodeAddressOptional.isPresent() &&
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
log.warn("We got a message from a banned peer. proto={}", Utilities.toTruncatedString(proto));
reportInvalidRequest(RuleViolation.PEER_BANNED);
String errorMessage = "We got a message from a banned peer. proto=" + Utilities.toTruncatedString(proto);
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
return;
}
@ -814,30 +812,28 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage &&
!((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) {
log.warn("PersistableNetworkPayload.verifyHashSize failed. hashSize={}; object={}",
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length,
Utilities.toTruncatedString(proto));
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
String errorMessage = "PersistableNetworkPayload.verifyHashSize failed. hashSize=" +
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length + "; object=" +
Utilities.toTruncatedString(proto);
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
return;
}
if (exceeds) {
log.warn("size > MAX_MSG_SIZE. size={}; object={}", size, Utilities.toTruncatedString(proto));
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
String errorMessage = "size > MAX_MSG_SIZE. size=" + size + "; object=" + Utilities.toTruncatedString(proto);
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
return;
}
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED))
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED, "Violates throttle limit"))
return;
// 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())
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) {
log.warn("RuleViolation.WRONG_NETWORK_ID. version of message={}, app version={}, " +
"proto.toTruncatedString={}", proto.getMessageVersion(),
Version.getP2PMessageVersion(),
Utilities.toTruncatedString(proto.toString()));
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID, errorMessage)) {
return;
}
@ -879,12 +875,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID);
}
} catch (InvalidClassException e) {
log.error(e.getMessage());
e.printStackTrace();
reportInvalidRequest(RuleViolation.INVALID_CLASS);
reportInvalidRequest(RuleViolation.INVALID_CLASS, e.getMessage());
} catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) {
log.error(e.getMessage());
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE, e.getMessage());
} catch (Throwable t) {
handleException(t);
}
@ -943,4 +936,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope);
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;
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.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 java.security.SecureRandom;
import java.net.Socket;
import java.io.IOException;
import java.util.Base64;
import java.util.concurrent.ExecutorService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class TorNetworkNode extends NetworkNode {
private static final long SHUT_DOWN_TIMEOUT = 2;
public abstract class TorNetworkNode extends NetworkNode {
private final String torControlHost;
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;
protected final ExecutorService executor;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
public TorNetworkNode(int servicePort,
NetworkProtoResolver networkProtoResolver,
boolean useStreamIsolation,
TorMode torMode,
@Nullable BanFilter banFilter,
int maxConnections, String torControlHost) {
int maxConnections) {
super(servicePort, networkProtoResolver, banFilter, maxConnections);
this.torMode = torMode;
this.streamIsolation = useStreamIsolation;
this.torControlHost = torControlHost;
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
}
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
@Override
public void start(@Nullable SetupListener setupListener) {
torMode.doRollingBackup();
if (setupListener != null)
addSetupListener(setupListener);
createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
}
@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;
}
createTorAndHiddenService();
}
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
log.info("TorNetworkNode shutdown started");
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();
}
});
super.shutDown(shutDownCompleteHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Create tor and hidden service
///////////////////////////////////////////////////////////////////////////////////////////
public abstract Socks5Proxy getSocksProxy();
private void createTorAndHiddenService(int localPort, int servicePort) {
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;
});
}
protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;
protected abstract void createTorAndHiddenService();
}

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

View File

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

View File

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

View File

@ -2,10 +2,10 @@
## 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```
## 4. As amnesia run ```source ~/.bashrc```
## 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\
--useLocalhostForP2P=false\
--useDevPrivilegeKeys=false\
# Uncomment the following line to use external tor
# --hiddenServiceAddress=example.onion\
--nodePort=2002\
--appName=haveno-XMR_STAGENET_Seed_2002
--xmrNode=[::1]:38088
--appName=haveno-XMR_STAGENET_Seed_2002\
--xmrNode=http://[::1]:38088
ExecStop=/bin/kill ${MAINPID} ; sleep 5
Restart=always

View File

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p {
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 Timer checkConnectionLossTime;

View File

@ -1,18 +1,155 @@
RunAsDaemon 1
SOCKSPort 9050
ControlPort 9051
Log notice syslog
## Configuration file for Haveno Seednode
##
## To start/reload/etc this instance, run "systemctl start tor" (or reload, or..).
## 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
CookieAuthFileGroupReadable 1
DataDirectoryGroupReadable 1
## Tor opens a socks proxy on port 9050 by default -- even if you don't
## configure one below. Set "SocksPort 0" if you plan to run Tor only
## 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
HiddenServiceStatistics 0
## Entry policies to allow/deny SOCKS requests based on IP address.
## 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
#MaxClientCircuitsPending 64
#KeepalivePeriod 2
#CircuitBuildTimeout 5
#NewCircuitPeriod 15
#NumEntryGuards 8
## TODO: This option has no effect. Bisq/Haveno is tor client &/or hidden service. 'man torrc':
## Relays and bridges only. When this option is enabled, a Tor relay writes obfuscated statistics on its
## role as hidden-service directory, introduction point, or rendezvous point to disk every 24 hours.
## If ExtraInfoStatistics is enabled, it will be published as part of the extra-info document. (Default: 1)
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