select online, registered, and least used arbitrator (#400)

support registering and unregistering arbitrators over grpc
maker always sends InitTradeRequest to arbitrator
share original contract for comparision
remove backup arbitator from model
cleanup trade states
This commit is contained in:
woodser 2022-08-13 09:47:33 -04:00 committed by GitHub
parent 757c7cf19c
commit 3727d12ef6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 472 additions and 382 deletions

View file

@ -14,7 +14,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInfo;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static bisq.core.trade.Trade.Phase.DEPOSIT_UNLOCKED;
import static bisq.core.trade.Trade.Phase.DEPOSITS_UNLOCKED;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG;
@ -80,7 +80,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
String tradeId) {
Predicate<TradeInfo> isTradeInDepositUnlockedStateAndPhase = (t) ->
t.getState().equals(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN.name())
&& t.getPhase().equals(DEPOSIT_UNLOCKED.name());
&& t.getPhase().equals(DEPOSITS_UNLOCKED.name());
String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
@ -95,7 +95,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
genBtcBlocksThenWait(1, 4_000);
} else {
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN)
.setPhase(DEPOSIT_UNLOCKED)
.setPhase(DEPOSITS_UNLOCKED)
.setDepositPublished(true)
.setDepositConfirmed(true);
verifyExpectedProtocolStatus(trade);

View file

@ -406,8 +406,12 @@ public class CoreApi {
// Dispute Agents
///////////////////////////////////////////////////////////////////////////////////////////
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey);
public void registerDisputeAgent(String disputeAgentType, String registrationKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey, resultHandler, errorMessageHandler);
}
public void unregisterDisputeAgent(String disputeAgentType, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
coreDisputeAgentsService.unregisterDisputeAgent(disputeAgentType, resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -31,7 +31,8 @@ import bisq.network.p2p.P2PService;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.ECKey;
import javax.inject.Inject;
@ -88,14 +89,10 @@ class CoreDisputeAgentsService {
this.languageCodes = asList("de", "en", "es", "fr");
}
void registerDisputeAgent(String disputeAgentType, String registrationKey) {
void registerDisputeAgent(String disputeAgentType, String registrationKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (!p2PService.isBootstrapped())
throw new IllegalStateException("p2p service is not bootstrapped yet");
if (config.baseCurrencyNetwork.isMainnet()
|| !config.useLocalhostForP2P)
throw new IllegalStateException("dispute agents must be registered in a Bisq UI");
Optional<SupportType> supportType = getSupportType(disputeAgentType);
if (supportType.isPresent()) {
ECKey ecKey;
@ -104,16 +101,18 @@ class CoreDisputeAgentsService {
case ARBITRATION:
if (user.getRegisteredArbitrator() != null) {
log.warn("ignoring request to re-register as arbitrator");
resultHandler.handleResult();
return;
}
ecKey = arbitratorManager.getRegistrationKey(registrationKey);
if (ecKey == null) throw new IllegalStateException("invalid registration key");
signature = arbitratorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey));
registerArbitrator(nodeAddress, languageCodes, ecKey, signature);
registerArbitrator(nodeAddress, languageCodes, ecKey, signature, resultHandler, errorMessageHandler);
return;
case MEDIATION:
if (user.getRegisteredMediator() != null) {
log.warn("ignoring request to re-register as mediator");
resultHandler.handleResult();
return;
}
ecKey = mediatorManager.getRegistrationKey(registrationKey);
@ -124,6 +123,7 @@ class CoreDisputeAgentsService {
case REFUND:
if (user.getRegisteredRefundAgent() != null) {
log.warn("ignoring request to re-register as refund agent");
resultHandler.handleResult();
return;
}
ecKey = refundAgentManager.getRegistrationKey(registrationKey);
@ -139,10 +139,38 @@ class CoreDisputeAgentsService {
}
}
void unregisterDisputeAgent(String disputeAgentType, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (!p2PService.isBootstrapped())
throw new IllegalStateException("p2p service is not bootstrapped yet");
Optional<SupportType> supportType = getSupportType(disputeAgentType);
if (supportType.isPresent()) {
switch (supportType.get()) {
case ARBITRATION:
if (user.getRegisteredArbitrator() == null) {
errorMessageHandler.handleErrorMessage("User is not arbitrator");
return;
}
unregisterDisputeAgent(resultHandler, errorMessageHandler);
return;
case MEDIATION:
throw new IllegalStateException("unregister mediator not implemented");
case REFUND:
throw new IllegalStateException("unregister refund agent not implemented");
case TRADE:
throw new IllegalArgumentException("trade agent registration not supported");
}
} else {
throw new IllegalArgumentException(format("unknown dispute agent type '%s'", disputeAgentType));
}
}
private void registerArbitrator(NodeAddress nodeAddress,
List<String> languageCodes,
ECKey ecKey,
String signature) {
String signature,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
Arbitrator arbitrator = new Arbitrator(
p2PService.getAddress(),
xmrWalletService.getWallet().getPrimaryAddress(), // TODO: how is this used?
@ -155,10 +183,9 @@ class CoreDisputeAgentsService {
null,
null);
arbitratorManager.addDisputeAgent(arbitrator, () -> {
}, errorMessage -> {
});
arbitratorManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() ->
new IllegalStateException("could not register arbitrator"));
if (!arbitratorManager.getDisputeAgentByNodeAddress(nodeAddress).isPresent()) errorMessageHandler.handleErrorMessage("could not register arbitrator");
else resultHandler.handleResult();
}, errorMessageHandler);
}
private void registerMediator(NodeAddress nodeAddress,
@ -219,4 +246,10 @@ class CoreDisputeAgentsService {
return Optional.empty();
}
}
private void unregisterDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
arbitratorManager.removeDisputeAgent(resultHandler, errorMesage -> {
errorMessageHandler.handleErrorMessage("Error unregistering dispute agent: " + errorMesage);
});
}
}

View file

@ -74,6 +74,8 @@ public class OfferInfo implements Payload {
private final String pubKeyRing;
private final String versionNumber;
private final int protocolVersion;
@Nullable
private final String arbitratorSigner;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId();
@ -104,6 +106,7 @@ public class OfferInfo implements Payload {
this.pubKeyRing = builder.getPubKeyRing();
this.versionNumber = builder.getVersionNumber();
this.protocolVersion = builder.getProtocolVersion();
this.arbitratorSigner = builder.getArbitratorSigner();
}
public static OfferInfo toOfferInfo(Offer offer) {
@ -166,7 +169,8 @@ public class OfferInfo implements Payload {
.withOwnerNodeAddress(offer.getOfferPayload().getOwnerNodeAddress().getFullAddress())
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
.withVersionNumber(offer.getOfferPayload().getVersionNr())
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion());
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -203,6 +207,7 @@ public class OfferInfo implements Payload {
.setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId);
return builder.build();
}
@ -238,6 +243,7 @@ public class OfferInfo implements Payload {
.withPubKeyRing(proto.getPubKeyRing())
.withVersionNumber(proto.getVersionNr())
.withProtocolVersion(proto.getProtocolVersion())
.withArbitratorSigner(proto.getArbitratorSigner())
.build();
}
}

View file

@ -46,6 +46,11 @@ public class TradeInfo implements Payload {
? ""
: trade.getTradingPeerNodeAddress().getFullAddress();
private static final Function<Trade, String> toArbitratorNodeAddress = (trade) ->
trade.getArbitratorNodeAddress() == null
? ""
: trade.getArbitratorNodeAddress().getFullAddress();
private static final Function<Trade, String> toRoundedVolume = (trade) ->
trade.getVolume() == null
? ""
@ -70,6 +75,7 @@ public class TradeInfo implements Payload {
private final long amountAsLong;
private final String price;
private final String volume;
private final String arbitratorNodeAddress;
private final String tradingPeerNodeAddress;
private final String state;
private final String phase;
@ -98,6 +104,7 @@ public class TradeInfo implements Payload {
this.amountAsLong = builder.getAmountAsLong();
this.price = builder.getPrice();
this.volume = builder.getVolume();
this.arbitratorNodeAddress = builder.getArbitratorNodeAddress();
this.tradingPeerNodeAddress = builder.getTradingPeerNodeAddress();
this.state = builder.getState();
this.phase = builder.getPhase();
@ -149,6 +156,7 @@ public class TradeInfo implements Payload {
.withAmountAsLong(trade.getAmountAsLong())
.withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade))
.withArbitratorNodeAddress(toArbitratorNodeAddress.apply(trade))
.withTradingPeerNodeAddress(toPeerNodeAddress.apply(trade))
.withState(trade.getState().name())
.withPhase(trade.getPhase().name())
@ -186,6 +194,7 @@ public class TradeInfo implements Payload {
.setAmountAsLong(amountAsLong)
.setPrice(price)
.setTradeVolume(volume)
.setArbitratorNodeAddress(arbitratorNodeAddress)
.setTradingPeerNodeAddress(tradingPeerNodeAddress)
.setState(state)
.setPhase(phase)
@ -220,6 +229,7 @@ public class TradeInfo implements Payload {
.withPeriodState(proto.getPeriodState())
.withState(proto.getState())
.withPhase(proto.getPhase())
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
.withIsDepositPublished(proto.getIsDepositPublished())
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
@ -247,6 +257,7 @@ public class TradeInfo implements Payload {
", payoutTxId='" + payoutTxId + '\'' + "\n" +
", amountAsLong='" + amountAsLong + '\'' + "\n" +
", price='" + price + '\'' + "\n" +
", arbitratorNodeAddress='" + arbitratorNodeAddress + '\'' + "\n" +
", tradingPeerNodeAddress='" + tradingPeerNodeAddress + '\'' + "\n" +
", state='" + state + '\'' + "\n" +
", phase='" + phase + '\'' + "\n" +

View file

@ -61,6 +61,7 @@ public final class OfferInfoBuilder {
private String pubKeyRing;
private String versionNumber;
private int protocolVersion;
private String arbitratorSigner;
public OfferInfoBuilder withId(String id) {
this.id = id;
@ -217,6 +218,11 @@ public final class OfferInfoBuilder {
return this;
}
public OfferInfoBuilder withArbitratorSigner(String arbitratorSigner) {
this.arbitratorSigner = arbitratorSigner;
return this;
}
public OfferInfo build() {
return new OfferInfo(this);
}

View file

@ -47,6 +47,7 @@ public final class TradeInfoV1Builder {
private long amountAsLong;
private String price;
private String volume;
private String arbitratorNodeAddress;
private String tradingPeerNodeAddress;
private String state;
private String phase;
@ -151,6 +152,11 @@ public final class TradeInfoV1Builder {
return this;
}
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
this.arbitratorNodeAddress = arbitratorNodeAddress;
return this;
}
public TradeInfoV1Builder withTradingPeerNodeAddress(String tradingPeerNodeAddress) {
this.tradingPeerNodeAddress = tradingPeerNodeAddress;
return this;

View file

@ -72,7 +72,7 @@ public class TradeEvents {
case DEPOSIT_REQUESTED:
case DEPOSITS_PUBLISHED:
break;
case DEPOSIT_UNLOCKED:
case DEPOSITS_UNLOCKED:
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
break;

View file

@ -154,14 +154,6 @@ public class CreateOfferService {
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode();
String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
List<NodeAddress> acceptedArbitratorAddresses = user.getAcceptedArbitratorAddresses();
ArrayList<NodeAddress> arbitratorNodeAddresses = acceptedArbitratorAddresses != null ?
Lists.newArrayList(acceptedArbitratorAddresses) :
new ArrayList<>();
List<NodeAddress> acceptedMediatorAddresses = user.getAcceptedMediatorAddresses();
ArrayList<NodeAddress> mediatorNodeAddresses = acceptedMediatorAddresses != null ?
Lists.newArrayList(acceptedMediatorAddresses) :
new ArrayList<>();
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
@ -191,10 +183,6 @@ public class CreateOfferService {
currencyCode,
makerFeeAsCoin);
// select signing arbitrator
Arbitrator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager);
if (arbitrator == null) throw new RuntimeException("No arbitrators available");
OfferPayload offerPayload = new OfferPayload(offerId,
creationTime,
makerAddress,
@ -230,7 +218,7 @@ public class CreateOfferService {
hashOfChallenge,
extraDataMap,
Version.TRADE_PROTOCOL_VERSION,
arbitrator.getNodeAddress(),
null,
null,
null);
Offer offer = new Offer(offerPayload);

View file

@ -78,6 +78,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// address and signature of signing arbitrator
@Setter
@Nullable
protected NodeAddress arbitratorSigner;
@Setter
@Nullable
@ -192,7 +193,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
@Nullable String hashOfChallenge,
@Nullable Map<String, String> extraDataMap,
int protocolVersion,
NodeAddress arbitratorSigner,
@Nullable NodeAddress arbitratorSigner,
@Nullable String arbitratorSignature,
@Nullable List<String> reserveTxKeyImages) {
this.id = id;
@ -297,8 +298,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
.setLowerClosePrice(lowerClosePrice)
.setUpperClosePrice(upperClosePrice)
.setIsPrivateOffer(isPrivateOffer)
.setProtocolVersion(protocolVersion)
.setArbitratorSigner(arbitratorSigner.toProtoMessage());
.setProtocolVersion(protocolVersion);
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(offerFeePaymentTxId).ifPresent(builder::setOfferFeePaymentTxId);
Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode);
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
@ -356,7 +357,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
hashOfChallenge,
extraDataMapMap,
proto.getProtocolVersion(),
NodeAddress.fromProto(proto.getArbitratorSigner()),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()),
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
}

View file

@ -19,8 +19,6 @@ package bisq.core.offer;
import bisq.core.trade.Tradable;
import bisq.network.p2p.NodeAddress;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.proto.ProtoUtil;
@ -55,10 +53,6 @@ public final class OpenOffer implements Tradable {
private final Offer offer;
@Getter
private State state;
@Getter
@Setter
@Nullable
private NodeAddress backupArbitrator;
@Setter
@Getter
private boolean autoSplit;
@ -113,7 +107,6 @@ public final class OpenOffer implements Tradable {
private OpenOffer(Offer offer,
State state,
@Nullable NodeAddress backupArbitrator,
long triggerPrice,
boolean autoSplit,
@Nullable String scheduledAmount,
@ -123,7 +116,6 @@ public final class OpenOffer implements Tradable {
@Nullable String reserveTxKey) {
this.offer = offer;
this.state = state;
this.backupArbitrator = backupArbitrator;
this.triggerPrice = triggerPrice;
this.autoSplit = autoSplit;
this.scheduledTxHashes = scheduledTxHashes;
@ -144,7 +136,6 @@ public final class OpenOffer implements Tradable {
.setAutoSplit(autoSplit);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(backupArbitrator).ifPresent(nodeAddress -> builder.setBackupArbitrator(nodeAddress.toProtoMessage()));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
@ -156,7 +147,6 @@ public final class OpenOffer implements Tradable {
public static Tradable fromProto(protobuf.OpenOffer proto) {
OpenOffer openOffer = new OpenOffer(Offer.fromProto(proto.getOffer()),
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null,
proto.getTriggerPrice(),
proto.getAutoSplit(),
proto.getScheduledAmount(),
@ -227,7 +217,6 @@ public final class OpenOffer implements Tradable {
return "OpenOffer{" +
",\n offer=" + offer +
",\n state=" + state +
",\n arbitratorNodeAddress=" + backupArbitrator +
",\n triggerPrice=" + triggerPrice +
"\n}";
}

View file

@ -24,7 +24,6 @@ import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.exceptions.TradePriceOutOfToleranceException;
import bisq.core.filter.FilterManager;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.offer.messages.OfferAvailabilityRequest;
import bisq.core.offer.messages.OfferAvailabilityResponse;
import bisq.core.offer.messages.SignOfferRequest;
@ -703,6 +702,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle result
resultHandler.handleResult(null);
} catch (Exception e) {
e.printStackTrace();
errorMessageHandler.handleErrorMessage(e.getMessage());
}
}
@ -760,7 +760,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// set reserve tx on open offer
openOffer.setReserveTxHash(model.getReserveTx().getHash());
openOffer.setReserveTxHex(model.getReserveTx().getHash());
openOffer.setReserveTxHex(model.getReserveTx().getFullHex());
openOffer.setReserveTxKey(model.getReserveTx().getKey());
// set offer state
@ -948,7 +948,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
AvailabilityResult availabilityResult;
String makerSignature = null;
NodeAddress backupArbitratorNodeAddress = null;
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
if (!apiUserDeniedByOffer(request)) {
@ -957,12 +956,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Offer offer = openOffer.getOffer();
if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) {
// set backup arbitrator if signer is not available
Mediator backupMediator = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, mediatorManager, offer.getOfferPayload().getArbitratorSigner());
backupArbitratorNodeAddress = backupMediator == null ? null : backupMediator.getNodeAddress();
openOffer.setBackupArbitrator(backupArbitratorNodeAddress);
// maker signs taker's request // TODO (woodser): should maker signature include selected arbitrator?
// maker signs taker's request
String tradeRequestAsJson = JsonUtil.objectToJson(request.getTradeRequest());
makerSignature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), tradeRequestAsJson);
@ -1005,8 +999,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId,
availabilityResult,
makerSignature,
backupArbitratorNodeAddress);
makerSignature);
log.info("Send {} with offerId {} and uid {} to peer {}",
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(),
offerAvailabilityResponse.getUid(), peer);

View file

@ -53,7 +53,7 @@ public class DisputeAgentSelection {
public static <T extends DisputeAgent> T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager,
NodeAddress excludedArbitrator) {
Set<NodeAddress> excludedArbitrator) {
return getLeastUsedDisputeAgent(tradeStatisticsManager,
disputeAgentManager,
excludedArbitrator);
@ -61,7 +61,7 @@ public class DisputeAgentSelection {
private static <T extends DisputeAgent> T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager,
NodeAddress excludedDisputeAgent) {
Set<NodeAddress> excludedDisputeAgents) {
// We take last 100 entries from trade statistics
List<TradeStatistics3> list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
@ -81,7 +81,7 @@ public class DisputeAgentSelection {
.map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress())
.collect(Collectors.toSet());
if (excludedDisputeAgent != null) disputeAgents.remove(excludedDisputeAgent.getFullAddress());
if (excludedDisputeAgents != null) disputeAgents.removeAll(excludedDisputeAgents.stream().map(NodeAddress::getFullAddress).collect(Collectors.toList()));
if (disputeAgents.isEmpty()) return null;
String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents);

View file

@ -65,9 +65,6 @@ public class OfferAvailabilityModel implements Model {
@Setter
@Getter
private String makerSignature;
@Setter
@Getter
private NodeAddress backupArbitrator;
// Added in v1.5.5
@Getter

View file

@ -62,7 +62,6 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
offer.setState(Offer.State.AVAILABLE);
model.setMakerSignature(offerAvailabilityResponse.getMakerSignature());
model.setBackupArbitrator(offerAvailabilityResponse.getBackupArbitrator());
checkNotNull(model.getMakerSignature());
complete();

View file

@ -20,7 +20,6 @@ package bisq.core.offer.messages;
import bisq.core.offer.AvailabilityResult;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SupportedCapabilitiesMessage;
import bisq.common.app.Capabilities;
@ -46,19 +45,16 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
@Nullable
private final String makerSignature;
private final NodeAddress backupArbitrator;
public OfferAvailabilityResponse(String offerId,
AvailabilityResult availabilityResult,
String makerSignature,
NodeAddress backupArbitrator) {
String makerSignature) {
this(offerId,
availabilityResult,
Capabilities.app,
Version.getP2PMessageVersion(),
UUID.randomUUID().toString(),
makerSignature,
backupArbitrator);
makerSignature);
}
@ -71,13 +67,11 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
@Nullable Capabilities supportedCapabilities,
String messageVersion,
@Nullable String uid,
String makerSignature,
NodeAddress arbitratorNodeAddress) {
String makerSignature) {
super(messageVersion, offerId, uid);
this.availabilityResult = availabilityResult;
this.supportedCapabilities = supportedCapabilities;
this.makerSignature = makerSignature;
this.backupArbitrator = arbitratorNodeAddress;
}
@Override
@ -89,7 +83,6 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(makerSignature));
Optional.ofNullable(backupArbitrator).ifPresent(nodeAddress -> builder.setBackupArbitrator(nodeAddress.toProtoMessage()));
return getNetworkEnvelopeBuilder()
.setOfferAvailabilityResponse(builder)
@ -102,7 +95,6 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
messageVersion,
proto.getUid().isEmpty() ? null : proto.getUid(),
proto.getMakerSignature().isEmpty() ? null : proto.getMakerSignature(),
proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
proto.getMakerSignature().isEmpty() ? null : proto.getMakerSignature());
}
}

View file

@ -17,13 +17,15 @@
package bisq.core.offer.placeoffer.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.app.Version;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.taskrunner.Task;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.offer.messages.SignOfferRequest;
import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
@ -34,6 +36,8 @@ import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -50,12 +54,10 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
@Override
protected void run() {
Offer offer = model.getOffer();
try {
runInterceptHook();
// create request for arbitrator to sign offer
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
SignOfferRequest request = new SignOfferRequest(
@ -73,45 +75,14 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress);
// get signing arbitrator
Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorNodeAddress) must not be null");
// complete on successful ack message
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
@Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
if (!(decryptedMessageWithPubKey.getNetworkEnvelope() instanceof AckMessage)) return;
if (!sender.equals(arbitrator.getNodeAddress())) return;
AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope();
if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return;
if (!ackMessage.getSourceUid().equals(request.getUid())) return;
if (ackMessage.isSuccess()) {
offer.setState(Offer.State.OFFER_FEE_RESERVED);
model.getP2PService().removeDecryptedDirectMessageListener(this);
complete();
} else {
if (!failed) {
failed = true;
failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task?
}
}
}
};
model.getP2PService().addDecryptedDirectMessageListener(ackListener);
// send request
model.getP2PService().sendEncryptedDirectMessage(arbitrator.getNodeAddress(), arbitrator.getPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: arbitrator={}; offerId={}; uid={}", request.getClass().getSimpleName(), arbitrator.getNodeAddress(), offer.getId());
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), arbitrator.getNodeAddress(), offer.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
});
// send request to least used arbitrators until success
sendSignOfferRequests(request, () -> {
complete();
}, (errorMessage) -> {
log.warn("Error signing offer: " + errorMessage);
appendToErrorMessage("Error signing offer: " + errorMessage);
failed(errorMessage);
});
} catch (Throwable t) {
offer.setErrorMessage("An error occurred.\n" +
"Error message:\n"
@ -119,4 +90,77 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
failed(t);
}
}
private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager());
sendSignOfferRequests(request, leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), resultHandler, errorMessageHandler);
}
private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// complete on successful ack message
DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() {
@Override
public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) {
if (!(decryptedMessageWithPubKey.getNetworkEnvelope() instanceof AckMessage)) return;
if (!sender.equals(arbitratorNodeAddress)) return;
AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope();
if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return;
if (!ackMessage.getSourceUid().equals(request.getUid())) return;
if (ackMessage.isSuccess()) {
model.getP2PService().removeDecryptedDirectMessageListener(this);
model.getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
model.getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
resultHandler.handleResult();
} else {
log.warn("Arbitrator nacked request: {}", errorMessage);
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
}
}
};
model.getP2PService().addDecryptedDirectMessageListener(ackListener);
// send sign offer request
sendSignOfferRequest(request, arbitratorNodeAddress, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", request.getClass().getSimpleName(), model.getOffer().getId());
}
// if unavailable, try alternative arbitrator
@Override
public void onFault(String errorMessage) {
log.warn("Arbitrator unavailable: {}", errorMessage);
handleArbitratorFailure(request, arbitratorNodeAddress, excludedArbitrators, resultHandler, errorMessageHandler);
}
});
}
private void sendSignOfferRequest(SignOfferRequest request, NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) {
// get registered arbitrator
Arbitrator arbitrator = model.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress);
if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator");
request.getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", request.getClass().getSimpleName(), request.getOfferId(), request.getUid(), arbitratorNodeAddress);
model.getP2PService().sendEncryptedDirectMessage(
arbitratorNodeAddress,
arbitrator.getPubKeyRing(),
request,
listener
);
}
private void handleArbitratorFailure(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
excludedArbitrators.add(arbitratorNodeAddress);
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager(), excludedArbitrators);
if (altArbitrator == null) {
errorMessageHandler.handleErrorMessage("Offer could not be signed by any arbitrator");
return;
}
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
}
}

View file

@ -47,7 +47,6 @@ import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -94,13 +93,12 @@ public abstract class DisputeAgentManager<T extends DisputeAgent> {
public DisputeAgentManager(KeyRing keyRing,
DisputeAgentService<T> disputeAgentService,
User user,
FilterManager filterManager,
boolean useDevPrivilegeKeys) {
FilterManager filterManager) {
this.keyRing = keyRing;
this.disputeAgentService = disputeAgentService;
this.user = user;
this.filterManager = filterManager;
publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : getPubKeyList();
publicKeys = getPubKeyList();
}
@ -245,6 +243,8 @@ public abstract class DisputeAgentManager<T extends DisputeAgent> {
resultHandler.handleResult();
},
errorMessageHandler);
} else {
errorMessageHandler.handleErrorMessage("User is not registered dispute agent");
}
}

View file

@ -27,7 +27,6 @@ import bisq.common.crypto.KeyRing;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.inject.Named;
import java.util.ArrayList;
import java.util.List;
@ -41,16 +40,18 @@ public class ArbitratorManager extends DisputeAgentManager<Arbitrator> {
public ArbitratorManager(KeyRing keyRing,
ArbitratorService arbitratorService,
User user,
FilterManager filterManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(keyRing, arbitratorService, user, filterManager, useDevPrivilegeKeys);
FilterManager filterManager) {
super(keyRing, arbitratorService, user, filterManager);
}
@Override
protected List<String> getPubKeyList() {
switch (Config.baseCurrencyNetwork()) {
case XMR_LOCAL:
throw new RuntimeException("No arbitrator pub key list for local XMR testnet. Set useDevPrivilegeKeys=true");
return List.of(
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492",
"026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519");
case XMR_STAGENET:
return List.of(
"03bb559ce207a4deb51d4c705076c95b85ad8581d35936b2a422dcb504eaf7cdb0",

View file

@ -23,11 +23,9 @@ import bisq.core.user.User;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import javax.inject.Singleton;
import javax.inject.Named;
import javax.inject.Inject;
@ -40,9 +38,8 @@ public class MediatorManager extends DisputeAgentManager<Mediator> {
public MediatorManager(KeyRing keyRing,
MediatorService mediatorService,
User user,
FilterManager filterManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(keyRing, mediatorService, user, filterManager, useDevPrivilegeKeys);
FilterManager filterManager) {
super(keyRing, mediatorService, user, filterManager);
}
@Override

View file

@ -23,12 +23,10 @@ import bisq.core.user.User;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.inject.Named;
import java.util.List;
@ -42,9 +40,8 @@ public class RefundAgentManager extends DisputeAgentManager<RefundAgent> {
public RefundAgentManager(KeyRing keyRing,
RefundAgentService refundAgentService,
User user,
FilterManager filterManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(keyRing, refundAgentService, user, filterManager, useDevPrivilegeKeys);
FilterManager filterManager) {
super(keyRing, refundAgentService, user, filterManager);
}
@Override

View file

@ -127,7 +127,7 @@ public abstract class Trade implements Tradable, Model {
// deposit confirmed (TODO)
// deposit unlocked
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN(Phase.DEPOSIT_UNLOCKED),
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN(Phase.DEPOSITS_UNLOCKED),
// payment sent
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT(Phase.PAYMENT_SENT),
@ -191,7 +191,7 @@ public abstract class Trade implements Tradable, Model {
INIT,
DEPOSIT_REQUESTED, // TODO (woodser): remove unused phases
DEPOSITS_PUBLISHED,
DEPOSIT_UNLOCKED, // TODO (woodser): rename to or add DEPOSIT_UNLOCKED
DEPOSITS_UNLOCKED,
PAYMENT_SENT,
PAYMENT_RECEIVED,
PAYOUT_PUBLISHED,
@ -1291,7 +1291,7 @@ public abstract class Trade implements Tradable, Model {
}
public boolean isDepositUnlocked() {
return getState().getPhase().ordinal() >= Phase.DEPOSIT_UNLOCKED.ordinal();
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
}
public boolean isPaymentSent() {

View file

@ -500,9 +500,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Offer offer = openOffer.getOffer();
// verify request is from signer or backup arbitrator
if (!sender.equals(offer.getOfferPayload().getArbitratorSigner()) && !sender.equals(openOffer.getBackupArbitrator())) { // TODO (woodser): get backup arbitrator from maker-signed InitTradeRequest and remove from OpenOffer
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from signer or backup arbitrator", sender, request.getTradeId());
// verify request is from arbitrator
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(sender);
if (arbitrator == null) {
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request is not from accepted arbitrator", sender, request.getTradeId());
return;
}
@ -762,7 +763,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
trade.getProcessModel().setTradeMessage(model.getTradeRequest());
trade.getProcessModel().setMakerSignature(model.getMakerSignature());
trade.getProcessModel().setBackupArbitrator(model.getBackupArbitrator()); // backup arbitrator only used if signer offline
trade.getProcessModel().setUseSavingsWallet(useSavingsWallet);
trade.getProcessModel().setFundsNeededForTradeAsLong(fundsNeededForTrade.value);
trade.setTakerPubKeyRing(model.getPubKeyRing());

View file

@ -30,14 +30,13 @@ import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class SignContractResponse extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing;
private final long currentDate;
private final String contractAsJson;
private final String contractSignature;
public SignContractResponse(String tradeId,
@ -46,11 +45,13 @@ public final class SignContractResponse extends TradeMessage implements DirectMe
String uid,
String messageVersion,
long currentDate,
String contractAsJson,
String contractSignature) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing;
this.currentDate = currentDate;
this.contractAsJson = contractAsJson;
this.contractSignature = contractSignature;
}
@ -67,6 +68,7 @@ public final class SignContractResponse extends TradeMessage implements DirectMe
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setUid(uid);
Optional.ofNullable(contractAsJson).ifPresent(e -> builder.setContractAsJson(contractAsJson));
Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(contractSignature));
builder.setCurrentDate(currentDate);
@ -83,6 +85,7 @@ public final class SignContractResponse extends TradeMessage implements DirectMe
proto.getUid(),
messageVersion,
proto.getCurrentDate(),
ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()),
ProtoUtil.stringOrNullFromProto(proto.getContractSignature()));
}
@ -92,6 +95,7 @@ public final class SignContractResponse extends TradeMessage implements DirectMe
"\n senderNodeAddress=" + senderNodeAddress +
",\n pubKeyRing=" + pubKeyRing +
",\n currentDate=" + currentDate +
",\n contractAsJson='" + contractAsJson +
",\n contractSignature='" + contractSignature +
"\n} " + super.toString();
}

View file

@ -8,7 +8,7 @@ import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeAndMultisigRequests;
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeOrMultisigRequests;
import bisq.core.trade.protocol.tasks.ArbitratorProcessesDepositRequest;
import bisq.core.trade.protocol.tasks.ArbitratorProcessesReserveTx;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
@ -42,7 +42,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
ApplyFilter.class,
ProcessInitTradeRequest.class,
ArbitratorProcessesReserveTx.class,
ArbitratorSendsInitTradeAndMultisigRequests.class)
ArbitratorSendsInitTradeOrMultisigRequests.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT);

View file

@ -26,9 +26,8 @@ import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.tasks.MakerSendsInitTradeRequestIfUnreserved;
import bisq.core.trade.protocol.tasks.MakerSendsInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
@ -65,7 +64,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
ProcessInitTradeRequest.class,
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
MakerSendsInitTradeRequestIfUnreserved.class)
MakerSendsInitTradeRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT);

View file

@ -90,7 +90,7 @@ public abstract class BuyerProtocol extends DisputeProtocol {
latchTrade();
this.errorMessageHandler = errorMessageHandler;
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
expect(phase(Trade.Phase.DEPOSIT_UNLOCKED)
expect(phase(Trade.Phase.DEPOSITS_UNLOCKED)
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(ApplyFilter.class,

View file

@ -58,7 +58,7 @@ public abstract class DisputeProtocol extends TradeProtocol {
// Trader has not yet received the peer's signature but has clicked the accept button.
public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
expect(anyPhase(Trade.Phase.DEPOSIT_UNLOCKED,
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(event)
@ -85,7 +85,7 @@ public abstract class DisputeProtocol extends TradeProtocol {
// Trader has already received the peer's signature and has clicked the accept button as well.
public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
expect(anyPhase(Trade.Phase.DEPOSIT_UNLOCKED,
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(event)
@ -113,7 +113,7 @@ public abstract class DisputeProtocol extends TradeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) {
expect(anyPhase(Trade.Phase.DEPOSIT_UNLOCKED,
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(message)
@ -123,7 +123,7 @@ public abstract class DisputeProtocol extends TradeProtocol {
}
protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) {
expect(anyPhase(Trade.Phase.DEPOSIT_UNLOCKED,
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(message)

View file

@ -160,9 +160,6 @@ public class ProcessModel implements Model, PersistablePayload {
@Getter
@Setter
private String makerSignature;
@Getter
@Setter
private NodeAddress backupArbitrator;
@Nullable
@Getter
@Setter
@ -231,7 +228,6 @@ public class ProcessModel implements Model, PersistablePayload {
Optional.ofNullable(myMultiSigPubKey).ifPresent(e -> builder.setMyMultiSigPubKey(ByteString.copyFrom(myMultiSigPubKey)));
Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage()));
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(makerSignature));
Optional.ofNullable(backupArbitrator).ifPresent(e -> builder.setBackupArbitrator(backupArbitrator.toProtoMessage()));
Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress));
return builder.build();
}
@ -260,7 +256,6 @@ public class ProcessModel implements Model, PersistablePayload {
processModel.setTempTradingPeerNodeAddress(proto.hasTempTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTempTradingPeerNodeAddress()) : null);
processModel.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature()));
processModel.setMakerSignature(proto.getMakerSignature());
processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress()));
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());

View file

@ -28,7 +28,7 @@ import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.MakerSendsInitTradeRequestIfUnreserved;
import bisq.core.trade.protocol.tasks.MakerSendsInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler;
@ -66,7 +66,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
ProcessInitTradeRequest.class,
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
MakerSendsInitTradeRequestIfUnreserved.class)
MakerSendsInitTradeRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT);
@ -123,7 +123,6 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
log.info("Received {} from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid());
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -139,6 +139,5 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
log.info("Received {} from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peer, message.getTradeId(), message.getUid());
}
}

View file

@ -86,7 +86,7 @@ public abstract class SellerProtocol extends DisputeProtocol {
return;
}
latchTrade();
expect(anyPhase(Trade.Phase.DEPOSIT_UNLOCKED, Trade.Phase.DEPOSITS_PUBLISHED)
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_PUBLISHED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,

View file

@ -17,16 +17,24 @@
package bisq.core.trade.protocol;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.HashSet;
import java.util.Set;
import bisq.common.app.Version;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -41,53 +49,54 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
protected void run() {
try {
runInterceptHook();
// send request to arbitrator
sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
}
// send request to backup arbitrator if signer unavailable
@Override
public void onFault(String errorMessage) {
log.info("Sending {} to signing arbitrator {} failed, using backup arbitrator {}. error={}", InitTradeRequest.class.getSimpleName(), trade.getOffer().getOfferPayload().getArbitratorSigner(), processModel.getBackupArbitrator(), errorMessage);
if (processModel.getBackupArbitrator() == null) {
log.warn("Cannot take offer because signing arbitrator is offline and backup arbitrator is null");
failed();
return;
}
sendInitTradeRequest(processModel.getBackupArbitrator(), new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at backup arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
}
@Override
public void onFault(String errorMessage) { // TODO (woodser): distinguish nack from offline
log.warn("Cannot take offer because arbitrators are unavailable: error={}.", InitTradeRequest.class.getSimpleName(), errorMessage);
failed();
}
});
}
// send request to signing arbitrator then least used arbitrators until success
sendInitTradeRequests(trade.getOffer().getOfferPayload().getArbitratorSigner(), new HashSet<NodeAddress>(), () -> {
complete();
}, (errorMessage) -> {
log.warn("Cannot initialize trade with arbitrators: " + errorMessage);
failed(errorMessage);
});
complete(); // TODO (woodser): onArrived() doesn't get called if arbitrator rejects concurrent requests. always complete before onArrived()?
} catch (Throwable t) {
failed(t);
}
}
private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set<NodeAddress> excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
sendInitTradeRequest(arbitratorNodeAddress, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
resultHandler.handleResult();
}
// if unavailable, try alternative arbitrator
@Override
public void onFault(String errorMessage) {
log.warn("Arbitrator {} unavailable: {}", arbitratorNodeAddress, errorMessage);
excludedArbitrators.add(arbitratorNodeAddress);
Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager(), excludedArbitrators);
if (altArbitrator == null) {
errorMessageHandler.handleErrorMessage("Cannot take offer because no arbitrators are available");
return;
}
log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress());
sendInitTradeRequests(altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler);
}
});
}
private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) {
// get registered arbitrator
Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress);
if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator");
// set pub keys
processModel.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
trade.setArbitratorNodeAddress(arbitratorNodeAddress);
trade.setArbitratorPubKeyRing(processModel.getArbitrator().getPubKeyRing());
// create request to arbitrator
InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // taker's InitTradeRequest to maker
InitTradeRequest arbitratorRequest = new InitTradeRequest(

View file

@ -638,8 +638,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
protected void unlatchTrade() {
if (tradeLatch != null) tradeLatch.countDown();
CountDownLatch lastLatch = tradeLatch;
tradeLatch = null;
if (lastLatch != null) lastLatch.countDown();
}
protected void awaitTradeLatch() {

View file

@ -22,7 +22,6 @@ import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
import bisq.core.trade.Trade;
@ -40,7 +39,7 @@ import monero.daemon.MoneroDaemon;
@Slf4j
public class ArbitratorProcessesDepositRequest extends TradeTask {
@SuppressWarnings({"unused"})
public ArbitratorProcessesDepositRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
@ -50,7 +49,7 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
protected void run() {
try {
runInterceptHook();
// get contract and signature
String contractAsJson = trade.getContractAsJson();
DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response
@ -68,10 +67,10 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
// verify signature
if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid");
// set peer's signature
peer.setContractSignature(signature);
// collect expected values of deposit tx
Offer offer = trade.getOffer();
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress());
@ -83,14 +82,9 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
else throw new RuntimeException("DepositRequest is not from maker or taker");
// flush reserve tx from pool
XmrWalletService xmrWalletService = trade.getXmrWalletService();
MoneroDaemon daemon = xmrWalletService.getDaemon();
daemon.flushTxPool(trader.getReserveTxHash());
// verify deposit tx
xmrWalletService.verifyTradeTx(depositAddress,
trade.getXmrWalletService().verifyTradeTx(depositAddress,
depositAmount,
tradeFee,
trader.getDepositTxHash(),
@ -98,16 +92,17 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
request.getDepositTxKey(),
null,
false);
// set deposit info
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
// relay deposit txs when both available
// TODO (woodser): add small delay so tx has head start against double spend attempts?
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
// relay txs
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
@ -130,14 +125,14 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
} else {
log.info("Arbitrator waiting for deposit request from maker and taker for trade " + trade.getId());
}
// TODO (woodser): request persistence?
complete();
} catch (Throwable t) {
failed(t);
}
}
private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) {
log.info("Sending deposit response to trader={}; offerId={}", nodeAddress, trade.getId());
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() {

View file

@ -20,9 +20,7 @@ package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
import bisq.core.offer.OfferPayload;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.ParsingUtils;

View file

@ -27,7 +27,6 @@ import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradeListener;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener;
import com.google.common.base.Charsets;
import java.util.Date;
@ -42,10 +41,10 @@ import monero.wallet.MoneroWallet;
* Arbitrator sends InitMultisigRequests after the maker acks.
*/
@Slf4j
public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
public class ArbitratorSendsInitTradeOrMultisigRequests extends TradeTask {
@SuppressWarnings({"unused"})
public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) {
public ArbitratorSendsInitTradeOrMultisigRequests(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -53,82 +52,67 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
protected void run() {
try {
runInterceptHook();
// skip if request not from taker
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
if (!request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) {
complete();
return;
}
// arbitrator signs offer id as nonce to avoid challenge protocol
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), processModel.getOfferId().getBytes(Charsets.UTF_8));
// save pub keys
processModel.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO (woodser): why duplicating field in process model
trade.setArbitratorPubKeyRing(processModel.getPubKeyRing());
trade.setMakerPubKeyRing(trade.getOffer().getPubKeyRing());
trade.setTakerPubKeyRing(request.getPubKeyRing());
// create request to initialize trade with maker
InitTradeRequest makerRequest = new InitTradeRequest(
processModel.getOfferId(),
request.getSenderNodeAddress(),
request.getPubKeyRing(),
trade.getAmount().value,
trade.getPrice().getValue(),
trade.getTakerFee().getValue(),
request.getAccountId(),
request.getPaymentAccountId(),
request.getPaymentMethodId(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
new Date().getTime(),
trade.getMakerNodeAddress(),
trade.getTakerNodeAddress(),
trade.getArbitratorNodeAddress(),
null,
null, // do not include taker's reserve tx
null,
null,
null);
// send init multisig requests on ack // TODO (woodser): only send InitMultisigRequests if arbitrator has maker reserve tx, else wait for that
TradeListener listener = new TradeListener() {
@Override
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
if (sender.equals(trade.getMakerNodeAddress()) &&
ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName()) &&
ackMessage.getSourceUid().equals(makerRequest.getUid())) {
trade.removeListener(this);
if (ackMessage.isSuccess()) sendInitMultisigRequests();
}
}
};
trade.addListener(listener);
// handle request from taker
if (request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) {
// send request to maker
log.info("Send {} with offerId {} and uid {} to maker {} with pub key ring", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid(), trade.getMakerNodeAddress(), trade.getMakerPubKeyRing());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getMakerNodeAddress(), // TODO (woodser): maker's address might be different from original owner address if they disconnect and reconnect, need to validate and update address when requests received
trade.getMakerPubKeyRing(),
makerRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid());
complete();
// create request to initialize trade with maker
InitTradeRequest makerRequest = new InitTradeRequest(
processModel.getOfferId(),
request.getSenderNodeAddress(),
request.getPubKeyRing(),
trade.getAmount().value,
trade.getPrice().getValue(),
trade.getTakerFee().getValue(),
request.getAccountId(),
request.getPaymentAccountId(),
request.getPaymentMethodId(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
new Date().getTime(),
trade.getMakerNodeAddress(),
trade.getTakerNodeAddress(),
trade.getArbitratorNodeAddress(),
null,
null, // do not include taker's reserve tx
null,
null,
null);
// send request to maker
log.info("Send {} with offerId {} and uid {} to maker {} with pub key ring", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid(), trade.getMakerNodeAddress(), trade.getMakerPubKeyRing());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getMakerNodeAddress(), // TODO (woodser): maker's address might be different from original owner address if they disconnect and reconnect, need to validate and update address when requests received
trade.getMakerPubKeyRing(),
makerRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid());
complete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", makerRequest.getClass().getSimpleName(), makerRequest.getUid(), trade.getArbitratorNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + makerRequest + "\nerrorMessage=" + errorMessage);
failed();
}
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", makerRequest.getClass().getSimpleName(), makerRequest.getUid(), trade.getArbitratorNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + makerRequest + "\nerrorMessage=" + errorMessage);
trade.removeListener(listener);
failed();
}
}
);
);
}
// handle request from maker
else if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) {
sendInitMultisigRequests();
complete(); // TODO: wait for InitMultisigRequest arrivals?
} else {
throw new RuntimeException("Request is not from maker or taker");
}
} catch (Throwable t) {
failed(t);
}
@ -138,14 +122,12 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
// ensure arbitrator has maker's reserve tx
if (processModel.getMaker().getReserveTxHash() == null) {
log.warn("Arbitrator {} does not have maker's reserve tx after initializing trade", P2PService.getMyNodeAddress());
failed();
return;
throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade");
}
// create wallet for multisig
MoneroWallet multisigWallet = processModel.getXmrWalletService().createMultisigWallet(trade.getId());
// prepare multisig
String preparedHex = multisigWallet.prepareMultisig();
trade.getSelf().setPreparedMultisigHex(preparedHex);
@ -176,8 +158,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getUid(), trade.getMakerNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + initMultisigRequest + "\nerrorMessage=" + errorMessage);
failed();
}
}
);
@ -196,8 +176,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getUid(), trade.getTakerNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + initMultisigRequest + "\nerrorMessage=" + errorMessage);
failed();
}
}
);

View file

@ -37,9 +37,9 @@ import static bisq.core.util.Validator.checkTradeId;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class MakerSendsInitTradeRequestIfUnreserved extends TradeTask {
public class MakerSendsInitTradeRequest extends TradeTask {
@SuppressWarnings({"unused"})
public MakerSendsInitTradeRequestIfUnreserved(TaskRunner taskHandler, Trade trade) {
public MakerSendsInitTradeRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -48,28 +48,22 @@ public class MakerSendsInitTradeRequestIfUnreserved extends TradeTask {
try {
runInterceptHook();
// skip if arbitrator is signer and therefore already has reserve tx
Offer offer = processModel.getOffer();
if (offer.getOfferPayload().getArbitratorSigner().equals(trade.getArbitratorNodeAddress())) {
complete();
return;
}
// verify trade
InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to maker
checkNotNull(makerRequest);
checkTradeId(processModel.getOfferId(), makerRequest);
// maker signs offer id as nonce to avoid challenge protocol // TODO (woodser): is this necessary?
Offer offer = processModel.getOffer();
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));
// create request to arbitrator
InitTradeRequest arbitratorRequest = new InitTradeRequest(
offer.getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
offer.getAmount().value,
offer.getPrice().getValue(),
trade.getPrice().getValue(),
offer.getMakerFee().value,
trade.getProcessModel().getAccountId(),
offer.getMakerPaymentAccountId(),
@ -86,7 +80,7 @@ public class MakerSendsInitTradeRequestIfUnreserved extends TradeTask {
trade.getSelf().getReserveTxKey(),
model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(),
null);
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {} with pub key ring {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getTradeId(), arbitratorRequest.getUid(), trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing());
processModel.getP2PService().sendEncryptedDirectMessage(

View file

@ -60,8 +60,8 @@ public class ProcessDepositResponse extends TradeTask {
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
complete();
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
complete();
}
@Override
public void onFault(String errorMessage) {

View file

@ -25,7 +25,6 @@ import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.user.User;
import bisq.common.taskrunner.TaskRunner;
import org.bitcoinj.core.Coin;
@ -62,6 +61,9 @@ public class ProcessInitTradeRequest extends TradeTask {
// handle request as arbitrator
TradingPeer multisigParticipant;
if (trade instanceof ArbitratorTrade) {
trade.setMakerPubKeyRing((trade.getOffer().getPubKeyRing()));
trade.setArbitratorPubKeyRing(processModel.getPubKeyRing());
processModel.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO (woodser): why duplicating field in process model
// handle request from taker
if (request.getSenderNodeAddress().equals(request.getTakerNodeAddress())) {
@ -70,6 +72,17 @@ public class ProcessInitTradeRequest extends TradeTask {
if (trade.getTakerPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest");
trade.setTakerPubKeyRing(request.getPubKeyRing());
if (!TradeUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
} catch (Throwable e2) {
failed(e2);
}
}
// handle request from maker
@ -79,6 +92,7 @@ public class ProcessInitTradeRequest extends TradeTask {
if (trade.getMakerPubKeyRing() == null) trade.setMakerPubKeyRing(request.getPubKeyRing());
else if (!trade.getMakerPubKeyRing().equals(request.getPubKeyRing())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); // TODO (woodser): proper handling
trade.setMakerPubKeyRing(request.getPubKeyRing());
if (trade.getPrice().getValue() != request.getTradePrice()) throw new RuntimeException("Maker and taker price do not agree");
} else {
throw new RuntimeException("Sender is not trade's maker or taker");
}
@ -89,6 +103,17 @@ public class ProcessInitTradeRequest extends TradeTask {
multisigParticipant = processModel.getTaker();
trade.setTakerNodeAddress(request.getSenderNodeAddress()); // arbitrator sends maker InitTradeRequest with taker's node address and pub key ring
trade.setTakerPubKeyRing(request.getPubKeyRing());
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
} catch (Throwable e2) {
failed(e2);
}
}
// handle invalid trade type
@ -106,17 +131,6 @@ public class ProcessInitTradeRequest extends TradeTask {
multisigParticipant.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId());
multisigParticipant.setCurrentDate(request.getCurrentDate());
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
} catch (Throwable e2) {
failed(e2);
}
// check trade amount
checkArgument(request.getTradeAmount() > 0);
trade.setAmount(Coin.valueOf(request.getTradeAmount()));

View file

@ -18,7 +18,10 @@
package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.app.Version;
import bisq.common.crypto.Hash;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
@ -77,6 +80,7 @@ public class ProcessSignContractRequest extends TradeTask {
// save contract and signature
trade.setContract(contract);
trade.setContractAsJson(contractAsJson);
trade.setContractHash(Hash.getSha256Hash(checkNotNull(contractAsJson)));
trade.getSelf().setContractSignature(signature);
// create response with contract signature
@ -87,6 +91,7 @@ public class ProcessSignContractRequest extends TradeTask {
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
contractAsJson,
signature);
// get response recipients. only arbitrator sends response to both peers

View file

@ -44,10 +44,13 @@ public class ProcessSignContractResponse extends TradeTask {
try {
runInterceptHook();
// get contract and signature
// compare contracts
String contractAsJson = trade.getContractAsJson();
SignContractResponse response = (SignContractResponse) processModel.getTradeMessage(); // TODO (woodser): verify response
String signature = response.getContractSignature();
SignContractResponse response = (SignContractResponse) processModel.getTradeMessage();
if (!contractAsJson.equals(response.getContractAsJson())) {
trade.getContract().printDiff(response.getContractAsJson());
failed("Contracts are not matching");
}
// get peer info
// TODO (woodser): make these utilities / refactor model
@ -60,6 +63,7 @@ public class ProcessSignContractResponse extends TradeTask {
// verify signature
// TODO (woodser): transfer contract for convenient comparison?
String signature = response.getContractSignature();
if (!Sig.verify(peerPubKeyRing.getSignaturePubKey(), contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid");
// set peer's signature

View file

@ -44,7 +44,7 @@ public class ArbitratorManagerTest {
User user = mock(User.class);
ArbitratorService arbitratorService = mock(ArbitratorService.class);
ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false);
ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null);
ArrayList<String> languagesOne = new ArrayList<String>() {{
add("en");
@ -80,7 +80,7 @@ public class ArbitratorManagerTest {
User user = mock(User.class);
ArbitratorService arbitratorService = mock(ArbitratorService.class);
ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false);
ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null);
ArrayList<String> languagesOne = new ArrayList<String>() {{
add("en");

View file

@ -1,9 +1,10 @@
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.proto.grpc.RegisterDisputeAgentReply;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.UnregisterDisputeAgentReply;
import bisq.proto.grpc.UnregisterDisputeAgentRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
@ -18,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.DisputeAgentsGrpc.DisputeAgentsImplBase;
import static bisq.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod;
import static bisq.proto.grpc.DisputeAgentsGrpc.getUnregisterDisputeAgentMethod;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -41,10 +43,39 @@ class GrpcDisputeAgentsService extends DisputeAgentsImplBase {
public void registerDisputeAgent(RegisterDisputeAgentRequest req,
StreamObserver<RegisterDisputeAgentReply> responseObserver) {
try {
coreApi.registerDisputeAgent(req.getDisputeAgentType(), req.getRegistrationKey());
var reply = RegisterDisputeAgentReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getRegisterDisputeAgentMethod().getFullMethodName(), responseObserver, exceptionHandler, log);
coreApi.registerDisputeAgent(
req.getDisputeAgentType(),
req.getRegistrationKey(),
() -> {
var reply = RegisterDisputeAgentReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
},
errorMessage -> {
if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void unregisterDisputeAgent(UnregisterDisputeAgentRequest req,
StreamObserver<UnregisterDisputeAgentReply> responseObserver) {
try {
GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getUnregisterDisputeAgentMethod().getFullMethodName(), responseObserver, exceptionHandler, log);
coreApi.unregisterDisputeAgent(
req.getDisputeAgentType(),
() -> {
var reply = UnregisterDisputeAgentReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
},
errorMessage -> {
if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}

View file

@ -415,7 +415,7 @@ class GrpcWalletsService extends WalletsImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(50, SECONDS));
put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));

View file

@ -192,7 +192,7 @@ public class NotificationCenter {
message = Res.get("notification.trade.accepted", role);
}
if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSIT_UNLOCKED.ordinal())
if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal())
message = Res.get("notification.trade.confirmed");
else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal())
message = Res.get("notification.trade.paymentStarted");

View file

@ -162,7 +162,7 @@ public class BuyerStep2View extends TradeStepView {
switch (state) {
case BUYER_CONFIRMED_IN_UI_PAYMENT_SENT:
busyAnimation.play();
statusLabel.setText("Confirming payment sent. This can take up to a few minutes depending on connection speed. Please wait...");
statusLabel.setText("Confirming payment sent. This can take up to a few minutes. Please wait...");
break;
case BUYER_SENT_PAYMENT_SENT_MSG:
busyAnimation.play();

View file

@ -121,7 +121,7 @@ public class SellerStep3View extends TradeStepView {
switch (state) {
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT:
busyAnimation.play();
statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes depending on connection speed. Please wait..."));
statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes. Please wait..."));
break;
case SELLER_PUBLISHED_PAYOUT_TX:
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG:

View file

@ -845,14 +845,6 @@
<sha256 value="4728eddd64e6ae3e1f205a775c6a327b24bd990b86d528584a17450a8b5f00d6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.github.monero-ecosystem" name="monero-java" version="0.7.0">
<artifact name="monero-java-0.7.0.jar">
<sha256 value="dbd9249df77c4d313b385bc5352a9e61ad930fae79acb5178813be19c2bb4771" origin="Generated by Gradle"/>
</artifact>
<artifact name="monero-java-0.7.0.pom">
<sha256 value="ead76a2facdfaa55fcc2bc4aa706e3c6eebd5df4b9dcb153a9ff01f8f0324596" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.github.monero-ecosystem" name="monero-java" version="0.7.2">
<artifact name="monero-java-0.7.2.jar">
<sha256 value="166903729f2f554f2c7a9c908bc79e5940a96510852d9f9673494d346cec3c82" origin="Generated by Gradle"/>

View file

@ -220,6 +220,8 @@ message SendDisputeChatMessageReply {
service DisputeAgents {
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
}
rpc UnregisterDisputeAgent (UnregisterDisputeAgentRequest) returns (UnregisterDisputeAgentReply) {
}
}
message RegisterDisputeAgentRequest {
@ -230,6 +232,13 @@ message RegisterDisputeAgentRequest {
message RegisterDisputeAgentReply {
}
message UnregisterDisputeAgentRequest {
string dispute_agent_type = 1;
}
message UnregisterDisputeAgentReply {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Notifications
///////////////////////////////////////////////////////////////////////////////////////////
@ -530,6 +539,7 @@ message OfferInfo {
string pub_key_ring = 26;
string version_nr = 27;
int32 protocol_version = 28;
string arbitrator_signer = 29;
}
message AvailabilityResultWithDescription {
@ -822,19 +832,20 @@ message TradeInfo {
string payout_tx_id = 11;
uint64 amount_as_long = 12;
string price = 13;
string trading_peer_node_address = 14;
string state = 15;
string phase = 16;
string period_state = 17;
bool is_deposit_published = 18;
bool is_deposit_unlocked = 19;
bool is_payment_sent = 20;
bool is_payment_received = 21;
bool is_payout_published = 22;
bool is_completed = 23;
string contract_as_json = 24;
ContractInfo contract = 25;
string trade_volume = 26;
string arbitrator_node_address = 14;
string trading_peer_node_address = 15;
string state = 16;
string phase = 17;
string period_state = 18;
bool is_deposit_published = 19;
bool is_deposit_unlocked = 20;
bool is_payment_sent = 21;
bool is_payment_received = 22;
bool is_payout_published = 23;
bool is_completed = 24;
string contract_as_json = 25;
ContractInfo contract = 26;
string trade_volume = 27;
string maker_deposit_tx_id = 100;
string taker_deposit_tx_id = 101;

View file

@ -184,7 +184,6 @@ message OfferAvailabilityResponse {
repeated int32 supported_capabilities = 3;
string uid = 4;
string maker_signature = 5;
NodeAddress backup_arbitrator = 6;
}
message RefreshOfferMessage {
@ -328,7 +327,8 @@ message SignContractResponse {
PubKeyRing pub_key_ring = 3;
string uid = 4;
int64 current_date = 5;
string contract_signature = 6;
string contract_as_json = 6;
string contract_signature = 7;
}
message DepositRequest {
@ -1598,14 +1598,13 @@ message OpenOffer {
Offer offer = 1;
State state = 2;
NodeAddress backup_arbitrator = 3;
int64 trigger_price = 4;
bool auto_split = 5;
repeated string scheduled_tx_hashes = 6;
string scheduled_amount = 7; // BigInteger
string reserve_tx_hash = 8;
string reserve_tx_hex = 9;
string reserve_tx_key = 10;
int64 trigger_price = 3;
bool auto_split = 4;
repeated string scheduled_tx_hashes = 5;
string scheduled_amount = 6; // BigInteger
string reserve_tx_hash = 7;
string reserve_tx_hex = 8;
string reserve_tx_key = 9;
}
message Tradable {
@ -1665,7 +1664,7 @@ message Trade {
INIT = 1;
DEPOSIT_REQUESTED = 2;
DEPOSITS_PUBLISHED = 3;
DEPOSITS_CONFIRMED = 4;
DEPOSITS_UNLOCKED = 4;
PAYMENT_SENT = 5;
PAYMENT_RECEIVED = 6;
PAYOUT_PUBLISHED = 7;
@ -1778,12 +1777,11 @@ message ProcessModel {
int64 seller_payout_amount_from_mediation = 20;
string maker_signature = 1001;
NodeAddress backup_arbitrator = 1002;
TradingPeer maker = 1003;
TradingPeer taker = 1004;
TradingPeer arbitrator = 1005;
NodeAddress temp_trading_peer_node_address = 1006;
string multisig_address = 1007;
TradingPeer maker = 1002;
TradingPeer taker = 1003;
TradingPeer arbitrator = 1004;
NodeAddress temp_trading_peer_node_address = 1005;
string multisig_address = 1006;
}
message TradingPeer {