Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions common/src/main/java/haveno/common/app/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,11 @@ private static int getSubVersion(String version, int index) {

// 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() {
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/haveno/core/api/CorePriceService.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ public CorePriceService(PriceFeedService priceFeedService, OfferBookService offe
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public Offer createAndGetOffer(String offerId,
// 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");
Expand Down Expand Up @@ -338,7 +338,7 @@ public Offer createClonedOffer(Offer sourceOffer,
// Private
///////////////////////////////////////////////////////////////////////////////////////////

private boolean isMarketPriceAvailable(String currencyCode) {
private boolean isExternalPriceAvailable(String currencyCode) {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/haveno/core/trade/HavenoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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;
Expand Down Expand Up @@ -134,6 +135,7 @@ public static Object getWalletFunctionLock() {
public static OpenOfferManager openOfferManager;
public static CoreNotificationService notificationService;
public static CorePaymentAccountsService corePaymentAccountService;
public static TradeStatisticsManager tradeStatisticsManager;
public static Preferences preferences;

public static boolean isSeedNode() {
Expand Down
31 changes: 20 additions & 11 deletions core/src/main/java/haveno/core/trade/Trade.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
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;
Expand Down Expand Up @@ -124,6 +123,7 @@
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;
Expand Down Expand Up @@ -2431,12 +2431,27 @@ public long getReprocessDelayInSeconds(int reprocessCount) {
}

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;
}


Expand All @@ -2451,13 +2466,7 @@ private boolean tradeAmountTransferred() {
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
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/haveno/core/trade/TradeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ private void initPersistedTrades() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ protected void run() {
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
request.getAccountAgeWitnessSignatureOfOfferId(),
new Date().getTime(),
request.getCurrentDate(),
trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(),
trade.getArbitrator().getNodeAddress(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectM
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ else if (trade instanceof ArbitratorTrade) {
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender == trade.getMaker()) {
trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing());
trade.setTakeOfferDate(request.getCurrentDate());

// check trade price
try {
Expand All @@ -116,6 +117,7 @@ else if (sender == trade.getTaker()) {
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
Expand All @@ -134,6 +136,7 @@ else if (trade instanceof TakerTrade) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> extraDataMap = new HashMap<>();
if (referralId != null) {
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
Expand All @@ -92,27 +105,39 @@ public static TradeStatistics3 from(Trade trade,

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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -68,8 +69,8 @@ public TradeStatisticsManager(P2PService p2PService,
this.storageDir = storageDir;
this.dumpStatistics = dumpStatistics;


appendOnlyDataStoreService.addService(tradeStatistics3StorageService);
HavenoUtils.tradeStatisticsManager = this;
}

public void shutDown() {
Expand Down Expand Up @@ -208,7 +209,13 @@ private void maybeDumpStatistics() {
jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics");
}

public void maybeRepublishTradeStatistics(Set<Trade> trades,
public void maybePublishTradeStatistics(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) {
Set<Trade> trades = new HashSet<>();
trades.add(trade);
maybePublishTradeStatistics(trades, referralId, isTorNetworkNode);
}

public void maybePublishTradeStatistics(Set<Trade> trades,
@Nullable String referralId,
boolean isTorNetworkNode) {
long ts = System.currentTimeMillis();
Expand All @@ -219,38 +226,46 @@ public void maybeRepublishTradeStatistics(Set<Trade> trades,
return;
}

TradeStatistics3 tradeStatistics3 = null;
TradeStatistics3 tradeStatistics3V0 = null;
try {
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 tradeStatistics3V1 = null;
try {
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false);
tradeStatistics3V1 = TradeStatistics3.fromV1(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 tradeStatistics3V2 = null;
try {
tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true);
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 hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash()));
boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash()));
if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) {
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());
Expand Down
Loading