diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 325f4a7a0e..f295661964 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -107,12 +107,11 @@ public class Version { // The version no. of the current protocol. The offer holds that version. // A taker will check the version of the offers to see if his version is compatible. - // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. - // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening // the Haveno app. // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 - public static final int TRADE_PROTOCOL_VERSION = 2; + // Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3 + public static final int TRADE_PROTOCOL_VERSION = 3; private static String p2pMessageVersion; public static String getP2PMessageVersion() { diff --git a/core/src/main/java/haveno/core/api/CorePriceService.java b/core/src/main/java/haveno/core/api/CorePriceService.java index ddd194ebab..54688f97e4 100644 --- a/core/src/main/java/haveno/core/api/CorePriceService.java +++ b/core/src/main/java/haveno/core/api/CorePriceService.java @@ -74,7 +74,9 @@ class CorePriceService { public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); if (marketPrice == null) { - throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client + throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined? + } else if (!marketPrice.isExternallyProvidedPrice()) { + throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean } return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); } diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index 4a35f3c43c..a41b0c5c2d 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -151,7 +151,7 @@ public class CreateOfferService { // verify price boolean useMarketBasedPriceValue = fixedPrice == null && useMarketBasedPrice && - isMarketPriceAvailable(currencyCode) && + isExternalPriceAvailable(currencyCode) && !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); if (fixedPrice == null && !useMarketBasedPriceValue) { throw new IllegalArgumentException("Must provide fixed price"); @@ -338,7 +338,7 @@ public class CreateOfferService { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private boolean isMarketPriceAvailable(String currencyCode) { + private boolean isExternalPriceAvailable(String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 79d1232c22..e0ddba01a0 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -40,6 +40,7 @@ import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.util.JsonUtil; import haveno.core.xmr.wallet.XmrWalletService; @@ -134,6 +135,7 @@ public class HavenoUtils { public static OpenOfferManager openOfferManager; public static CoreNotificationService notificationService; public static CorePaymentAccountsService corePaymentAccountService; + public static TradeStatisticsManager tradeStatisticsManager; public static Preferences preferences; public static boolean isSeedNode() { diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 311d91ec2d..7d5a928676 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -65,7 +65,6 @@ import haveno.core.trade.protocol.ProcessModelServiceProvider; import haveno.core.trade.protocol.TradeListener; import haveno.core.trade.protocol.TradePeer; 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; @@ -124,6 +123,7 @@ import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -2446,12 +2446,27 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public void maybePublishTradeStatistics() { - if (shouldPublishTradeStatistics()) doPublishTradeStatistics(); + if (shouldPublishTradeStatistics()) { + + // publish after random delay within 24 hours + UserThread.runAfterRandomDelay(() -> { + if (!isShutDownStarted) doPublishTradeStatistics(); + }, 0, 24, TimeUnit.HOURS); + } } public boolean shouldPublishTradeStatistics() { - if (!isSeller()) return false; - return tradeAmountTransferred(); + + // do not publish if funds not transferred + if (!tradeAmountTransferred()) return false; + + // only seller or arbitrator publish trade stats + if (!isSeller() && !isArbitrator()) return false; + + // prior to v3 protocol, only seller publishes trade stats + if (getOffer().getOfferPayload().getProtocolVersion() < 3 && !isSeller()) return false; + + return true; } @@ -2466,13 +2481,7 @@ public abstract class Trade extends XmrWalletBase 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, true); - if (tradeStatistics.isValid()) { - log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId()); - processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true); - } else { - log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", getClass().getSimpleName(), getId(), tradeStatistics); - } + HavenoUtils.tradeStatisticsManager.maybePublishTradeStatistics(this, referralId, isTorNetworkNode); } // lazy initialization diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index a1f2b5d0b7..1a8e2236e5 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -523,7 +523,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi nonFailedTrades.addAll(tradableList.getList()); String referralId = referralIdService.getOptionalReferralId().orElse(null); boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; - tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); + tradeStatisticsManager.maybePublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); }).start(); // allow execution to start diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index a846077db4..0956358b1f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -71,7 +71,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { UUID.randomUUID().toString(), Version.getP2PMessageVersion(), request.getAccountAgeWitnessSignatureOfOfferId(), - new Date().getTime(), + request.getCurrentDate(), trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java index 9f191131ec..7f3c97df9a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java @@ -131,7 +131,7 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask { takerRequest.getUid(), Version.getP2PMessageVersion(), null, - takerRequest.getCurrentDate(), + trade.getTakeOfferDate().getTime(), // maker's date is used as shared timestamp trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java index 61962ce1d2..a97017d1c2 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -98,6 +98,7 @@ public class ProcessInitTradeRequest extends TradeTask { sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender == trade.getMaker()) { trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); + trade.setTakeOfferDate(request.getCurrentDate()); // check trade price try { @@ -116,6 +117,7 @@ public class ProcessInitTradeRequest extends TradeTask { if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring"); if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); + if (request.getCurrentDate() != trade.getTakeOfferDate().getTime()) throw new RuntimeException("Trade's take offer date does not match request's current date"); } // handle invalid sender @@ -134,6 +136,7 @@ public class ProcessInitTradeRequest extends TradeTask { trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator"); + trade.setTakeOfferDate(request.getCurrentDate()); } // handle invalid trade type diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 5d7cfead47..a9da2bb99e 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -69,13 +69,26 @@ 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 isFuzzed) { + public static TradeStatistics3 fromV0(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.0, 0, 0); + } + + public static TradeStatistics3 fromV1(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.05, 24, 0); + } + + public static TradeStatistics3 fromV2(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.10, 48, .01); + } + + // randomize completed trade info #1099 + private static TradeStatistics3 from(Trade trade, + @Nullable String referralId, + boolean isTorNetworkNode, + double fuzzAmountPct, + int fuzzDateHours, + double fuzzPricePct) { Map extraDataMap = new HashMap<>(); if (referralId != null) { extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); @@ -92,27 +105,39 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), - trade.getPrice().getValue(), - isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(), + fuzzTradePriceReproducibly(trade, fuzzPricePct), + fuzzTradeAmountReproducibly(trade, fuzzAmountPct), offer.getPaymentMethod().getId(), - isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(), + fuzzTradeDateReproducibly(trade, fuzzDateHours), truncatedArbitratorNodeAddress, extraDataMap); } - private static long fuzzTradeAmountReproducibly(Trade trade) { // randomize completed trade info #1099 + private static long fuzzTradePriceReproducibly(Trade trade, double fuzzPricePct) { + if (fuzzPricePct == 0.0) return trade.getPrice().getValue(); long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long exactPrice = trade.getPrice().getValue(); + long adjustedPrice = (long) random.nextDouble(exactPrice * (1.0 - fuzzPricePct), exactPrice * (1.0 + fuzzPricePct)); + log.debug("trade {} fuzzed trade price for tradeStatistics is {}", trade.getShortId(), adjustedPrice); + return adjustedPrice; + } + + private static long fuzzTradeAmountReproducibly(Trade trade, double fuzzAmountPct) { + if (fuzzAmountPct == 0.0) return trade.getAmount().longValueExact(); + long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long exactAmount = trade.getAmount().longValueExact(); - Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp - long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT)); + long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - fuzzAmountPct), exactAmount * (1.0 + fuzzAmountPct)); log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); return adjustedAmount; } - private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 + private static long fuzzTradeDateReproducibly(Trade trade, int fuzzDateHours) { + if (fuzzDateHours == 0) return trade.getTakeOfferDate().getTime(); 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(FUZZ_DATE_HOURS), originalTimestamp); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(fuzzDateHours), originalTimestamp); log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); return adjustedTimestamp; } diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java index b85e6932e6..863a6a885d 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java @@ -26,6 +26,7 @@ import haveno.core.locale.CurrencyTuple; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; import haveno.network.p2p.P2PService; @@ -68,8 +69,8 @@ public class TradeStatisticsManager { this.storageDir = storageDir; this.dumpStatistics = dumpStatistics; - appendOnlyDataStoreService.addService(tradeStatistics3StorageService); + HavenoUtils.tradeStatisticsManager = this; } public void shutDown() { @@ -208,7 +209,13 @@ public class TradeStatisticsManager { jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics"); } - public void maybeRepublishTradeStatistics(Set trades, + public void maybePublishTradeStatistics(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + Set trades = new HashSet<>(); + trades.add(trade); + maybePublishTradeStatistics(trades, referralId, isTorNetworkNode); + } + + public void maybePublishTradeStatistics(Set trades, @Nullable String referralId, boolean isTorNetworkNode) { long ts = System.currentTimeMillis(); @@ -219,38 +226,46 @@ public class TradeStatisticsManager { return; } - TradeStatistics3 tradeStatistics3 = null; + TradeStatistics3 tradeStatistics3V0 = null; try { - tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false); + tradeStatistics3V0 = TradeStatistics3.fromV0(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } - TradeStatistics3 tradeStatistics3Fuzzed = null; + TradeStatistics3 tradeStatistics3V1 = null; try { - tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true); + tradeStatistics3V1 = TradeStatistics3.fromV1(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } - boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); - boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash())); - if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) { + TradeStatistics3 tradeStatistics3V2 = null; + try { + tradeStatistics3V2 = TradeStatistics3.fromV2(trade, referralId, isTorNetworkNode); + } catch (Exception e) { + log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); + return; + } + + boolean hasTradeStatistics3V0 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V0.getHash())); + boolean hasTradeStatistics3V1 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V1.getHash())); + boolean hasTradeStatistics3V2 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V2.getHash())); + if (hasTradeStatistics3V0 || hasTradeStatistics3V1 || hasTradeStatistics3V2) { log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", trade.getShortId()); return; } - if (!tradeStatistics3.isValid()) { - log.warn("Trade: {}. Trade statistics is invalid. We do not publish it.", tradeStatistics3); + if (!tradeStatistics3V2.isValid()) { + log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", trade.getClass().getSimpleName(), trade.getShortId(), tradeStatistics3V1); return; } - log.info("Trade: {}. We republish tradeStatistics3 as we did not find it in the existing trade statistics. ", - trade.getShortId()); - p2PService.addPersistableNetworkPayload(tradeStatistics3, true); + log.info("Publishing trade statistics for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); + p2PService.addPersistableNetworkPayload(tradeStatistics3V2, true); }); log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}", System.currentTimeMillis() - ts, hashes.size(), trades.size());