mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
verify payout & dispute payout tx fees are within range of recreated tx
This commit is contained in:
parent
9260cf53ee
commit
a49611a234
5 changed files with 48 additions and 16 deletions
|
@ -698,6 +698,7 @@ public class TradeWalletService {
|
||||||
* @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signatures
|
* @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signatures
|
||||||
* @throws WalletException if the seller's wallet is null or structurally inconsistent
|
* @throws WalletException if the seller's wallet is null or structurally inconsistent
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx,
|
public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx,
|
||||||
byte[] buyerSignature,
|
byte[] buyerSignature,
|
||||||
Coin buyerPayoutAmount,
|
Coin buyerPayoutAmount,
|
||||||
|
|
|
@ -701,7 +701,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
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
|
// create dispute payout tx if not given
|
||||||
if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute, disputeResult); // can be null if already published or we don't have receiver's multisig hex
|
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
|
// persist result in dispute's chat message
|
||||||
ChatMessage chatMessage = new ChatMessage(
|
ChatMessage chatMessage = new ChatMessage(
|
||||||
|
@ -804,7 +804,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult) {
|
public MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult, boolean skipMultisigImport) {
|
||||||
|
|
||||||
// sync and save wallet
|
// sync and save wallet
|
||||||
trade.syncWallet();
|
trade.syncWallet();
|
||||||
|
@ -813,15 +813,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// create unsigned dispute payout tx if not already published and arbitrator has trader's updated multisig info
|
// create unsigned dispute payout tx if not already published and arbitrator has trader's updated multisig info
|
||||||
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
|
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
|
||||||
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) {
|
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null) {
|
||||||
|
MoneroWallet multisigWallet = trade.getWallet();
|
||||||
|
|
||||||
// import multisig hex
|
// import multisig hex
|
||||||
MoneroWallet multisigWallet = trade.getWallet();
|
if (!skipMultisigImport) {
|
||||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
||||||
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||||
if (!updatedMultisigHexes.isEmpty()) {
|
if (!updatedMultisigHexes.isEmpty()) {
|
||||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sync and save wallet
|
// sync and save wallet
|
||||||
trade.syncWallet();
|
trade.syncWallet();
|
||||||
|
|
|
@ -329,8 +329,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
||||||
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
||||||
|
|
||||||
// TODO: verify miner fee is within expected range
|
|
||||||
|
|
||||||
// verify winner and loser payout amounts
|
// verify winner and loser payout amounts
|
||||||
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
|
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
|
||||||
BigInteger expectedWinnerAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
BigInteger expectedWinnerAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||||
|
@ -348,6 +346,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
|
|
||||||
|
// verify mining fee is within tolerance by recreating payout tx
|
||||||
|
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||||
|
MoneroTxWallet feeEstimateTx = null;
|
||||||
|
try {
|
||||||
|
feeEstimateTx = createDisputePayoutTx(trade, dispute, disputeResult, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage());
|
||||||
|
}
|
||||||
|
if (feeEstimateTx != null) {
|
||||||
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
|
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// submit fully signed payout tx to the network
|
// submit fully signed payout tx to the network
|
||||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
|
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
|
||||||
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||||
|
|
|
@ -716,7 +716,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
||||||
|
|
||||||
// create transaction to get fee estimate
|
// create transaction to get fee estimate
|
||||||
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
|
|
||||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
|
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
|
||||||
|
@ -798,17 +797,34 @@ public abstract class Trade implements Tradable, Model {
|
||||||
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
||||||
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
||||||
|
|
||||||
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
|
// handle tx signing
|
||||||
|
|
||||||
// sign payout tx
|
|
||||||
if (sign) {
|
if (sign) {
|
||||||
|
|
||||||
|
// sign tx
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||||
payoutTxHex = result.getSignedMultisigTxHex();
|
payoutTxHex = result.getSignedMultisigTxHex();
|
||||||
|
describedTxSet = multisigWallet.describeMultisigTxSet(payoutTxHex); // update described set
|
||||||
|
payoutTx = describedTxSet.getTxs().get(0);
|
||||||
|
|
||||||
|
// verify fee is within tolerance by recreating payout tx
|
||||||
|
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||||
|
MoneroTxWallet feeEstimateTx = null;
|
||||||
|
try {
|
||||||
|
feeEstimateTx = createPayoutTx();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not recreate payout tx to verify fee: " + e.getMessage());
|
||||||
|
}
|
||||||
|
if (feeEstimateTx != null) {
|
||||||
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
|
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||||
|
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 " + payoutTx.getFee());
|
||||||
|
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update trade state
|
// update trade state
|
||||||
setPayoutTx(describedTxSet.getTxs().get(0));
|
setPayoutTx(payoutTx);
|
||||||
setPayoutTxHex(payoutTxHex);
|
setPayoutTxHex(payoutTxHex);
|
||||||
|
|
||||||
// submit payout tx
|
// submit payout tx
|
||||||
|
|
|
@ -583,7 +583,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
closeTicketButton.setOnAction(e -> {
|
closeTicketButton.setOnAction(e -> {
|
||||||
|
|
||||||
// create payout tx
|
// create payout tx
|
||||||
MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute, disputeResult);
|
MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute, disputeResult, false);
|
||||||
|
|
||||||
// show confirmation
|
// show confirmation
|
||||||
if (dispute.getSupportType() == SupportType.ARBITRATION &&
|
if (dispute.getSupportType() == SupportType.ARBITRATION &&
|
||||||
|
|
Loading…
Reference in a new issue