resend dispute closed message with payout tx on updated multisig info

advance dispute state if progress
SupportManager processes messages in order
re-arrange domain initialization to fix null pub key in DisputeManager
This commit is contained in:
woodser 2023-01-23 14:25:28 -05:00
parent 533639bd19
commit 23393bb4b8
7 changed files with 176 additions and 46 deletions

View file

@ -176,6 +176,10 @@ public class DomainInitialisation {
PersistenceManager.onAllServicesInitialized();
arbitratorManager.onAllServicesInitialized();
mediatorManager.onAllServicesInitialized();
refundAgentManager.onAllServicesInitialized();
tradeManager.onAllServicesInitialized();
arbitrationManager.onAllServicesInitialized();
mediationManager.onAllServicesInitialized();
@ -192,10 +196,6 @@ public class DomainInitialisation {
walletAppSetup.setRejectedTxErrorMessageHandler(rejectedTxErrorMessageHandler, openOfferManager, tradeManager);
arbitratorManager.onAllServicesInitialized();
mediatorManager.onAllServicesInitialized();
refundAgentManager.onAllServicesInitialized();
privateNotificationManager.privateNotificationProperty().addListener((observable, oldValue, newValue) -> {
if (displayPrivateNotificationHandler != null)
displayPrivateNotificationHandler.accept(newValue);

View file

@ -22,13 +22,15 @@ import bisq.core.api.CoreNotificationService;
import bisq.core.locale.Res;
import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.protocol.TradeProtocol;
import bisq.core.trade.protocol.TradeProtocol.MailboxMessageComparator;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.AckMessageSourceType;
import bisq.network.p2p.DecryptedMessageWithPubKey;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.mailbox.MailboxMessage;
import bisq.network.p2p.mailbox.MailboxMessageService;
import bisq.common.Timer;
@ -36,6 +38,7 @@ import bisq.common.UserThread;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -322,9 +325,27 @@ public abstract class SupportManager {
private void applyMessages() {
synchronized (lock) {
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
// apply non-mailbox messages
decryptedDirectMessageWithPubKeys.stream()
.filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage))
.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.stream()
.filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage))
.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
// apply mailbox messages in order
decryptedDirectMessageWithPubKeys.stream()
.filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage))
.sorted(new DecryptedMessageWithPubKeyComparator())
.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.stream()
.filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage))
.sorted(new DecryptedMessageWithPubKeyComparator())
.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
// clear messages
decryptedDirectMessageWithPubKeys.clear();
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.clear();
}
}
@ -351,4 +372,22 @@ public abstract class SupportManager {
mailboxMessageService.removeMailboxMsg(ackMessage);
}
}
private static class DecryptedMessageWithPubKeyComparator implements Comparator<DecryptedMessageWithPubKey> {
MailboxMessageComparator mailboxMessageComparator;
public DecryptedMessageWithPubKeyComparator() {
mailboxMessageComparator = new TradeProtocol.MailboxMessageComparator();
}
@Override
public int compare(DecryptedMessageWithPubKey m1, DecryptedMessageWithPubKey m2) {
if (m1.getNetworkEnvelope() instanceof MailboxMessage) {
if (m2.getNetworkEnvelope() instanceof MailboxMessage) return mailboxMessageComparator.compare((MailboxMessage) m1.getNetworkEnvelope(), (MailboxMessage) m2.getNetworkEnvelope());
else return 1;
} else {
return m2.getNetworkEnvelope() instanceof MailboxMessage ? -1 : 0;
}
}
}
}

View file

@ -97,7 +97,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
protected final TradeManager tradeManager;
protected final ClosedTradableManager closedTradableManager;
protected final OpenOfferManager openOfferManager;
protected final PubKeyRing pubKeyRing;
protected final KeyRing keyRing;
protected final DisputeListService<T> disputeListService;
private final Config config;
private final PriceFeedService priceFeedService;
@ -105,9 +105,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
@Getter
protected final ObservableList<TradeDataValidation.ValidationException> validationExceptions =
FXCollections.observableArrayList();
@Getter
private final KeyPair signatureKeyPair;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -132,18 +129,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager;
this.openOfferManager = openOfferManager;
this.pubKeyRing = keyRing.getPubKeyRing();
signatureKeyPair = keyRing.getSignatureKeyPair();
this.keyRing = keyRing;
this.disputeListService = disputeListService;
this.config = config;
this.priceFeedService = priceFeedService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
public KeyPair getSignatureKeyPair() {
return keyRing.getSignatureKeyPair();
}
@Override
public void requestPersistence() {
disputeListService.requestPersistence();
@ -204,7 +203,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// Abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
// We get that message at both peers. The dispute object is in context of the trader
// We get this message at both peers. The dispute object is in context of the trader
public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage);
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
@ -292,7 +291,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
public boolean isTrader(Dispute dispute) {
return pubKeyRing.equals(dispute.getTraderPubKeyRing());
return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing());
}
public Optional<Dispute> findOwnDispute(String tradeId) {
@ -352,7 +351,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
pubKeyRing.hashCode(),
keyRing.getPubKeyRing().hashCode(),
false,
Res.get("support.systemMsg", sysMsg),
p2PService.getAddress());
@ -389,7 +388,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence();
resultHandler.handleResult();
}
@ -405,7 +404,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// We use the chatMessage wrapped inside the openNewDisputeMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
requestPersistence();
resultHandler.handleResult();
}
@ -507,7 +506,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
// send dispute opened message to peer if arbitrator
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
@ -703,22 +702,25 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// create dispute payout tx if not given
if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute, disputeResult, false); // can be null if already published or we don't have receiver's multisig hex
// persist result in dispute's chat message
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(),
false,
summaryText,
p2PService.getAddress());
disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage);
// persist result in dispute's chat message once
boolean resending = disputeResult.getChatMessage() != null;
if (!resending) {
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(),
false,
summaryText,
p2PService.getAddress());
disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage);
}
// create dispute closed message
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
String unsignedPayoutTxHex = payoutTx == null ? null : payoutTx.getTxSet().getMultisigTxHex();
TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
boolean deferPublishPayout = !resending && unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
p2PService.getAddress(),
UUID.randomUUID().toString(),
@ -731,7 +733,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(),
disputeClosedMessage.getUid(), chatMessage.getUid());
disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid());
mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(),
dispute.getTraderPubKeyRing(),
disputeClosedMessage,
@ -742,11 +744,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
disputeResult.getChatMessage().getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
disputeResult.getChatMessage().setArrived(true);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.syncWalletNormallyForMs(30000);
requestPersistence();
@ -759,11 +761,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
disputeResult.getChatMessage().getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
disputeResult.getChatMessage().setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence();
@ -776,11 +778,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
"chatMessage.uid={}, errorMessage={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid(), errorMessage);
disputeResult.getChatMessage().getUid(), errorMessage);
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage);
disputeResult.getChatMessage().setSendMessageError(errorMessage);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence();
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
@ -914,7 +916,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
private boolean isAgent(Dispute dispute) {
return pubKeyRing.equals(dispute.getAgentPubKeyRing());
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
}
private Optional<Dispute> findDispute(Dispute dispute) {
@ -987,7 +989,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
ChatMessage mediatorsDisputeClosedMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
pubKeyRing.hashCode(),
keyRing.getPubKeyRing().hashCode(),
false,
mediatorsDisputeResult,
p2PService.getAddress());
@ -1074,7 +1076,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
ChatMessage priceInfoMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
pubKeyRing.hashCode(),
keyRing.getPubKeyRing().hashCode(),
false,
priceInfoText,
p2PService.getAddress());

View file

@ -88,6 +88,7 @@ public class DisputeSummaryVerification {
throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed"));
}
} catch (Throwable e) {
e.printStackTrace();
throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat"));
}
}

View file

@ -211,7 +211,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
Dispute dispute = disputeOptional.get();
// verify that arbitrator does not get DisputeClosedMessage
if (pubKeyRing.equals(dispute.getAgentPubKeyRing())) {
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
log.error("Arbitrator received disputeResultMessage. That should never happen.");
return;
}
@ -225,8 +225,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) {
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
"again because the first close did not succeed. TradeId = " + tradeId);
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
}
dispute.setDisputeResult(disputeResult);

View file

@ -41,6 +41,7 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.ResendDisputeClosedMessageWithPayout;
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
@ -73,6 +74,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
@ -162,6 +164,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// TODO (woodser): this method only necessary because isPubKeyValid not called with sender argument, so it's validated before
private void handleMailboxCollectionSkipValidation(Collection<DecryptedMessageWithPubKey> collection) {
log.warn("TradeProtocol.handleMailboxCollectionSkipValidation");
collection.stream()
.map(DecryptedMessageWithPubKey::getNetworkEnvelope)
.filter(this::isMyMessage)
@ -181,8 +184,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.forEach(this::handleMailboxMessage);
}
private static class MailboxMessageComparator implements Comparator<MailboxMessage> {
public static class MailboxMessageComparator implements Comparator<MailboxMessage> {
private static List<Class<? extends MailboxMessage>> messageOrder = Arrays.asList(
AckMessage.class,
DepositsConfirmedMessage.class,
PaymentSentMessage.class,
PaymentReceivedMessage.class,
@ -397,7 +401,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.from(sender))
.setup(tasks(
ProcessDepositsConfirmedMessage.class,
VerifyPeersAccountAgeWitness.class)
VerifyPeersAccountAgeWitness.class,
ResendDisputeClosedMessageWithPayout.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(sender, response);

View file

@ -0,0 +1,84 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.support.dispute.Dispute;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.Validator;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.List;
@Slf4j
public class ResendDisputeClosedMessageWithPayout extends TradeTask {
@SuppressWarnings({"unused"})
public ResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// get peer
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
checkNotNull(request);
Validator.checkTradeId(processModel.getOfferId(), request);
TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing());
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// arbitrator resends DisputeClosedMessage with payout tx when updated multisig info received
boolean ticketClosed = false;
if (!trade.isPayoutPublished() && trade.isArbitrator()) {
List<Dispute> disputes = trade.getDisputes();
for (Dispute dispute : disputes) {
if (!dispute.isClosed()) continue; // dispute must be closed
if (sender.getPubKeyRing().equals(dispute.getTraderPubKeyRing())) {
HavenoUtils.arbitrationManager.closeDisputeTicket(dispute.getDisputeResultProperty().get(), dispute, dispute.getDisputeResultProperty().get().summaryNotesProperty().get(), null, () -> {
completeAux();
}, (errMessage, err) -> {
err.printStackTrace();
failed(err);
});
ticketClosed = true;
break;
}
}
}
// complete if not waiting for result
if (!ticketClosed) completeAux();
} catch (Throwable t) {
failed(t);
}
}
private void completeAux() {
processModel.getTradeManager().requestPersistence();
complete();
}
}