use backup arbitrator if signing arbitrator not available

This commit is contained in:
woodser 2021-11-15 17:20:28 -05:00
parent 7d21bdf9f3
commit 4dafd57026
32 changed files with 370 additions and 388 deletions

View file

@ -54,6 +54,17 @@ arbitrator-desktop:
--apiPassword=apitest \
--apiPort=9998
arbitrator-desktop2:
# Arbitrator and mediator need to be registerd in the UI after launching it.
./haveno-desktop \
--baseCurrencyNetwork=XMR_STAGENET \
--useLocalhostForP2P=true \
--useDevPrivilegeKeys=true \
--nodePort=7777 \
--appName=haveno-XMR_STAGENET_arbitrator2 \
--apiPassword=apitest \
--apiPort=10001
alice-desktop:
./haveno-desktop \
--baseCurrencyNetwork=XMR_STAGENET \

View file

@ -216,8 +216,8 @@ public class OfferFilter {
public boolean hasValidSignature(Offer offer) {
// get arbitrator
Mediator arbitrator = user.getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorNodeAddress()); // TODO (woodser): does this return null if arbitrator goes offline?
if (arbitrator == null) return false; // TODO (woodser): if arbitrator is null, get arbirator's pub key ring from store, otherwise cannot validate and offer is not seen by takers when arbitrator goes offline
Mediator arbitrator = user.getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorSigner());
if (arbitrator == null) return false; // invalid arbitrator
// validate arbitrator signature
return TradeUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator);

View file

@ -169,7 +169,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// address and signature of signing arbitrator
@Setter
private NodeAddress arbitratorNodeAddress;
private NodeAddress arbitratorSigner;
@Setter
@Nullable
private String arbitratorSignature;
@ -255,7 +255,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.hashOfChallenge = hashOfChallenge;
this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap);
this.protocolVersion = protocolVersion;
this.arbitratorNodeAddress = arbitratorSigner;
this.arbitratorSigner = arbitratorSigner;
this.arbitratorSignature = arbitratorSignature;
this.reserveTxKeyImages = reserveTxKeyImages;
}
@ -295,7 +295,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
.setUpperClosePrice(upperClosePrice)
.setIsPrivateOffer(isPrivateOffer)
.setProtocolVersion(protocolVersion)
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
.setArbitratorSigner(arbitratorSigner.toProtoMessage());
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
"OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network."));
@ -357,7 +357,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
hashOfChallenge,
extraDataMapMap,
proto.getProtocolVersion(),
NodeAddress.fromProto(proto.getArbitratorNodeAddress()),
NodeAddress.fromProto(proto.getArbitratorSigner()),
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()),
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
}
@ -424,7 +424,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\n hashOfChallenge='" + hashOfChallenge + '\'' +
",\n extraDataMap=" + extraDataMap +
",\n protocolVersion=" + protocolVersion +
",\n arbitratorSigner=" + arbitratorNodeAddress +
",\n arbitratorSigner=" + arbitratorSigner +
",\n arbitratorSignature=" + arbitratorSignature +
"\n}";
}

View file

@ -24,9 +24,7 @@ import bisq.network.p2p.NodeAddress;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.proto.ProtoUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import lombok.EqualsAndHashCode;
@ -58,10 +56,17 @@ public final class OpenOffer implements Tradable {
@Getter
@Setter
@Nullable
private NodeAddress arbitratorNodeAddress;
private NodeAddress backupArbitrator;
@Setter
@Getter
private List<String> frozenKeyImages = new ArrayList<>();
private String reserveTxHash;
@Setter
@Getter
private String reserveTxHex;
@Setter
@Getter
private String reserveTxKey;
// Added in v1.5.3.
// If market price reaches that trigger price the offer gets deactivated
@ -81,11 +86,17 @@ public final class OpenOffer implements Tradable {
state = State.AVAILABLE;
}
public OpenOffer(Offer offer, long triggerPrice, List<String> frozenKeyImages) {
public OpenOffer(Offer offer,
long triggerPrice,
String reserveTxHash,
String reserveTxHex,
String reserveTxKey) {
this.offer = offer;
this.triggerPrice = triggerPrice;
state = State.AVAILABLE;
this.frozenKeyImages = frozenKeyImages;
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -94,12 +105,18 @@ public final class OpenOffer implements Tradable {
private OpenOffer(Offer offer,
State state,
@Nullable NodeAddress arbitratorNodeAddress,
long triggerPrice) {
@Nullable NodeAddress backupArbitrator,
long triggerPrice,
String reserveTxHash,
String reserveTxHex,
String reserveTxKey) {
this.offer = offer;
this.state = state;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.backupArbitrator = backupArbitrator;
this.triggerPrice = triggerPrice;
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
if (this.state == State.RESERVED)
setState(State.AVAILABLE);
@ -111,9 +128,11 @@ public final class OpenOffer implements Tradable {
.setOffer(offer.toProtoMessage())
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
.addAllFrozenKeyImages(frozenKeyImages);
.setReserveTxHash(reserveTxHash)
.setReserveTxHex(reserveTxHex)
.setReserveTxKey(reserveTxKey);
Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage()));
Optional.ofNullable(backupArbitrator).ifPresent(nodeAddress -> builder.setBackupArbitrator(nodeAddress.toProtoMessage()));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@ -121,9 +140,11 @@ 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.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
proto.getTriggerPrice());
openOffer.setFrozenKeyImages(proto.getFrozenKeyImagesList());
proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null,
proto.getTriggerPrice(),
proto.getReserveTxHash(),
proto.getReserveTxHex(),
proto.getReserveTxKey());
return openOffer;
}
@ -187,7 +208,7 @@ public final class OpenOffer implements Tradable {
return "OpenOffer{" +
",\n offer=" + offer +
",\n state=" + state +
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n arbitratorNodeAddress=" + backupArbitrator +
",\n triggerPrice=" + triggerPrice +
"\n}";
}

View file

@ -91,7 +91,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import monero.daemon.model.MoneroOutput;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
@ -416,10 +415,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
model,
transaction -> {
// save frozen key images with open offer
List<String> frozenKeyImages = new ArrayList<String>();
for (MoneroOutput output : model.getReserveTx().getInputs()) frozenKeyImages.add(output.getKeyImage().getHex());
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, frozenKeyImages);
// save reserve tx with open offer
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, model.getReserveTx().getHash(), model.getReserveTx().getFullHex(), model.getReserveTx().getKey());
openOffers.add(openOffer);
requestPersistence();
resultHandler.handleResult(transaction);
@ -577,7 +574,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
closedTradableManager.add(openOffer);
log.info("onRemoved offerId={}", offer.getId());
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
for (String frozenKeyImage : openOffer.getFrozenKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
requestPersistence();
resultHandler.handleResult();
}
@ -642,7 +639,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
// verify arbitrator is signer of offer payload
if (!request.getOfferPayload().getArbitratorNodeAddress().equals(thisAddress)) {
if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) {
errorMessage = "Cannot sign offer because offer payload is for a different arbitrator";
log.info(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
@ -784,7 +781,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
AvailabilityResult availabilityResult;
String makerSignature = null;
NodeAddress arbitratorNodeAddress = null;
NodeAddress backupArbitratorNodeAddress = null;
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
if (!apiUserDeniedByOffer(request)) {
@ -793,10 +790,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Offer offer = openOffer.getOffer();
if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) {
// use signing arbitrator if available, otherwise use least used arbitrator
boolean isSignerOnline = true;
arbitratorNodeAddress = isSignerOnline ? offer.getOfferPayload().getArbitratorNodeAddress() : DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, mediatorManager).getNodeAddress();
openOffer.setArbitratorNodeAddress(arbitratorNodeAddress);
// 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?
String tradeRequestAsJson = Utilities.objectToJson(request.getTradeRequest());
@ -848,7 +845,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId,
availabilityResult,
makerSignature,
arbitratorNodeAddress);
backupArbitratorNodeAddress);
log.info("Send {} with offerId {} and uid {} to peer {}",
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(),
offerAvailabilityResponse.getUid(), peer);
@ -934,6 +931,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// Update persisted offer if a new capability is required after a software update
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): arbitrator signature will be invalid if offer updated (exclude updateable fields from signature? re-sign?)
private void maybeUpdatePersistedOffers() {
// We need to clone to avoid ConcurrentModificationException
ArrayList<OpenOffer> openOffersClone = new ArrayList<>(openOffers.getList());
@ -1019,7 +1018,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getHashOfChallenge(),
updatedExtraDataMap,
protocolVersion,
originalOfferPayload.getArbitratorNodeAddress(),
originalOfferPayload.getArbitratorSigner(),
originalOfferPayload.getArbitratorSignature(),
originalOfferPayload.getReserveTxKeyImages());

View file

@ -21,7 +21,7 @@ import bisq.core.support.dispute.agent.DisputeAgent;
import bisq.core.support.dispute.agent.DisputeAgentManager;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.network.p2p.NodeAddress;
import bisq.common.util.Tuple2;
import com.google.common.annotations.VisibleForTesting;
@ -47,11 +47,21 @@ public class DisputeAgentSelection {
public static <T extends DisputeAgent> T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager) {
return getLeastUsedDisputeAgent(tradeStatisticsManager,
disputeAgentManager);
disputeAgentManager,
null);
}
public static <T extends DisputeAgent> T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager,
NodeAddress excludedArbitrator) {
return getLeastUsedDisputeAgent(tradeStatisticsManager,
disputeAgentManager,
excludedArbitrator);
}
private static <T extends DisputeAgent> T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager) {
DisputeAgentManager<T> disputeAgentManager,
NodeAddress excludedDisputeAgent) {
// We take last 100 entries from trade statistics
List<TradeStatistics3> list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
@ -71,6 +81,9 @@ public class DisputeAgentSelection {
.map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress())
.collect(Collectors.toSet());
if (excludedDisputeAgent != null) disputeAgents.remove(excludedDisputeAgent.getFullAddress());
if (disputeAgents.isEmpty()) return null;
String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents);
Optional<T> optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream()

View file

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

View file

@ -53,7 +53,7 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
return;
}
// check maker signature for trade request
// verify maker signature for trade request
if (!TradeUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
offer.setState(Offer.State.NOT_AVAILABLE);
failed("Take offer attempt failed because maker signature is invalid");
@ -61,11 +61,9 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
}
offer.setState(Offer.State.AVAILABLE);
model.setMakerSignature(offerAvailabilityResponse.getMakerSignature());
model.setArbitratorNodeAddress(offerAvailabilityResponse.getArbitratorNodeAddress());
model.setBackupArbitrator(offerAvailabilityResponse.getBackupArbitrator());
checkNotNull(model.getMakerSignature());
checkNotNull(model.getArbitratorNodeAddress());
complete();
} catch (Throwable t) {

View file

@ -37,6 +37,7 @@ import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
// TODO (woodser): rename to TakerSendOfferAvailabilityRequest and group with other taker tasks
@Slf4j
public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
public SendOfferAvailabilityRequest(TaskRunner<OfferAvailabilityModel> taskHandler, OfferAvailabilityModel model) {
@ -78,7 +79,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
new Date().getTime(),
offer.getMakerNodeAddress(),
P2PService.getMyNodeAddress(),
null, // maker provides node address of arbitrator on response
null, // maker provides node address of backup arbitrator on response
null, // reserve tx not sent from taker to maker
null,
null,

View file

@ -46,19 +46,19 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
@Nullable
private final String makerSignature;
private final NodeAddress arbitratorNodeAddress;
private final NodeAddress backupArbitrator;
public OfferAvailabilityResponse(String offerId,
AvailabilityResult availabilityResult,
String makerSignature,
NodeAddress arbitratorNodeAddress) {
NodeAddress backupArbitrator) {
this(offerId,
availabilityResult,
Capabilities.app,
Version.getP2PMessageVersion(),
UUID.randomUUID().toString(),
makerSignature,
arbitratorNodeAddress);
backupArbitrator);
}
@ -77,19 +77,19 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
this.availabilityResult = availabilityResult;
this.supportedCapabilities = supportedCapabilities;
this.makerSignature = makerSignature;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.backupArbitrator = arbitratorNodeAddress;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder()
.setOfferId(offerId)
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()))
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()));
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)
@ -103,6 +103,6 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
messageVersion,
proto.getUid().isEmpty() ? null : proto.getUid(),
proto.getMakerSignature().isEmpty() ? null : proto.getMakerSignature(),
NodeAddress.fromProto(proto.getArbitratorNodeAddress()));
proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
}
}

View file

@ -56,6 +56,7 @@ public class PlaceOfferProtocol {
// Called from UI
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): this returns before offer is placed
public void placeOffer() {
log.debug("placeOffer() " + model.getOffer().getId());
TaskRunner<PlaceOfferModel> taskRunner = new TaskRunner<>(model,
@ -82,7 +83,7 @@ public class PlaceOfferProtocol {
log.debug("handleSignOfferResponse() " + model.getOffer().getId());
model.setSignOfferResponse(response);
if (!model.getOffer().getOfferPayload().getArbitratorNodeAddress().equals(sender)) {
if (!model.getOffer().getOfferPayload().getArbitratorSigner().equals(sender)) {
log.warn("Ignoring sign offer response from different sender");
return;
}

View file

@ -39,7 +39,7 @@ public class MakerProcessesSignOfferResponse extends Task<PlaceOfferModel> {
runInterceptHook();
// validate arbitrator signature
Mediator arbitrator = checkNotNull(model.getUser().getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorNodeAddress()), "user.getAcceptedMediatorByAddress(arbitratorNodeAddress) must not be null");
Mediator arbitrator = checkNotNull(model.getUser().getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedMediatorByAddress(arbitratorSigner) must not be null");
if (!TradeUtils.isArbitratorSignatureValid(model.getSignOfferResponse().getSignedOfferPayload(), arbitrator)) {
throw new RuntimeException("Offer payload has invalid arbitrator signature");
}

View file

@ -64,7 +64,7 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
// TODO (woodser): persist
model.setReserveTx(reserveTx);
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
offer.setState(Offer.State.OFFER_FEE_RESERVED);
complete();
} catch (Throwable t) {

View file

@ -68,7 +68,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
returnAddress);
// get signing arbitrator
Mediator arbitrator = checkNotNull(model.getUser().getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorNodeAddress()), "user.getAcceptedMediatorByAddress(mediatorNodeAddress) must not be null");
Mediator arbitrator = checkNotNull(model.getUser().getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedMediatorByAddress(mediatorNodeAddress) must not be null");
// send request
model.getP2PService().sendEncryptedDirectMessage(arbitrator.getNodeAddress(), arbitrator.getPubKeyRing(), request, new SendDirectMessageListener() {

View file

@ -433,11 +433,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
request.getTakerNodeAddress(),
request.getArbitratorNodeAddress());
// set reserve tx hash
// set reserve tx hash if available
Optional<SignedOffer> signedOfferOptional = openOfferManager.getSignedOfferById(request.getTradeId());
if (!signedOfferOptional.isPresent()) return;
if (signedOfferOptional.isPresent()) {
SignedOffer signedOffer = signedOfferOptional.get();
trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash());
}
initTradeAndProtocol(trade, getTradeProtocol(trade));
tradableList.add(trade);
}
@ -464,19 +466,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
Offer offer = openOffer.getOffer();
openOfferManager.reserveOpenOffer(openOffer); // TODO (woodser): reserve offer if arbitrator?
// verify request is from signing arbitrator when they're online, else from selected arbitrator
if (!sender.equals(offer.getOfferPayload().getArbitratorNodeAddress())) {
boolean isSignerOnline = true; // TODO (woodser): determine if signer is online and test
if (isSignerOnline) {
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from signing arbitrator when online", sender, request.getTradeId());
return;
} else if (!sender.equals(openOffer.getArbitratorNodeAddress())) {
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from selected arbitrator when signing arbitrator is offline", sender, request.getTradeId());
// 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());
return;
}
}
openOfferManager.reserveOpenOffer(openOffer); // TODO (woodser): reserve offer if arbitrator?
Trade trade;
if (offer.isBuyOffer())
@ -504,12 +501,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
//System.out.println("TradeManager trade.setTradingPeerNodeAddress(): " + sender);
//trade.setTradingPeerNodeAddress(sender);
// TODO (woodser): what if maker's address changes while offer open, or taker's address changes after multisig deposit available? need to verify and update
// TODO (woodser): what if maker's address changes while offer open, or taker's address changes after multisig deposit available? need to verify and update. see OpenOfferManager.maybeUpdatePersistedOffers()
trade.setArbitratorPubKeyRing(user.getAcceptedMediatorByAddress(sender).getPubKeyRing());
trade.setMakerPubKeyRing(trade.getOffer().getPubKeyRing());
initTradeAndProtocol(trade, getTradeProtocol(trade));
trade.getProcessModel().setReserveTxHash(offer.getOfferFeePaymentTxId()); // TODO (woodser): initialize in initTradeAndProtocol ?
trade.getProcessModel().setFrozenKeyImages(openOffer.getFrozenKeyImages());
trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol?
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey());
trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages());
tradableList.add(trade);
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
@ -702,7 +701,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
model.getPeerNodeAddress(),
P2PService.getMyNodeAddress(),
offer.getOfferPayload().getArbitratorNodeAddress());
offer.getOfferPayload().getArbitratorSigner());
} else {
trade = new BuyerAsTakerTrade(offer,
amount,
@ -713,12 +712,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
model.getPeerNodeAddress(),
P2PService.getMyNodeAddress(),
offer.getOfferPayload().getArbitratorNodeAddress());
offer.getOfferPayload().getArbitratorSigner());
}
trade.getProcessModel().setTradeMessage(model.getTradeRequest());
trade.getProcessModel().setMakerSignature(model.getMakerSignature());
trade.getProcessModel().setArbitratorNodeAddress(model.getArbitratorNodeAddress());
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

@ -7,11 +7,10 @@ import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeRequestToMakerIfFromTaker;
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeAndMultisigRequests;
import bisq.core.trade.protocol.tasks.ProcessDepositRequest;
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
import bisq.core.trade.protocol.tasks.ArbitratorProcessesReserveTx;
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitMultisigRequestsIfFundsReserved;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
import bisq.core.util.Validator;
@ -42,8 +41,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
ApplyFilter.class,
ProcessInitTradeRequest.class,
ArbitratorProcessesReserveTx.class,
ArbitratorSendsInitTradeRequestToMakerIfFromTaker.class,
ArbitratorSendsInitMultisigRequestsIfFundsReserved.class))
ArbitratorSendsInitTradeAndMultisigRequests.class))
.executeTasks();
}

View file

@ -42,6 +42,7 @@ import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureRe
import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesPreparedDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.maker.MakerRemovesOpenOffer;
import bisq.core.trade.protocol.tasks.maker.MakerSendsInitTradeRequestIfUnreserved;
import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
import bisq.core.util.Validator;
@ -142,7 +143,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, // TODO (woodser): implement this
MakerSendsInitTradeRequestIfUnreserved.class,
MakerRemovesOpenOffer.class).
using(new TradeTaskRunner(trade,
() -> {

View file

@ -76,9 +76,9 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) {
super(trade);
Offer offer = checkNotNull(trade.getOffer());
trade.getTradingPeer().setPubKeyRing(offer.getPubKeyRing());
trade.setMakerPubKeyRing(offer.getPubKeyRing());
// TODO (woodser): setup deposit and payout listeners on construction for startup like before rebase?
}
@ -100,7 +100,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
.setup(tasks(
ApplyFilter.class,
TakerReservesTradeFunds.class,
TakerSendsInitTradeRequestToArbitrator.class)
TakerSendsInitTradeRequestToArbitrator.class) // TODO (woodser): app hangs if this pipeline fails. use .using() like below
.withTimeout(30))
.executeTasks();
}

View file

@ -25,7 +25,6 @@ import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.filter.FilterManager;
import bisq.core.network.MessageState;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload.Direction;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentAccountPayload;
@ -33,9 +32,7 @@ import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.TakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.trade.messages.TradeMessage;
@ -60,7 +57,6 @@ import org.bitcoinj.core.Transaction;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -145,7 +141,7 @@ public class ProcessModel implements Model, PersistablePayload {
// After successful verified we copy that over to the trade.tradingPeerAddress
@Nullable
@Setter
private NodeAddress tempTradingPeerNodeAddress; // TODO (woodser): remove entirely
private NodeAddress tempTradingPeerNodeAddress; // TODO (woodser): remove entirely?
// Added in v.1.1.6
@Nullable
@ -166,17 +162,11 @@ public class ProcessModel implements Model, PersistablePayload {
private String makerSignature;
@Getter
@Setter
private NodeAddress arbitratorNodeAddress;
private NodeAddress backupArbitrator;
@Nullable
@Getter
@Setter
transient private MoneroTxWallet reserveTx;
@Setter
@Getter
private String reserveTxHash;
@Setter
@Getter
private List<String> frozenKeyImages = new ArrayList<>();
@Getter
@Setter
transient private MoneroTxWallet depositTxXmr;
@ -247,20 +237,18 @@ public class ProcessModel implements Model, PersistablePayload {
.setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong)
.setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name())
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
.addAllFrozenKeyImages(frozenKeyImages);
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation);
Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradingPeer) maker.toProtoMessage()));
Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradingPeer) taker.toProtoMessage()));
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradingPeer) arbitrator.toProtoMessage()));
Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId);
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature)));
Optional.ofNullable(rawTransactionInputs).ifPresent(e -> builder.addAllRawTransactionInputs(ProtoUtil.collectionToProto(rawTransactionInputs, protobuf.RawTransactionInput.class)));
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
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(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
Optional.ofNullable(backupArbitrator).ifPresent(e -> builder.setBackupArbitrator(backupArbitrator.toProtoMessage()));
Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex));
Optional.ofNullable(multisigSetupComplete).ifPresent(e -> builder.setMultisigSetupComplete(multisigSetupComplete));
@ -282,8 +270,6 @@ public class ProcessModel implements Model, PersistablePayload {
processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation());
// nullable
processModel.setReserveTxHash(proto.getReserveTxHash());
processModel.setFrozenKeyImages(proto.getFrozenKeyImagesList());
processModel.setTakeOfferFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakeOfferFeeTxId()));
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
List<RawTransactionInput> rawTransactionInputs = proto.getRawTransactionInputsList().isEmpty() ?
@ -295,7 +281,7 @@ 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.setArbitratorNodeAddress(proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null);
processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
processModel.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
processModel.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
processModel.setMultisigSetupComplete(proto.getMultisigSetupComplete());

View file

@ -38,6 +38,7 @@ import bisq.core.trade.protocol.tasks.ProcessSignContractResponse;
import bisq.core.trade.protocol.tasks.SendSignContractRequestAfterMultisig;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.maker.MakerRemovesOpenOffer;
import bisq.core.trade.protocol.tasks.maker.MakerSendsInitTradeRequestIfUnreserved;
import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
@ -144,7 +145,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, // TODO (woodser): implement this
MakerSendsInitTradeRequestIfUnreserved.class,
MakerRemovesOpenOffer.class).
using(new TradeTaskRunner(trade,
() -> {

View file

@ -73,6 +73,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
super(trade);
Offer offer = checkNotNull(trade.getOffer());
trade.getTradingPeer().setPubKeyRing(offer.getPubKeyRing());
trade.setMakerPubKeyRing(offer.getPubKeyRing());
}

View file

@ -27,7 +27,7 @@ import bisq.common.proto.persistable.PersistablePayload;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -102,6 +102,8 @@ public final class TradingPeer implements PersistablePayload {
@Nullable
private String reserveTxKey;
@Nullable
private List<String> reserveTxKeyImages = new ArrayList<>();
@Nullable
private String preparedMultisigHex;
@Nullable
private String madeMultisigHex;
@ -120,7 +122,8 @@ public final class TradingPeer implements PersistablePayload {
@Override
public Message toProtoMessage() {
final protobuf.TradingPeer.Builder builder = protobuf.TradingPeer.newBuilder()
.setChangeOutputValue(changeOutputValue);
.setChangeOutputValue(changeOutputValue)
.addAllReserveTxKeyImages(reserveTxKeyImages);
Optional.ofNullable(accountId).ifPresent(builder::setAccountId);
Optional.ofNullable(paymentAccountId).ifPresent(builder::setPaymentAccountId);
Optional.ofNullable(paymentMethodId).ifPresent(builder::setPaymentMethodId);
@ -183,6 +186,7 @@ public final class TradingPeer implements PersistablePayload {
tradingPeer.setReserveTxHash(ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()));
tradingPeer.setReserveTxHex(ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()));
tradingPeer.setReserveTxKey(ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()));
tradingPeer.setReserveTxKeyImages(proto.getReserveTxKeyImagesList());
tradingPeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
tradingPeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
tradingPeer.setSignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()));

View file

@ -1,131 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.app.Version;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.network.p2p.SendDirectMessageListener;
import com.google.common.base.Charsets;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
/**
* Arbitrator sends InitMultisigRequest to maker and taker if both reserve txs received.
*/
@Slf4j
public class ArbitratorSendsInitMultisigRequestsIfFundsReserved extends TradeTask {
private boolean takerAck;
private boolean makerAck;
@SuppressWarnings({"unused"})
public ArbitratorSendsInitMultisigRequestsIfFundsReserved(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// skip if arbitrator does not have maker reserve tx
if (processModel.getMaker().getReserveTxHash() == null) {
log.info("Arbitrator does not have maker reserve tx for offerId {}, waiting to receive before initializing multisig wallet", processModel.getOffer().getId());
complete();
return;
}
// create wallet for multisig
MoneroWallet multisigWallet = processModel.getXmrWalletService().createMultisigWallet(trade.getId());
// prepare multisig
String preparedHex = multisigWallet.prepareMultisig();
processModel.setPreparedMultisigHex(preparedHex);
// create message to initialize multisig
InitMultisigRequest request = new InitMultisigRequest(
processModel.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
preparedHex,
null);
// send request to maker
log.info("Send {} with offerId {} and uid {} to maker {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), trade.getMakerNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getMakerNodeAddress(),
trade.getMakerPubKeyRing(),
request,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}; uid={}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid());
makerAck = true;
checkComplete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), trade.getMakerNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
}
);
// send request to taker
log.info("Send {} with offerId {} and uid {} to taker {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), trade.getTakerNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getTakerNodeAddress(),
trade.getTakerPubKeyRing(),
request,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer: offerId={}; uid={}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid());
takerAck = true;
checkComplete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), trade.getTakerNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
}
);
} catch (Throwable t) {
failed(t);
}
}
private void checkComplete() {
if (makerAck && takerAck) complete();
}
}

View file

@ -22,24 +22,33 @@ import bisq.common.app.Version;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitMultisigRequest;
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;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
/**
* Arbitrator sends InitTradeRequest to maker after receiving InitTradeRequest
* from taker and verifying taker reserve tx.
*
* Arbitrator sends InitMultisigRequests after the maker acks.
*/
@Slf4j
public class ArbitratorSendsInitTradeRequestToMakerIfFromTaker extends TradeTask {
public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
private boolean takerAck;
private boolean makerAck;
@SuppressWarnings({"unused"})
public ArbitratorSendsInitTradeRequestToMakerIfFromTaker(TaskRunner taskHandler, Trade trade) {
public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -48,12 +57,15 @@ public class ArbitratorSendsInitTradeRequestToMakerIfFromTaker extends TradeTask
try {
runInterceptHook();
// collect fields for request
String offerId = processModel.getOffer().getId();
// 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(), offerId.getBytes(Charsets.UTF_8));
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
@ -63,7 +75,7 @@ public class ArbitratorSendsInitTradeRequestToMakerIfFromTaker extends TradeTask
// create request to initialize trade with maker
InitTradeRequest makerRequest = new InitTradeRequest(
offerId,
processModel.getOfferId(),
request.getSenderNodeAddress(),
request.getPubKeyRing(),
trade.getTradeAmount().value,
@ -91,7 +103,7 @@ public class ArbitratorSendsInitTradeRequestToMakerIfFromTaker extends TradeTask
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
if (sender.equals(trade.getMakerNodeAddress()) && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
trade.removeListener(this);
if (ackMessage.isSuccess()) complete();
if (ackMessage.isSuccess()) sendInitMultisigRequests();
else failed("Received unsuccessful ack for InitTradeRequest from maker"); // TODO (woodser): maker should not do this, penalize them by broadcasting reserve tx?
}
}
@ -122,4 +134,80 @@ public class ArbitratorSendsInitTradeRequestToMakerIfFromTaker extends TradeTask
failed(t);
}
}
private void sendInitMultisigRequests() {
// 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;
}
// create wallet for multisig
MoneroWallet multisigWallet = processModel.getXmrWalletService().createMultisigWallet(trade.getId());
// prepare multisig
String preparedHex = multisigWallet.prepareMultisig();
processModel.setPreparedMultisigHex(preparedHex);
// create message to initialize multisig
InitMultisigRequest initMultisigRequest = new InitMultisigRequest(
processModel.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
preparedHex,
null);
// send request to maker
log.info("Send {} with offerId {} and uid {} to maker {}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid(), trade.getMakerNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getMakerNodeAddress(),
trade.getMakerPubKeyRing(),
initMultisigRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
makerAck = true;
checkComplete();
}
@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();
}
}
);
// send request to taker
log.info("Send {} with offerId {} and uid {} to taker {}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid(), trade.getTakerNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getTakerNodeAddress(),
trade.getTakerPubKeyRing(),
initMultisigRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
takerAck = true;
checkComplete();
}
@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();
}
}
);
}
private void checkComplete() {
if (makerAck && takerAck) complete();
}
}

View file

@ -23,7 +23,6 @@ import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.TakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.SignContractRequest;
@ -62,8 +61,8 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
// thaw reserved outputs
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
for (String frozenKeyImage : processModel.getFrozenKeyImages()) {
wallet.thawOutput(frozenKeyImage);
for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) {
wallet.thawOutput(reserveTxKeyImage);
}
// create deposit tx

View file

@ -101,8 +101,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
new Date().getTime(),
updatedMultisigHex);
System.out.println("SENDING MESSAGE!!!!!!!");
System.out.println(message);
System.out.println("Sending message: " + message);
// TODO (woodser): trade.getTradingPeerNodeAddress() and/or trade.getTradingPeerPubKeyRing() are null on restart of application, so cannot send payment to complete trade
log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid(), trade.getTradingPeerNodeAddress());

View file

@ -18,14 +18,11 @@
package bisq.core.trade.protocol.tasks.maker;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.offer.Offer;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.user.User;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.common.app.Version;
@ -34,8 +31,6 @@ import bisq.common.taskrunner.TaskRunner;
import com.google.common.base.Charsets;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@ -55,88 +50,63 @@ public class MakerSendsInitTradeRequestIfUnreserved extends TradeTask {
try {
runInterceptHook();
System.out.println("MAKER SENDING INIT TRADE REQ TO ARBITRATOR");
// 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 request = (InitTradeRequest) processModel.getTradeMessage();
checkNotNull(request);
checkTradeId(processModel.getOfferId(), request);
InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to maker
checkNotNull(makerRequest);
checkTradeId(processModel.getOfferId(), makerRequest);
// collect fields to send taker prepared multisig response // TODO (woodser): this should happen on response from arbitrator
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
String offerId = processModel.getOffer().getId();
String payoutAddress = walletService.getWallet().createSubaddress(0).getAddress(); // TODO (woodser): register TRADE_PAYOUT?
walletService.getWallet().save();
checkNotNull(trade.getTradeAmount(), "TradeAmount must not be null");
// checkNotNull(trade.getTakerFeeTxId(), "TakeOfferFeeTxId must not be null"); // TODO (woodser): no taker fee tx yet if creating multisig first
final User user = processModel.getUser();
checkNotNull(user, "User must not be null");
final List<NodeAddress> acceptedMediatorAddresses = user.getAcceptedMediatorAddresses();
checkNotNull(acceptedMediatorAddresses, "acceptedMediatorAddresses must not be null");
// maker signs offer id as nonce to avoid challenge protocol // TODO (woodser): is this necessary?
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offer.getId().getBytes(Charsets.UTF_8));
// maker signs offer id as nonce to avoid challenge protocol
final PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade), "processModel.getPaymentAccountPayload(trade) must not be null");
byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offerId.getBytes(Charsets.UTF_8));
System.out.println("MAKER SENDING ARBITRTATOR SENDER NODE ADDRESS");
System.out.println(processModel.getMyNodeAddress());
if (true) throw new RuntimeException("Not yet implemented");
// create message to initialize trade
InitTradeRequest message = new InitTradeRequest(
offerId,
// create request to arbitrator
InitTradeRequest arbitratorRequest = new InitTradeRequest(
offer.getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
trade.getTradeAmount().value,
trade.getTradePrice().getValue(),
trade.getTakerFee().getValue(),
processModel.getAccountId(),
paymentAccountPayload.getId(),
paymentAccountPayload.getPaymentMethodId(),
offer.getAmount().value,
offer.getPrice().getValue(),
offer.getMakerFee().value,
trade.getProcessModel().getAccountId(),
offer.getMakerPaymentAccountId(),
offer.getOfferPayload().getPaymentMethodId(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
new Date().getTime(),
makerRequest.getCurrentDate(),
trade.getMakerNodeAddress(),
trade.getTakerNodeAddress(),
trade.getArbitratorNodeAddress(),
processModel.getReserveTx().getHash(), // TODO (woodser): need to first create and save reserve tx
processModel.getReserveTx().getFullHex(),
processModel.getReserveTx().getKey(),
processModel.getXmrWalletService().getAddressEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
trade.getSelf().getReserveTxHash(),
trade.getSelf().getReserveTxHex(),
trade.getSelf().getReserveTxKey(),
model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(),
null);
log.info("Send {} with offerId {} and uid {} to peer {}",
message.getClass().getSimpleName(), message.getTradeId(),
message.getUid(), trade.getArbitratorNodeAddress());
System.out.println("MAKER TRADE INFO");
System.out.println("Trading peer node address: " + trade.getTradingPeerNodeAddress());
System.out.println("Maker node address: " + trade.getMakerNodeAddress());
System.out.println("Taker node adddress: " + trade.getTakerNodeAddress());
System.out.println("Arbitrator node address: " + trade.getArbitratorNodeAddress());
// 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(
trade.getArbitratorNodeAddress(),
trade.getArbitratorPubKeyRing(),
message,
arbitratorRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}; uid={}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
complete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", message.getClass().getSimpleName(), message.getUid(), trade.getTradingPeerNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
log.warn("Failed to send {} to arbitrator, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage);
failed();
}
}
);
});
} catch (Throwable t) {
failed(t);
}

View file

@ -49,19 +49,18 @@ public class TakerReservesTradeFunds extends TradeTask {
// freeze trade funds
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
List<String> frozenKeyImages = new ArrayList<String>();
List<String> reserveTxKeyImages = new ArrayList<String>();
MoneroWallet wallet = model.getXmrWalletService().getWallet();
for (MoneroOutput input : reserveTx.getInputs()) {
frozenKeyImages.add(input.getKeyImage().getHex());
reserveTxKeyImages.add(input.getKeyImage().getHex());
wallet.freezeOutput(input.getKeyImage().getHex());
}
// save process state
// TODO (woodser): persist
processModel.setReserveTx(reserveTx);
processModel.setReserveTxHash(reserveTx.getHash());
processModel.setFrozenKeyImages(frozenKeyImages);
trade.setTakerFeeTxId(reserveTx.getHash());
processModel.getTaker().setReserveTxKeyImages(reserveTxKeyImages);
trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used?
//trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states
complete();
} catch (Throwable t) {

View file

@ -21,7 +21,7 @@ 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.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.common.app.Version;
@ -42,17 +42,55 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
try {
runInterceptHook();
// get primary arbitrator
Mediator arbitrator = processModel.getUser().getAcceptedMediatorByAddress(trade.getArbitratorNodeAddress());
if (arbitrator == null) throw new RuntimeException("Cannot get arbitrator instance from node address"); // TODO (woodser): null if arbitrator goes offline or never seen?
// send request to offer signer
sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
complete();
}
// save pub keys
// 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());
complete();
}
@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();
}
});
}
});
} catch (Throwable t) {
failed(t);
}
}
private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) {
// get registered arbitrator
Mediator arbitrator = processModel.getUser().getAcceptedMediatorByAddress(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());
trade.setMakerPubKeyRing(trade.getTradingPeer().getPubKeyRing());
// send trade request to arbitrator
InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage();
// create request to arbitrator
InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // taker's InitTradeRequest to maker
InitTradeRequest arbitratorRequest = new InitTradeRequest(
makerRequest.getTradeId(),
makerRequest.getSenderNodeAddress(),
@ -77,27 +115,12 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
processModel.getMakerSignature());
// send request to arbitrator
System.out.println("SENDING INIT TRADE REQUEST TO ARBITRATOR!");
log.info("Send {} with offerId {} and uid {} to arbitrator {} with pub key ring", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getTradeId(), arbitratorRequest.getUid(), trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing());
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(
trade.getArbitratorNodeAddress(),
trade.getArbitratorPubKeyRing(),
arbitratorNodeAddress,
arbitrator.getPubKeyRing(),
arbitratorRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}; uid={}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getTradeId(), arbitratorRequest.getUid());
}
@Override // TODO (woodser): handle case where primary arbitrator is unavailable so use backup arbitrator, distinguish offline from bad ack
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getUid(), trade.getArbitratorNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + arbitratorRequest + "\nerrorMessage=" + errorMessage);
failed();
}
}
listener
);
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -39,7 +39,7 @@ public class TradableListTest {
// test adding an OpenOffer and convert toProto
Offer offer = new Offer(offerPayload);
OpenOffer openOffer = new OpenOffer(offer);
OpenOffer openOffer = new OpenOffer(offer, 0, "", "", "");
openOfferTradableList.add(openOffer);
message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage();
assertEquals(message.getMessageCase(), TRADABLE_LIST);

View file

@ -219,7 +219,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
offerPayload.getHashOfChallenge(),
offerPayload.getExtraDataMap(),
offerPayload.getProtocolVersion(),
offerPayload.getArbitratorNodeAddress(),
offerPayload.getArbitratorSigner(),
offerPayload.getArbitratorSignature(),
offerPayload.getReserveTxKeyImages());

View file

@ -189,7 +189,7 @@ message OfferAvailabilityResponse {
repeated int32 supported_capabilities = 3;
string uid = 4;
string maker_signature = 5;
NodeAddress arbitrator_node_address = 6;
NodeAddress backup_arbitrator = 6;
}
message RefreshOfferMessage {
@ -853,7 +853,7 @@ message OfferPayload {
map<string, string> extra_data = 34;
int32 protocol_version = 35;
NodeAddress arbitrator_node_address = 1001;
NodeAddress arbitrator_signer = 1001;
string arbitrator_signature = 1002;
repeated string reserve_tx_key_images = 1003;
}
@ -1472,9 +1472,11 @@ message OpenOffer {
Offer offer = 1;
State state = 2;
NodeAddress arbitrator_node_address = 3;
NodeAddress backup_arbitrator = 3;
int64 trigger_price = 4;
repeated string frozen_key_images = 5;
string reserve_tx_hash = 5;
string reserve_tx_hex = 6;
string reserve_tx_key = 7;
}
message Tradable {
@ -1644,18 +1646,16 @@ message ProcessModel {
int64 seller_payout_amount_from_mediation = 20;
string maker_signature = 1001;
NodeAddress arbitrator_node_address = 1002;
NodeAddress backup_arbitrator = 1002;
TradingPeer maker = 1003;
TradingPeer taker = 1004;
TradingPeer arbitrator = 1005;
NodeAddress temp_trading_peer_node_address = 1006;
string reserve_tx_hash = 1007;
repeated string frozen_key_images = 1008;
string prepared_multisig_hex = 1009;
string made_multisig_hex = 1010;
bool multisig_setup_complete = 1011;
bool maker_ready_to_fund_multisig = 1012;
bool multisig_deposit_initiated = 1013;
string prepared_multisig_hex = 1007;
string made_multisig_hex = 1008;
bool multisig_setup_complete = 1009;
bool maker_ready_to_fund_multisig = 1010;
bool multisig_deposit_initiated = 1011;
}
message TradingPeer {
@ -1681,12 +1681,13 @@ message TradingPeer {
string reserve_tx_hash = 1001;
string reserve_tx_hex = 1002;
string reserve_tx_key = 1003;
string prepared_multisig_hex = 1004;
string made_multisig_hex = 1005;
string signed_payout_tx_hex = 1006;
string deposit_tx_hash = 1007;
string deposit_tx_hex = 1008;
string deposit_tx_key = 1009;
repeated string reserve_tx_key_images = 1004;
string prepared_multisig_hex = 1005;
string made_multisig_hex = 1006;
string signed_payout_tx_hex = 1007;
string deposit_tx_hash = 1008;
string deposit_tx_hex = 1009;
string deposit_tx_key = 1010;
}
///////////////////////////////////////////////////////////////////////////////////////////