use nack flow if cannot create payout tx and stop repeat sending

This commit is contained in:
woodser 2025-09-24 01:38:59 -04:00
parent 0bf6052b7c
commit 4989eab498
No known key found for this signature in database
GPG key ID: 55A10DD48ADEE5EF
7 changed files with 44 additions and 21 deletions

View file

@ -542,7 +542,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
break; break;
} catch (Exception e) { } catch (Exception e) {
if (trade.isPayoutPublished()) return null; if (trade.isPayoutPublished()) return null;
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e); if (HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e);
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection); if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);

View file

@ -630,22 +630,35 @@ public class HavenoUtils {
return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e); return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e);
} }
public static boolean isNotEnoughSigners(Throwable e) { private static boolean isNotEnoughSigners(Throwable e) {
return e != null && e.getMessage().contains("Not enough signers"); return e != null && e.getMessage().contains("Not enough signers");
} }
public static boolean isFailedToParse(Throwable e) { private static boolean isFailedToParse(Throwable e) {
return e != null && e.getMessage().contains("Failed to parse"); return e != null && e.getMessage().contains("Failed to parse");
} }
private static boolean isStaleData(Throwable e) {
return e != null && e.getMessage().contains("stale data");
}
private static boolean isNoTransactionCreated(Throwable e) {
return e != null && e.getMessage().contains("No transaction created");
}
private static boolean isLRNotFound(Throwable e) {
return e != null && e.getMessage().contains("LR not found for enough participants");
}
// TODO: handling specific error messages is brittle, inverse so all errors are illegal except known local issues?
public static boolean isMultisigError(Throwable e) {
return isLRNotFound(e) || isNotEnoughSigners(e) || isNoTransactionCreated(e) || isFailedToParse(e) || isStaleData(e);
}
public static boolean isTransactionRejected(Throwable e) { public static boolean isTransactionRejected(Throwable e) {
return e != null && e.getMessage().contains("was rejected"); return e != null && e.getMessage().contains("was rejected");
} }
public static boolean isLRNotFound(Throwable e) {
return e != null && e.getMessage().contains("LR not found for enough participants");
}
public static boolean isIllegal(Throwable e) { public static boolean isIllegal(Throwable e) {
return e instanceof IllegalArgumentException || e instanceof IllegalStateException; return e instanceof IllegalArgumentException || e instanceof IllegalStateException;
} }

View file

@ -828,7 +828,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
for (TradePeer peer : getAllPeers()) { for (TradePeer peer : getAllPeers()) {
peer.setPaymentReceivedMessage(null); peer.setPaymentReceivedMessage(null);
peer.setPaymentReceivedMessageState(MessageState.UNDEFINED);
} }
setPayoutTxHex(null); setPayoutTxHex(null);
} }
@ -1385,7 +1384,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
.setRelay(false) .setRelay(false)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
} catch (Exception e) { } catch (Exception e) {
if (HavenoUtils.isLRNotFound(e)) throw new IllegalStateException(e); if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e);
else throw e; else throw e;
} }
@ -1409,6 +1408,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) { } catch (IllegalArgumentException | IllegalStateException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e);
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
handleWalletError(e, sourceConnection, i + 1); handleWalletError(e, sourceConnection, i + 1);
doPollWallet(); doPollWallet();
@ -1557,7 +1557,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
setPayoutStatePublished(); setPayoutStatePublished();
} catch (Exception e) { } catch (Exception e) {
if (!isPayoutPublished()) { if (!isPayoutPublished()) {
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e); if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e);
throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId() + ", error=" + e.getMessage(), e); throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId() + ", error=" + e.getMessage(), e);
} }
} }
@ -2191,13 +2191,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
throw new RuntimeException("Trade is not maker, taker, or arbitrator"); throw new RuntimeException("Trade is not maker, taker, or arbitrator");
} }
private List<TradePeer> getOtherPeers() { public List<TradePeer> getOtherPeers() {
List<TradePeer> peers = getAllPeers(); List<TradePeer> peers = getAllPeers();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers; return peers;
} }
private List<TradePeer> getAllPeers() { public List<TradePeer> getAllPeers() {
List<TradePeer> peers = new ArrayList<TradePeer>(); List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker()); peers.add(getMaker());
peers.add(getTaker()); peers.add(getTaker());
@ -3194,7 +3194,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
processModel.setPaymentSentPayoutTxStale(true); processModel.setPaymentSentPayoutTxStale(true);
if (paymentReceivedNackSender != null) { if (paymentReceivedNackSender != null) {
paymentReceivedNackSender.setPaymentReceivedMessage(null); paymentReceivedNackSender.setPaymentReceivedMessage(null);
paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.UNDEFINED); paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.NACKED);
} }
if (!isPayoutPublished()) { if (!isPayoutPublished()) {
getSelf().setUnsignedPayoutTxHex(null); getSelf().setUnsignedPayoutTxHex(null);
@ -3302,8 +3302,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// rescan blockchain // rescan blockchain
rescanBlockchain(); rescanBlockchain();
// import multisig hex // must import multisig hex after rescan
log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId()); log.warn("Importing multisig hex after rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
importMultisigHex(); importMultisigHex();
// poll wallet // poll wallet

View file

@ -120,7 +120,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
private boolean makerInitTradeRequestHasBeenNacked = false; private boolean makerInitTradeRequestHasBeenNacked = false;
private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null; private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null;
private static int MAX_PAYMENT_RECEIVED_NACKS = 5; private static int MAX_PAYMENT_RECEIVED_NACKS = 6;
private int numPaymentReceivedNacks = 0; private int numPaymentReceivedNacks = 0;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -47,7 +47,6 @@ import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.util.Validator; import haveno.core.util.Validator;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
@ -66,7 +65,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage(); PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
checkNotNull(message); checkNotNull(message);
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
// verify signature of payment received message // verify signature of payment received message
HavenoUtils.verifyPaymentReceivedMessage(trade, message); HavenoUtils.verifyPaymentReceivedMessage(trade, message);
@ -146,6 +144,11 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// handle if payout tx not published // handle if payout tx not published
if (!trade.isPayoutPublished()) { if (!trade.isPayoutPublished()) {
// nack with updated multisig info if no payout tx provided
if (message.getUnsignedPayoutTxHex() == null && message.getSignedPayoutTxHex() == null && message.getPayoutTxId() == null) {
throw new IllegalStateException("No payout tx provided in PaymentReceivedMessage for " + trade.getClass().getSimpleName() + " " + trade.getId());
}
// wait to publish payout tx if defer flag set from seller (payout is expected) // wait to publish payout tx if defer flag set from seller (payout is expected)
if (message.isDeferPublishPayout()) { if (message.isDeferPublishPayout()) {
log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());

View file

@ -107,7 +107,13 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
trade.requestPersistence(); trade.requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); if (HavenoUtils.isIllegal(t)) {
log.error("Illegal exception preparing payment received message in {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage(), t);
trade.exportMultisigHex();
complete(); // proceed to send the message to perform nack flow with updated multsig state
} else {
failed(t);
}
} }
} }

View file

@ -96,6 +96,9 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
try { try {
runInterceptHook(); runInterceptHook();
// reset ack state
getReceiver().setPaymentReceivedMessageState(MessageState.UNDEFINED);
// skip if stopped // skip if stopped
if (stopSending()) { if (stopSending()) {
if (!isCompleted()) complete(); if (!isCompleted()) complete();
@ -149,8 +152,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
// verify message // verify message
if (trade.isPayoutPublished()) { if (trade.isPayoutPublished()) {
checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published"); checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published");
} else {
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex");
} }
// sign message // sign message