|
26 | 26 | import bisq.chat.bisq_easy.open_trades.BisqEasyOpenTradeChannelService;
|
27 | 27 | import bisq.common.application.Service;
|
28 | 28 | import bisq.common.observable.Pin;
|
| 29 | +import bisq.common.util.DateUtils; |
29 | 30 | import bisq.contract.bisq_easy.BisqEasyContract;
|
30 | 31 | import bisq.i18n.Res;
|
31 | 32 | import bisq.network.NetworkService;
|
|
41 | 42 | import com.google.common.primitives.Ints;
|
42 | 43 | import lombok.extern.slf4j.Slf4j;
|
43 | 44 |
|
| 45 | +import java.nio.ByteBuffer; |
44 | 46 | import java.nio.charset.StandardCharsets;
|
45 | 47 | import java.util.*;
|
46 | 48 | import java.util.concurrent.CompletableFuture;
|
|
54 | 56 | */
|
55 | 57 | @Slf4j
|
56 | 58 | public class MediationRequestService implements Service, ConfidentialMessageService.Listener {
|
| 59 | + public final static Date MEDIATION_SELECTION_V1_ACTIVATION_DATE = DateUtils.getUTCDate(2025, GregorianCalendar.JUNE, 1); |
| 60 | + |
57 | 61 | private final NetworkService networkService;
|
58 | 62 | private final UserProfileService userProfileService;
|
59 | 63 | private final BisqEasyOpenTradeChannelService bisqEasyOpenTradeChannelService;
|
@@ -134,36 +138,69 @@ public void requestMediation(BisqEasyOpenTradeChannel channel,
|
134 | 138 | myUserIdentity.getNetworkIdWithKeyPair());
|
135 | 139 | }
|
136 | 140 |
|
137 |
| - public Optional<UserProfile> selectMediator(String makersUserProfileId, String takersUserProfileId) { |
| 141 | + public Optional<UserProfile> selectMediator(String makersUserProfileId, |
| 142 | + String takersUserProfileId, |
| 143 | + String offerId) { |
138 | 144 | Set<AuthorizedBondedRole> mediators = authorizedBondedRolesService.getAuthorizedBondedRoleStream()
|
139 | 145 | .filter(role -> role.getBondedRoleType() == BondedRoleType.MEDIATOR)
|
140 | 146 | .filter(role -> !role.getProfileId().equals(makersUserProfileId) &&
|
141 | 147 | !role.getProfileId().equals(takersUserProfileId))
|
142 | 148 | .collect(Collectors.toSet());
|
143 |
| - return selectMediator(mediators, makersUserProfileId, takersUserProfileId); |
| 149 | + return selectMediator(mediators, makersUserProfileId, takersUserProfileId, offerId); |
144 | 150 | }
|
145 | 151 |
|
146 | 152 | // This method can be used for verification when taker provides mediators list.
|
147 | 153 | // If mediator list was not matching the expected one present in the network it might have been a manipulation attempt.
|
148 | 154 | public Optional<UserProfile> selectMediator(Set<AuthorizedBondedRole> mediators,
|
149 | 155 | String makersProfileId,
|
150 |
| - String takersProfileId) { |
| 156 | + String takersProfileId, |
| 157 | + String offerId) { |
151 | 158 | if (mediators.isEmpty()) {
|
152 | 159 | return Optional.empty();
|
153 | 160 | }
|
154 |
| - int index; |
| 161 | + |
155 | 162 | 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()); |
161 | 164 | }
|
| 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 | + |
162 | 170 | ArrayList<AuthorizedBondedRole> list = new ArrayList<>(mediators);
|
163 | 171 | list.sort(Comparator.comparing(AuthorizedBondedRole::getProfileId));
|
164 | 172 | return userProfileService.findUserProfile(list.get(index).getProfileId());
|
165 | 173 | }
|
166 | 174 |
|
| 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 | + |
167 | 204 |
|
168 | 205 | /* --------------------------------------------------------------------- */
|
169 | 206 | // Private
|
|
0 commit comments