arbitrator sends original unsigned payout tx if published

synchronize on trade when processing dispute messages
This commit is contained in:
woodser 2023-02-25 08:13:44 -05:00
parent 17ac09fa4d
commit 6dca11f471
5 changed files with 135 additions and 123 deletions

View File

@ -36,7 +36,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.model.MoneroTxWallet;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format; import static java.lang.String.format;
@ -171,25 +170,25 @@ public class CoreDisputesService {
applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount);
// create dispute payout tx // create dispute payout tx
MoneroTxWallet disputePayoutTx = arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), disputeResult, false); trade.getProcessModel().setUnsignedPayoutTx(arbitrationManager.createDisputePayoutTx(trade, winningDispute.getContract(), disputeResult, false));
// close winning dispute ticket // close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, disputePayoutTx, () -> { closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
}, (errMessage, err) -> { }, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err); throw new IllegalStateException(errMessage, err);
}); });
// close loser's dispute ticket // close loser's dispute ticket
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() var loserDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId()) .filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId())
.findFirst(); .findFirst();
if (!peersDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute"); if (!loserDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute");
var peerDispute = peersDisputeOptional.get(); var loserDispute = loserDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); loserDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); loserDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, disputePayoutTx, () -> { closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> {
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
}, (errMessage, err) -> { }, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err); throw new IllegalStateException(errMessage, err);
@ -248,7 +247,7 @@ public class CoreDisputesService {
} }
} }
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) { public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler, FaultHandler faultHandler) {
DisputeResult.Reason reason = disputeResult.getReason(); DisputeResult.Reason reason = disputeResult.getReason();
String role = Res.get("shared.arbitrator"); String role = Res.get("shared.arbitrator");
@ -279,7 +278,7 @@ public class CoreDisputesService {
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, payoutTx, () -> { disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, () -> {
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);
dispute.setIsClosed(); dispute.setIsClosed();
resultHandler.handleResult(); resultHandler.handleResult();

View File

@ -443,105 +443,106 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
Dispute dispute = message.getDispute(); Dispute dispute = message.getDispute();
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
Trade trade = null; // get trade
String errorMessage = null; Trade trade = tradeManager.getTrade(dispute.getTradeId());
PubKeyRing senderPubKeyRing = null; if (trade == null) {
try { log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
// initialize }
T disputeList = getDisputeList();
if (disputeList == null) { synchronized (trade) {
log.warn("disputes is null"); String errorMessage = null;
return; PubKeyRing senderPubKeyRing = null;
}
dispute.setSupportType(message.getSupportType());
dispute.setState(Dispute.State.NEW);
Contract contract = dispute.getContract();
// validate dispute
try { try {
DisputeValidation.validateDisputeData(dispute);
DisputeValidation.validateNodeAddresses(dispute, config); // initialize
DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress()); T disputeList = getDisputeList();
DisputeValidation.validatePaymentAccountPayload(dispute); if (disputeList == null) {
//DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); log.warn("disputes is null");
} catch (DisputeValidation.ValidationException e) { return;
validationExceptions.add(e);
throw e;
}
// get trade
trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
// get sender
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// message to trader is expected from arbitrator
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
}
// arbitrator verifies signature of payment sent message if given
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
trade.advanceState(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
}
// update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// update peer node address
// TODO: tests can reuse the same addresses so nullify equal peer
sender.setNodeAddress(message.getSenderNodeAddress());
// add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
// add dispute
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.advanceDisputeState(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());
tradeManager.requestPersistence();
errorMessage = null;
} else {
// valid case if both have opened a dispute and agent was not online
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
dispute.getTradeId());
}
// add chat message with mediation info if applicable
addMediationResultMessage(dispute);
} else {
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
} }
dispute.setSupportType(message.getSupportType());
dispute.setState(Dispute.State.NEW);
Contract contract = dispute.getContract();
// validate dispute
try {
DisputeValidation.validateDisputeData(dispute);
DisputeValidation.validateNodeAddresses(dispute, config);
DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress());
DisputeValidation.validatePaymentAccountPayload(dispute);
//DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
} catch (DisputeValidation.ValidationException e) {
validationExceptions.add(e);
throw e;
}
// get sender
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// message to trader is expected from arbitrator
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
}
// arbitrator verifies signature of payment sent message if given
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
trade.advanceState(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
}
// update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// update peer node address
// TODO: tests can reuse the same addresses so nullify equal peer
sender.setNodeAddress(message.getSenderNodeAddress());
// add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
// add dispute
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.advanceDisputeState(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());
tradeManager.requestPersistence();
errorMessage = null;
} else {
// valid case if both have opened a dispute and agent was not online
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
dispute.getTradeId());
}
// add chat message with mediation info if applicable
addMediationResultMessage(dispute);
} else {
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
}
}
} catch (Exception e) {
errorMessage = e.getMessage();
log.warn(errorMessage);
if (trade != null) trade.setErrorMessage(errorMessage);
} }
} catch (Exception e) {
errorMessage = e.getMessage(); // use chat message instead of open dispute message for the ack
log.warn(errorMessage); ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
if (trade != null) trade.setErrorMessage(errorMessage); if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0);
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
}
requestPersistence();
} }
// use chat message instead of open dispute message for the ack
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
if (!messages.isEmpty()) {
ChatMessage msg = messages.get(0);
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
}
requestPersistence();
} }
// arbitrator sends dispute opened message to opener's peer // arbitrator sends dispute opened message to opener's peer
@ -700,16 +701,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
// arbitrator sends result to trader when their dispute is closed // arbitrator sends result to trader when their dispute is closed
public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) { public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler, FaultHandler faultHandler) {
try { try {
// get trade // get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) throw new RuntimeException("Dispute trade " + dispute.getTradeId() + " does not exist"); if (trade == null) throw new RuntimeException("Dispute trade " + dispute.getTradeId() + " does not exist");
// create dispute payout tx if not given
if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false); // can be null if already published or we don't have receiver's multisig hex
// persist result in dispute's chat message once // persist result in dispute's chat message once
boolean resending = disputeResult.getChatMessage() != null; boolean resending = disputeResult.getChatMessage() != null;
if (!resending) { if (!resending) {
@ -724,9 +722,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
} }
// create dispute closed message // create dispute payout tx if not published
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing()); TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
String unsignedPayoutTxHex = payoutTx == null ? null : payoutTx.getTxSet().getMultisigTxHex(); if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) {
trade.getProcessModel().setUnsignedPayoutTx(createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false)); // can be null if we don't have receiver's multisig hex
}
// create dispute closed message
MoneroTxWallet unsignedPayoutTx = receiver.getUpdatedMultisigHex() == null ? null : trade.getProcessModel().getUnsignedPayoutTx();
String unsignedPayoutTxHex = unsignedPayoutTx == null ? null : unsignedPayoutTx.getTxSet().getMultisigTxHex();
TradePeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); TradePeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = !resending && 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, DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
@ -799,9 +803,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
); );
// save state // save state
if (payoutTx != null) { if (unsignedPayoutTx != null) {
trade.setPayoutTx(payoutTx); trade.setPayoutTx(unsignedPayoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(unsignedPayoutTx.getTxSet().getMultisigTxHex());
} }
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence(); requestPersistence();

View File

@ -169,6 +169,9 @@ public class ProcessModel implements Model, PersistablePayload {
@Getter @Getter
@Setter @Setter
transient private MoneroTxWallet depositTxXmr; transient private MoneroTxWallet depositTxXmr;
@Getter
@Setter
transient private MoneroTxWallet unsignedPayoutTx;
@Nullable @Nullable
@Getter @Getter
@Setter @Setter

View File

@ -58,7 +58,8 @@ public class ResendDisputeClosedMessageWithPayout extends TradeTask {
for (Dispute dispute : disputes) { for (Dispute dispute : disputes) {
if (!dispute.isClosed()) continue; // dispute must be closed if (!dispute.isClosed()) continue; // dispute must be closed
if (sender.getPubKeyRing().equals(dispute.getTraderPubKeyRing())) { if (sender.getPubKeyRing().equals(dispute.getTraderPubKeyRing())) {
HavenoUtils.arbitrationManager.closeDisputeTicket(dispute.getDisputeResultProperty().get(), dispute, dispute.getDisputeResultProperty().get().summaryNotesProperty().get(), null, () -> { log.info("Arbitrator resending DisputeClosedMessage for trade {} after receiving updated multisig hex", trade.getId());
HavenoUtils.arbitrationManager.closeDisputeTicket(dispute.getDisputeResultProperty().get(), dispute, dispute.getDisputeResultProperty().get().summaryNotesProperty().get(), () -> {
completeAux(); completeAux();
}, (errMessage, err) -> { }, (errMessage, err) -> {
err.printStackTrace(); err.printStackTrace();

View File

@ -590,8 +590,13 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
closeTicketButton.setOnAction(e -> { closeTicketButton.setOnAction(e -> {
// get or create payout tx // get or create dispute payout tx
MoneroTxWallet payoutTx = trade.isPayoutPublished() ? trade.getPayoutTx() : arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false); MoneroTxWallet payoutTx = null;
if (trade.isPayoutPublished()) payoutTx = trade.getPayoutTx();
else {
payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, false);
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
}
// show confirmation // show confirmation
if (dispute.getSupportType() == SupportType.ARBITRATION && if (dispute.getSupportType() == SupportType.ARBITRATION &&
@ -600,9 +605,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
showPayoutTxConfirmation(contract, showPayoutTxConfirmation(contract,
disputeResult, disputeResult,
payoutTx, payoutTx,
() -> doClose(closeTicketButton, payoutTx)); () -> doClose(closeTicketButton));
} else { } else {
doClose(closeTicketButton, payoutTx); doClose(closeTicketButton);
} }
}); });
@ -654,7 +659,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
} }
private void doClose(Button closeTicketButton, MoneroTxWallet payoutTx) { private void doClose(Button closeTicketButton) {
DisputeManager<? extends DisputeList<Dispute>> disputeManager = getDisputeManager(dispute); DisputeManager<? extends DisputeList<Dispute>> disputeManager = getDisputeManager(dispute);
if (disputeManager == null) { if (disputeManager == null) {
return; return;
@ -663,7 +668,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, payoutTx, () -> { disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> {
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show(); new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show();
} }