randomize trade amount +-10%, price +-1%, date within 48 hours (fork) (#1815)

This commit is contained in:
woodser 2025-07-21 09:56:04 -04:00 committed by GitHub
parent 79dbe34359
commit 51d40d73a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 104 additions and 49 deletions

View file

@ -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() {

View file

@ -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());
}

View file

@ -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();
}

View file

@ -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() {

View file

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

View file

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

View file

@ -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(),

View file

@ -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(),

View file

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

View file

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

View file

@ -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());