seller signs payout tx unless illegal state, avoid recreating payout tx

This commit is contained in:
woodser 2024-06-06 12:53:31 -04:00
parent dd28c237c9
commit f252265ede
9 changed files with 69 additions and 46 deletions

View file

@ -901,8 +901,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (updateState) { if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx); trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
trade.setPayoutTx(payoutTx); trade.updatePayout(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} }

View file

@ -62,6 +62,7 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage; import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Contract; import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
@ -86,11 +87,9 @@ import monero.wallet.model.MoneroTxWallet;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -433,19 +432,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// check daemon connection // check daemon connection
trade.verifyDaemonConnection(); trade.verifyDaemonConnection();
// determine if we already signed dispute payout tx // adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field? // TODO: remove after future updates to allow old trades to clear
Set<String> nonSignedDisputePayoutTxHexes = new HashSet<String>(); if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
if (trade.getTradePeer().getPaymentSentMessage() != null) nonSignedDisputePayoutTxHexes.add(trade.getTradePeer().getPaymentSentMessage().getPayoutTxHex()); log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade.getTradePeer().getPaymentReceivedMessage() != null) { if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
nonSignedDisputePayoutTxHexes.add(trade.getTradePeer().getPaymentReceivedMessage().getUnsignedPayoutTxHex()); trade.setPayoutTxHex(null);
nonSignedDisputePayoutTxHexes.add(trade.getTradePeer().getPaymentReceivedMessage().getSignedPayoutTxHex());
} }
boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex());
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
if (signed) disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex()); if (trade.getPayoutTxHex() == null) {
else {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
@ -468,6 +464,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee()); if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff); log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
} }
} else {
disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
} }
// submit fully signed payout tx to the network // submit fully signed payout tx to the network
@ -485,8 +483,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
// update state // update state
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? trade.updatePayout(disputeTxSet.getTxs().get(0));
trade.setPayoutTxId(disputeTxSet.getTxs().get(0).getHash());
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED); trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash()); dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
return disputeTxSet; return disputeTxSet;

View file

@ -482,7 +482,7 @@ public abstract class Trade implements Tradable, Model {
@Nullable @Nullable
@Getter @Getter
@Setter @Setter
private String payoutTxHex; private String payoutTxHex; // signed payout tx hex
@Getter @Getter
@Setter @Setter
private String payoutTxKey; private String payoutTxKey;
@ -1230,8 +1230,9 @@ public abstract class Trade implements Tradable, Model {
// sign tx // sign tx
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx"); if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null");
payoutTxHex = result.getSignedMultisigTxHex(); payoutTxHex = result.getSignedMultisigTxHex();
setPayoutTxHex(payoutTxHex);
// describe result // describe result
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
@ -1247,8 +1248,8 @@ public abstract class Trade implements Tradable, Model {
} }
// update trade state // update trade state
setPayoutTx(payoutTx); updatePayout(payoutTx);
setPayoutTxHex(payoutTxHex); requestPersistence();
// submit payout tx // submit payout tx
if (publish) { if (publish) {
@ -1692,7 +1693,7 @@ public abstract class Trade implements Tradable, Model {
getVolumeProperty().set(getVolume()); getVolumeProperty().set(getVolume());
} }
public void setPayoutTx(MoneroTxWallet payoutTx) { public void updatePayout(MoneroTxWallet payoutTx) {
// set payout tx fields // set payout tx fields
this.payoutTx = payoutTx; this.payoutTx = payoutTx;
@ -2467,7 +2468,7 @@ public abstract class Trade implements Tradable, Model {
// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
for (MoneroTxWallet tx : txs) { for (MoneroTxWallet tx : txs) {
if (tx.isOutgoing() && !tx.isFailed()) { if (tx.isOutgoing() && !tx.isFailed()) {
setPayoutTx(tx); updatePayout(tx);
setPayoutStatePublished(); setPayoutStatePublished();
if (tx.isConfirmed()) setPayoutStateConfirmed(); if (tx.isConfirmed()) setPayoutStateConfirmed();
if (!tx.isLocked()) setPayoutStateUnlocked(); if (!tx.isLocked()) setPayoutStateUnlocked();

View file

@ -63,7 +63,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
runInterceptHook(); runInterceptHook();
// skip if payout tx already created // skip if payout tx already created
if (trade.getPayoutTxHex() != null) { if (trade.getSelf().getUnsignedPayoutTxHex() != null) {
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
complete(); complete();
return; return;
@ -85,8 +85,8 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// create payout tx // create payout tx
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx); trade.updatePayout(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} }
complete(); complete();

View file

@ -120,7 +120,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
trade.getCounterCurrencyTxId(), trade.getCounterCurrencyTxId(),
trade.getCounterCurrencyExtraData(), trade.getCounterCurrencyExtraData(),
deterministicId, deterministicId,
trade.getPayoutTxHex(), trade.getSelf().getUnsignedPayoutTxHex(),
trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
trade.getSelf().getPaymentAccountKey(), trade.getSelf().getPaymentAccountKey(),
trade.getTradePeer().getAccountAgeWitness() trade.getTradePeer().getAccountAgeWitness()

View file

@ -45,7 +45,6 @@ import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage; 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 org.apache.commons.lang3.StringUtils;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -132,6 +131,14 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
private void processPayoutTx(PaymentReceivedMessage message) { private void processPayoutTx(PaymentReceivedMessage message) {
// adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
trade.setPayoutTxHex(null);
}
// update wallet // update wallet
trade.importMultisigHex(); trade.importMultisigHex();
trade.syncAndPollWallet(); trade.syncAndPollWallet();
@ -160,11 +167,11 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
try { try {
PaymentSentMessage paymentSentMessage = (trade.isArbitrator() ? trade.getBuyer() : trade.getArbitrator()).getPaymentSentMessage(); PaymentSentMessage paymentSentMessage = (trade.isArbitrator() ? trade.getBuyer() : trade.getArbitrator()).getPaymentSentMessage();
if (paymentSentMessage == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId()); if (paymentSentMessage == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId());
if (StringUtils.equals(trade.getPayoutTxHex(), paymentSentMessage.getPayoutTxHex())) { // unsigned if (trade.getPayoutTxHex() == null) { // unsigned
log.info("{} {} verifying, signing, and publishing payout tx", trade.getClass().getSimpleName(), trade.getId()); log.info("{} {} verifying, signing, and publishing payout tx", trade.getClass().getSimpleName(), trade.getId());
trade.processPayoutTx(message.getUnsignedPayoutTxHex(), true, true); trade.processPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
} else { } else {
log.info("{} {} re-verifying and publishing payout tx", trade.getClass().getSimpleName(), trade.getId()); log.info("{} {} re-verifying and publishing signed payout tx", trade.getClass().getSimpleName(), trade.getId());
trade.processPayoutTx(trade.getPayoutTxHex(), false, true); trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
} }
} catch (Exception e) { } catch (Exception e) {

View file

@ -49,7 +49,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
// update state from message // update state from message
trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setPaymentSentMessage(message);
trade.setPayoutTxHex(message.getPayoutTxHex());
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
String counterCurrencyTxId = message.getCounterCurrencyTxId(); String counterCurrencyTxId = message.getCounterCurrencyTxId();

View file

@ -42,25 +42,45 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
// handle first time preparation // handle first time preparation
if (trade.getArbitrator().getPaymentReceivedMessage() == null) { if (trade.getArbitrator().getPaymentReceivedMessage() == null) {
// import multisig hex // adapt from 1.0.6 to 1.0.7 which changes field usage
trade.importMultisigHex(); // TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
// verify, sign, and publish payout tx if given. otherwise create payout tx log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade.getPayoutTxHex() != null) { trade.setPayoutTxHex(null);
try {
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getPayoutTxHex(), true, true);
} catch (Exception e) {
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}. Creating unsigned payout tx", trade.getId(), e.getMessage());
createUnsignedPayoutTx();
} }
// import multisig hex unless already signed
if (trade.getPayoutTxHex() == null) {
trade.importMultisigHex();
}
// verify, sign, and publish payout tx if given
if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null) {
try {
if (trade.getPayoutTxHex() == null) {
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true);
} else { } else {
log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
}
} catch (IllegalArgumentException | IllegalStateException e) {
log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}: {}. Creating new unsigned payout tx", trade.getClass().getSimpleName(), trade.getId(), e.getMessage());
createUnsignedPayoutTx();
} catch (Exception e) {
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage());
throw e;
}
}
// otherwise create unsigned payout tx
else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
createUnsignedPayoutTx(); createUnsignedPayoutTx();
} }
} else if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) { } else if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
// republish payout tx from previous message // republish payout tx from previous message
log.info("Seller re-verifying and publishing payout tx for trade {}", trade.getId()); log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true); trade.processPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true);
} }
@ -80,7 +100,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
private void createUnsignedPayoutTx() { private void createUnsignedPayoutTx() {
log.info("Seller creating unsigned payout tx for trade {}", trade.getId()); log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx); trade.updatePayout(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} }
} }

View file

@ -50,7 +50,7 @@ import haveno.network.p2p.NodeAddress;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkArgument;
@Slf4j @Slf4j
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -85,7 +85,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
@Override @Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
if (getReceiver().getPaymentReceivedMessage() == null) { if (getReceiver().getPaymentReceivedMessage() == null) {
// sign account witness // sign account witness
@ -104,14 +103,15 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
tradeId, tradeId,
processModel.getMyNodeAddress(), processModel.getMyNodeAddress(),
deterministicId, deterministicId,
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null, // unsigned // TODO: phase in after next update to clear old style trades
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed trade.getPayoutTxHex() == null ? null : trade.getPayoutTxHex(), // signed
trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
trade.getTradePeer().getAccountAgeWitness(), trade.getTradePeer().getAccountAgeWitness(),
signedWitness, signedWitness,
getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message
); );
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex");
// sign message // sign message
try { try {