Skip to content

Commit 9cd22f8

Browse files
committed
Add new version for mediation selection including the offer ID. Will get activated with June 1st 2025.
1 parent 34b713d commit 9cd22f8

File tree

5 files changed

+63
-18
lines changed

5 files changed

+63
-18
lines changed

apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/take_offer/review/TakeOfferReviewController.java

+8-6
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ public void takeOffer(Runnable onCancelHandler) {
171171
onCancelHandler.run();
172172
return;
173173
}
174-
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(), takerIdentity.getId());
174+
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(),
175+
takerIdentity.getId(),
176+
bisqEasyOffer.getId());
175177
if (!DevMode.isDevMode() && mediator.isEmpty()) {
176178
new Popup().warning(Res.get("bisqEasy.takeOffer.noMediatorAvailable.warning"))
177179
.closeButtonText(Res.get("action.cancel"))
@@ -204,11 +206,11 @@ private void doTakeOffer(BisqEasyOffer bisqEasyOffer, UserIdentity takerIdentity
204206
log.info("Selected mediator for trade {}: {}", bisqEasyTrade.getShortId(), mediator.map(UserProfile::getUserName).orElse("N/A"));
205207
model.setBisqEasyTrade(bisqEasyTrade);
206208
errorMessagePin = bisqEasyTrade.errorMessageObservable().addObserver(errorMessage -> {
207-
if (errorMessage != null) {
208-
UIThread.run(() -> new Popup().error(Res.get("bisqEasy.openTrades.failed.popup",
209-
errorMessage,
210-
StringUtils.truncate(bisqEasyTrade.getErrorStackTrace(), 500)))
211-
.show());
209+
if (errorMessage != null) {
210+
UIThread.run(() -> new Popup().error(Res.get("bisqEasy.openTrades.failed.popup",
211+
errorMessage,
212+
StringUtils.truncate(bisqEasyTrade.getErrorStackTrace(), 500)))
213+
.show());
212214
}
213215
}
214216
);

apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/review/TradeWizardReviewController.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,9 @@ public void takeOffer() {
423423
// If taker is banned we don't need to show them a popup
424424
return;
425425
}
426-
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(), takerIdentity.getId());
426+
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(),
427+
takerIdentity.getId(),
428+
bisqEasyOffer.getId());
427429
if (!DevMode.isDevMode() && mediator.isEmpty()) {
428430
new Popup().warning(Res.get("bisqEasy.takeOffer.noMediatorAvailable.warning"))
429431
.closeButtonText(Res.get("action.cancel"))

http-api/src/main/java/bisq/http_api/rest_api/domain/trades/TradeRestApi.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ public void takeOffer(TakeOfferRequest request, @Suspended AsyncResponse asyncRe
141141
BitcoinPaymentMethodSpec bitcoinPaymentMethodSpec = new BitcoinPaymentMethodSpec(bitcoinPaymentMethod);
142142
FiatPaymentMethod fiatPaymentMethod = PaymentMethodSpecUtil.getFiatPaymentMethod(request.fiatPaymentMethod());
143143
FiatPaymentMethodSpec fiatPaymentMethodSpec = new FiatPaymentMethodSpec(fiatPaymentMethod);
144-
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(), takerIdentity.getId());
144+
Optional<UserProfile> mediator = mediationRequestService.selectMediator(bisqEasyOffer.getMakersUserProfileId(),
145+
takerIdentity.getId(),
146+
bisqEasyOffer.getId());
145147
PriceSpec makersPriceSpec = bisqEasyOffer.getPriceSpec();
146148
long marketPrice = marketPriceService.findMarketPrice(bisqEasyOffer.getMarket())
147149
.map(e -> e.getPriceQuote().getValue())

support/src/main/java/bisq/support/mediation/MediationRequestService.java

+46-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import bisq.chat.bisq_easy.open_trades.BisqEasyOpenTradeChannelService;
2727
import bisq.common.application.Service;
2828
import bisq.common.observable.Pin;
29+
import bisq.common.util.DateUtils;
2930
import bisq.contract.bisq_easy.BisqEasyContract;
3031
import bisq.i18n.Res;
3132
import bisq.network.NetworkService;
@@ -41,6 +42,7 @@
4142
import com.google.common.primitives.Ints;
4243
import lombok.extern.slf4j.Slf4j;
4344

45+
import java.nio.ByteBuffer;
4446
import java.nio.charset.StandardCharsets;
4547
import java.util.*;
4648
import java.util.concurrent.CompletableFuture;
@@ -54,6 +56,8 @@
5456
*/
5557
@Slf4j
5658
public class MediationRequestService implements Service, ConfidentialMessageService.Listener {
59+
public final static Date MEDIATION_SELECTION_V1_ACTIVATION_DATE = DateUtils.getUTCDate(2025, GregorianCalendar.JUNE, 1);
60+
5761
private final NetworkService networkService;
5862
private final UserProfileService userProfileService;
5963
private final BisqEasyOpenTradeChannelService bisqEasyOpenTradeChannelService;
@@ -134,36 +138,69 @@ public void requestMediation(BisqEasyOpenTradeChannel channel,
134138
myUserIdentity.getNetworkIdWithKeyPair());
135139
}
136140

137-
public Optional<UserProfile> selectMediator(String makersUserProfileId, String takersUserProfileId) {
141+
public Optional<UserProfile> selectMediator(String makersUserProfileId,
142+
String takersUserProfileId,
143+
String offerId) {
138144
Set<AuthorizedBondedRole> mediators = authorizedBondedRolesService.getAuthorizedBondedRoleStream()
139145
.filter(role -> role.getBondedRoleType() == BondedRoleType.MEDIATOR)
140146
.filter(role -> !role.getProfileId().equals(makersUserProfileId) &&
141147
!role.getProfileId().equals(takersUserProfileId))
142148
.collect(Collectors.toSet());
143-
return selectMediator(mediators, makersUserProfileId, takersUserProfileId);
149+
return selectMediator(mediators, makersUserProfileId, takersUserProfileId, offerId);
144150
}
145151

146152
// This method can be used for verification when taker provides mediators list.
147153
// If mediator list was not matching the expected one present in the network it might have been a manipulation attempt.
148154
public Optional<UserProfile> selectMediator(Set<AuthorizedBondedRole> mediators,
149155
String makersProfileId,
150-
String takersProfileId) {
156+
String takersProfileId,
157+
String offerId) {
151158
if (mediators.isEmpty()) {
152159
return Optional.empty();
153160
}
154-
int index;
161+
155162
if (mediators.size() == 1) {
156-
index = 0;
157-
} else {
158-
String combined = makersProfileId + takersProfileId;
159-
int space = Math.abs(Ints.fromByteArray(DigestUtil.hash(combined.getBytes(StandardCharsets.UTF_8))));
160-
index = space % mediators.size();
163+
return userProfileService.findUserProfile(mediators.iterator().next().getProfileId());
161164
}
165+
166+
int index = new Date().after(MEDIATION_SELECTION_V1_ACTIVATION_DATE)
167+
? getDeterministicIndex_V1(mediators, makersProfileId, takersProfileId, offerId)
168+
: getDeterministicIndex_V0(mediators, makersProfileId, takersProfileId);
169+
162170
ArrayList<AuthorizedBondedRole> list = new ArrayList<>(mediators);
163171
list.sort(Comparator.comparing(AuthorizedBondedRole::getProfileId));
164172
return userProfileService.findUserProfile(list.get(index).getProfileId());
165173
}
166174

175+
private int getDeterministicIndex_V0(Set<AuthorizedBondedRole> mediators,
176+
String makersProfileId,
177+
String takersProfileId) {
178+
try {
179+
String combined = makersProfileId + takersProfileId;
180+
int space = Math.abs(Ints.fromByteArray(DigestUtil.hash(combined.getBytes(StandardCharsets.UTF_8))));
181+
return space % mediators.size();
182+
} catch (Exception e) {
183+
log.error("getDeterministicIndex_V0 failed", e);
184+
return 0;
185+
}
186+
}
187+
188+
private int getDeterministicIndex_V1(Set<AuthorizedBondedRole> mediators,
189+
String makersProfileId,
190+
String takersProfileId,
191+
String offerId) {
192+
String input = makersProfileId + takersProfileId + offerId;
193+
byte[] hash = DigestUtil.hash(input.getBytes(StandardCharsets.UTF_8)); // returns 20 bytes
194+
// XOR multiple 4-byte chunks to use more of the hash
195+
ByteBuffer buffer = ByteBuffer.wrap(hash);
196+
int space = buffer.getInt(); // First 4 bytes
197+
space ^= buffer.getInt(); // XOR with next 4 bytes
198+
space ^= buffer.getInt(); // XOR with next 4 bytes
199+
space ^= buffer.getInt(); // XOR with next 4 bytes
200+
space ^= buffer.getInt(); // XOR with last 4 bytes (20 bytes total)
201+
return Math.floorMod(space, mediators.size());
202+
}
203+
167204

168205
/* --------------------------------------------------------------------- */
169206
// Private

trade/src/main/java/bisq/trade/bisq_easy/protocol/messages/BisqEasyTakeOfferRequestHandler.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ protected void verifyMessage(BisqEasyTakeOfferRequest message) {
124124
checkArgument(takersOffer.getQuoteSidePaymentMethodSpecs().contains(takersContract.getQuoteSidePaymentMethodSpec()));
125125

126126
Optional<UserProfile> mediator = serviceProvider.getSupportService().getMediationRequestService()
127-
.selectMediator(takersOffer.getMakersUserProfileId(), trade.getTaker().getNetworkId().getId());
127+
.selectMediator(takersOffer.getMakersUserProfileId(),
128+
trade.getTaker().getNetworkId().getId(),
129+
trade.getOffer().getId());
128130
checkArgument(mediator.map(UserProfile::getNetworkId).equals(takersContract.getMediator().map(UserProfile::getNetworkId)), "Mediators do not match. " +
129131
"\nmediator=" + mediator +
130132
"\ntakersContract.getMediator()=" + takersContract.getMediator());

0 commit comments

Comments
 (0)