mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-07 06:02:33 -04:00
randomize trade amount +-10%, price +-1%, date within 48 hours (fork) (#1815)
This commit is contained in:
parent
79dbe34359
commit
51d40d73a7
11 changed files with 104 additions and 49 deletions
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<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();
|
||||
|
@ -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());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue