From 6798630dfc1fde57bcbcbc84376436dafda84a14 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 17 Sep 2021 16:29:54 -0400 Subject: [PATCH] filter offers with spent or duplicate funds using key images reserve tx does not remain in arbitrator pool --- .../java/bisq/core/api/CoreOffersService.java | 57 +++++++++- .../bisq/core/btc/setup/WalletConfig.java | 8 +- .../bisq/core/offer/CreateOfferService.java | 1 + .../java/bisq/core/offer/OfferPayload.java | 18 ++-- .../bisq/core/offer/OpenOfferManager.java | 8 +- .../core/offer/messages/SignOfferRequest.java | 8 ++ .../tasks/MakerReservesTradeFunds.java | 5 +- .../tasks/MakerSendsSignOfferRequest.java | 5 +- .../main/java/bisq/core/trade/TradeUtils.java | 102 +++++++++++------- .../tasks/ArbitratorProcessesReserveTx.java | 1 + .../protocol/tasks/ProcessDepositRequest.java | 7 +- .../test/java/bisq/core/offer/OfferMaker.java | 1 + .../bisq/daemon/grpc/GrpcTradesService.java | 6 +- .../editoffer/EditOfferDataModel.java | 3 +- .../trades/TradesChartsViewModelTest.java | 1 + .../offerbook/OfferBookViewModelTest.java | 1 + .../java/bisq/desktop/maker/OfferMaker.java | 1 + proto/src/main/proto/pb.proto | 4 +- 18 files changed, 166 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index e18a760c07..6580e9592d 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -17,6 +17,7 @@ package bisq.core.api; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; @@ -39,14 +40,17 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.math.BigDecimal; - +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import monero.daemon.model.MoneroKeyImageSpentStatus; import static bisq.common.util.MathUtils.exactMultiply; import static bisq.common.util.MathUtils.roundDoubleToLong; @@ -77,6 +81,7 @@ class CoreOffersService { private final OpenOfferManager openOfferManager; private final OfferUtil offerUtil; private final User user; + private final XmrWalletService xmrWalletService; @Inject public CoreOffersService(CoreContext coreContext, @@ -87,7 +92,8 @@ class CoreOffersService { OfferFilter offerFilter, OpenOfferManager openOfferManager, OfferUtil offerUtil, - User user) { + User user, + XmrWalletService xmrWalletService) { this.coreContext = coreContext; this.keyRing = keyRing; this.coreWalletsService = coreWalletsService; @@ -97,6 +103,7 @@ class CoreOffersService { this.openOfferManager = openOfferManager; this.offerUtil = offerUtil; this.user = user; + this.xmrWalletService = xmrWalletService; } Offer getOffer(String id) { @@ -117,20 +124,62 @@ class CoreOffersService { } List getOffers(String direction, String currencyCode) { - return offerBookService.getOffers().stream() + List offers = offerBookService.getOffers().stream() .filter(o -> !o.isMyOffer(keyRing)) .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid()) .sorted(priceComparator(direction)) .collect(Collectors.toList()); + offers.removeAll(getUnreservedOffers(offers)); + return offers; } List getMyOffers(String direction, String currencyCode) { - return offerBookService.getOffers().stream() + List offers = offerBookService.getOffers().stream() .filter(o -> o.isMyOffer(keyRing)) .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .sorted(priceComparator(direction)) .collect(Collectors.toList()); + Set unreservedOffers = getUnreservedOffers(offers); + offers.removeAll(unreservedOffers); + + // remove my unreserved offers from offer manager + List unreservedOpenOffers = new ArrayList(); + for (Offer unreservedOffer : unreservedOffers) { + unreservedOpenOffers.add(openOfferManager.getOpenOfferById(unreservedOffer.getId()).get()); + } + openOfferManager.removeOpenOffers(unreservedOpenOffers, null); + return offers; + } + + private Set getUnreservedOffers(List offers) { + Set unreservedOffers = new HashSet(); + + // collect reserved key images and check for duplicate funds + List allKeyImages = new ArrayList(); + for (Offer offer : offers) { + for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { + if (!allKeyImages.add(keyImage)) unreservedOffers.add(offer); + } + } + + // get spent key images + // TODO (woodser): paginate offers and only check key images of current page + List spentKeyImages = new ArrayList(); + List spentStatuses = allKeyImages.isEmpty() ? new ArrayList() : xmrWalletService.getDaemon().getKeyImageSpentStatuses(allKeyImages); + for (int i = 0; i < spentStatuses.size(); i++) { + if (spentStatuses.get(i) != MoneroKeyImageSpentStatus.NOT_SPENT) spentKeyImages.add(allKeyImages.get(i)); + } + + // check for offers with spent key images + for (Offer offer : offers) { + if (unreservedOffers.contains(offer)) continue; + for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { + if (spentKeyImages.contains(keyImage)) unreservedOffers.add(offer); + } + } + + return unreservedOffers; } OpenOffer getMyOpenOffer(String id) { diff --git a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java index 4d0e8ea198..9843f9989d 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java @@ -140,7 +140,7 @@ public class WalletConfig extends AbstractIdleService { protected volatile BlockChain vChain; protected volatile SPVBlockStore vStore; protected volatile MoneroDaemon vXmrDaemon; - protected volatile MoneroWallet vXmrWallet; + protected volatile MoneroWalletRpc vXmrWallet; protected volatile Wallet vBtcWallet; protected volatile Wallet vBsqWallet; protected volatile PeerGroup vPeerGroup; @@ -287,7 +287,7 @@ public class WalletConfig extends AbstractIdleService { // Meant to be overridden by subclasses } - public MoneroWallet createWallet(MoneroWalletConfig config) { + public MoneroWalletRpc createWallet(MoneroWalletConfig config) { // start monero-wallet-rpc instance MoneroWalletRpc walletRpc = startWalletRpcInstance(); @@ -304,7 +304,7 @@ public class WalletConfig extends AbstractIdleService { } } - public MoneroWallet openWallet(MoneroWalletConfig config) { + public MoneroWalletRpc openWallet(MoneroWalletConfig config) { // start monero-wallet-rpc instance MoneroWalletRpc walletRpc = startWalletRpcInstance(); @@ -362,7 +362,7 @@ public class WalletConfig extends AbstractIdleService { } System.out.println("Monero wallet path: " + vXmrWallet.getPath()); System.out.println("Monero wallet address: " + vXmrWallet.getPrimaryAddress()); - System.out.println("Monero mnemonic: " + vXmrWallet.getMnemonic()); + System.out.println("Monero wallet uri: " + vXmrWallet.getRpcConnection().getUri()); // vXmrWallet.rescanSpent(); // vXmrWallet.rescanBlockchain(); vXmrWallet.sync(); diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 9d4c6de82e..f30412a037 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -232,6 +232,7 @@ public class CreateOfferService { extraDataMap, Version.TRADE_PROTOCOL_VERSION, arbitrator.getNodeAddress(), + null, null); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index 6be94c7979..b44b8b0f2b 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -171,9 +171,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // address and signature of signing arbitrator @Setter private NodeAddress arbitratorNodeAddress; - @Nullable @Setter + @Nullable private String arbitratorSignature; + @Setter + @Nullable + private List reserveTxKeyImages; /////////////////////////////////////////////////////////////////////////////////////////// @@ -217,7 +220,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay @Nullable Map extraDataMap, int protocolVersion, NodeAddress arbitratorSigner, - @Nullable String arbitratorSignature) { + @Nullable String arbitratorSignature, + @Nullable List reserveTxKeyImages) { this.id = id; this.date = date; this.ownerNodeAddress = ownerNodeAddress; @@ -256,6 +260,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.protocolVersion = protocolVersion; this.arbitratorNodeAddress = arbitratorSigner; this.arbitratorSignature = arbitratorSignature; + this.reserveTxKeyImages = reserveTxKeyImages; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -293,7 +298,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay .setLowerClosePrice(lowerClosePrice) .setUpperClosePrice(upperClosePrice) .setIsPrivateOffer(isPrivateOffer) - .setProtocolVersion(protocolVersion); + .setProtocolVersion(protocolVersion) + .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()); builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId, "OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network.")); @@ -304,9 +310,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); - - builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()); Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature); + Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); } @@ -358,7 +363,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay extraDataMapMap, proto.getProtocolVersion(), NodeAddress.fromProto(proto.getArbitratorNodeAddress()), - ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature())); + ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()), + proto.getReserveTxKeyImagesList() == null ? null : new ArrayList(proto.getReserveTxKeyImagesList())); } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index d0d88697b5..af1ed84d13 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -282,7 +282,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffers(getObservableList(), completeHandler); } - private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { + public void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { int size = openOffers.size(); // Copy list as we remove in the loop List openOffersList = new ArrayList<>(openOffers); @@ -670,8 +670,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - // verify reserve tx not signed before - // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) Offer offer = new Offer(request.getOfferPayload()); BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); @@ -685,6 +683,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKey(), + request.getReserveTxKeyImages(), true); // arbitrator signs offer to certify they have valid reserve tx @@ -1034,7 +1033,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe updatedExtraDataMap, protocolVersion, originalOfferPayload.getArbitratorNodeAddress(), - originalOfferPayload.getArbitratorSignature()); + originalOfferPayload.getArbitratorSignature(), + originalOfferPayload.getReserveTxKeyImages()); // Save states from original data to use for the updated Offer.State originalOfferState = originalOffer.getState(); diff --git a/core/src/main/java/bisq/core/offer/messages/SignOfferRequest.java b/core/src/main/java/bisq/core/offer/messages/SignOfferRequest.java index 2fff876f55..80733ebfbd 100644 --- a/core/src/main/java/bisq/core/offer/messages/SignOfferRequest.java +++ b/core/src/main/java/bisq/core/offer/messages/SignOfferRequest.java @@ -21,6 +21,8 @@ import bisq.common.crypto.PubKeyRing; import bisq.core.offer.OfferPayload; import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; +import java.util.ArrayList; +import java.util.List; import lombok.EqualsAndHashCode; import lombok.Value; @@ -35,6 +37,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag private final String reserveTxHash; private final String reserveTxHex; private final String reserveTxKey; + private final List reserveTxKeyImages; private final String payoutAddress; public SignOfferRequest(String offerId, @@ -48,6 +51,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag String reserveTxHash, String reserveTxHex, String reserveTxKey, + List reserveTxKeyImages, String payoutAddress) { super(messageVersion, offerId, uid); this.senderNodeAddress = senderNodeAddress; @@ -58,6 +62,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; + this.reserveTxKeyImages = reserveTxKeyImages; this.payoutAddress = payoutAddress; } @@ -79,6 +84,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag .setReserveTxHash(reserveTxHash) .setReserveTxHex(reserveTxHex) .setReserveTxKey(reserveTxKey) + .addAllReserveTxKeyImages(reserveTxKeyImages) .setPayoutAddress(payoutAddress); return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build(); @@ -97,6 +103,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag proto.getReserveTxHash(), proto.getReserveTxHex(), proto.getReserveTxKey(), + new ArrayList(proto.getReserveTxKeyImagesList()), proto.getPayoutAddress()); } @@ -109,6 +116,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag ",\n reserveTxHash='" + reserveTxHash + ",\n reserveTxHex='" + reserveTxHex + ",\n reserveTxKey='" + reserveTxKey + + ",\n reserveTxKeyImages='" + reserveTxKeyImages + ",\n payoutAddress='" + payoutAddress + "\n} " + super.toString(); } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java index 140775a798..b16e322c60 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java @@ -53,16 +53,17 @@ public class MakerReservesTradeFunds extends Task { // freeze reserved outputs // TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs - List frozenKeyImages = new ArrayList(); + List reservedKeyImages = new ArrayList(); MoneroWallet wallet = model.getXmrWalletService().getWallet(); for (MoneroOutput input : reserveTx.getInputs()) { - frozenKeyImages.add(input.getKeyImage().getHex()); + reservedKeyImages.add(input.getKeyImage().getHex()); wallet.freezeOutput(input.getKeyImage().getHex()); } // save offer state // TODO (woodser): persist model.setReserveTx(reserveTx); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id offer.setState(Offer.State.OFFER_FEE_RESERVED); complete(); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java index 29bde2629d..474c163a32 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerSendsSignOfferRequest.java @@ -17,6 +17,8 @@ package bisq.core.offer.placeoffer.tasks; +import static com.google.common.base.Preconditions.checkNotNull; + import bisq.common.app.Version; import bisq.common.taskrunner.Task; import bisq.common.taskrunner.TaskRunner; @@ -32,8 +34,6 @@ import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.google.common.base.Preconditions.checkNotNull; - public class MakerSendsSignOfferRequest extends Task { private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class); @@ -64,6 +64,7 @@ public class MakerSendsSignOfferRequest extends Task { model.getReserveTx().getHash(), model.getReserveTx().getFullHex(), model.getReserveTx().getKey(), + offer.getOfferPayload().getReserveTxKeyImages(), returnAddress); // get signing arbitrator diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java index 446a2f8d14..01b68500ea 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -17,14 +17,6 @@ package bisq.core.trade; -import bisq.core.btc.model.XmrAddressEntry; -import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferPayload.Direction; -import bisq.core.support.dispute.mediation.mediator.Mediator; -import bisq.core.trade.messages.InitTradeRequest; -import common.utils.JsonUtils; - import static com.google.common.base.Preconditions.checkNotNull; import bisq.common.crypto.KeyRing; @@ -32,9 +24,20 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.Sig; import bisq.common.util.Tuple2; import bisq.common.util.Utilities; +import bisq.core.btc.model.XmrAddressEntry; +import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferPayload.Direction; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.trade.messages.InitTradeRequest; +import common.utils.JsonUtils; import java.math.BigInteger; +import java.util.HashSet; +import java.util.List; import java.util.Objects; +import java.util.Set; import monero.daemon.MoneroDaemon; +import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; @@ -182,7 +185,7 @@ public class TradeUtils { /** * Process a reserve or deposit transaction used during trading. * Checks double spends, deposit amount and destination, trade fee, and mining fee. - * The transaction is submitted but not relayed to the pool. + * The transaction is submitted but not relayed to the pool then flushed. * * @param daemon is the Monero daemon to check for double spends * @param wallet is the Monero wallet to verify the tx @@ -192,41 +195,58 @@ public class TradeUtils { * @param txHash is the transaction hash * @param txHex is the transaction hex * @param txKey is the transaction key - * @param isReserveTx indicates if the tx is a reserve tx, which requires fee padding + * @param keyImages are expected key images of inputs, ignored if null + * @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase */ - public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, boolean isReserveTx) { - - // get tx from daemon - MoneroTx tx = daemon.getTx(txHash); - - // if tx is not submitted, submit but do not relay - if (tx == null) { - MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? - if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); - } else if (tx.isRelayed()) { - throw new RuntimeException("Reserve tx must not be relayed"); - } - - // verify trade fee - String feeAddress = TradeUtils.FEE_ADDRESS; - MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); - if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); - if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); + public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List keyImages, boolean miningFeePadding) { + boolean submittedToPool = false; + try { + + // get tx from daemon + MoneroTx tx = daemon.getTx(txHash); + + // if tx is not submitted, submit but do not relay + if (tx == null) { + MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? + if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); + submittedToPool = true; + tx = daemon.getTx(txHash); + } else if (tx.isRelayed()) { + throw new RuntimeException("Trade tx must not be relayed"); + } + + // verify reserved key images + if (keyImages != null) { + Set txKeyImages = new HashSet(); + for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); + if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images"); + } + + // verify trade fee + String feeAddress = TradeUtils.FEE_ADDRESS; + MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); + if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); + if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); - // verify mining fee - BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate - BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee - tx = daemon.getTx(txHash); - if (tx.getFee().compareTo(feeThreshold) < 0) { - throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee()); - } + // verify mining fee + BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate + BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee + tx = daemon.getTx(txHash); + if (tx.getFee().compareTo(feeThreshold) < 0) { + throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee()); + } - // verify deposit amount - check = wallet.checkTxKey(txHash, txKey, depositAddress); - if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); - BigInteger depositThreshold = depositAmount; - if (isReserveTx) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee) - if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount()); + // verify deposit amount + check = wallet.checkTxKey(txHash, txKey, depositAddress); + if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); + BigInteger depositThreshold = depositAmount; + if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee) + if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount()); + } finally { + + // flush tx from pool if we added it + if (submittedToPool) daemon.flushTxPool(txHash); + } } /** diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java index d545fda21d..524450b75f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java @@ -64,6 +64,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask { request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKey(), + null, true); // save reserve tx to model diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java index 082a65a981..1973db16a6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositRequest.java @@ -90,7 +90,7 @@ public class ProcessDepositRequest extends TradeTask { MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); daemon.flushTxPool(trader.getReserveTxHash()); - // process and verify deposit tx which submits to the pool + // process and verify deposit tx TradeUtils.processTradeTx( daemon, trade.getXmrWalletService().getWallet(), @@ -100,6 +100,7 @@ public class ProcessDepositRequest extends TradeTask { trader.getDepositTxHash(), request.getDepositTxHex(), request.getDepositTxKey(), + null, false); // sychronize to send only one response @@ -114,8 +115,8 @@ public class ProcessDepositRequest extends TradeTask { if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) { // relay txs - daemon.relayTxByHash(processModel.getMaker().getDepositTxHash()); - daemon.relayTxByHash(processModel.getTaker().getDepositTxHash()); + daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); + daemon.submitTxHex(processModel.getTaker().getDepositTxHex()); // create deposit response DepositResponse response = new DepositResponse( diff --git a/core/src/test/java/bisq/core/offer/OfferMaker.java b/core/src/test/java/bisq/core/offer/OfferMaker.java index 8f50b22be0..5678cdbade 100644 --- a/core/src/test/java/bisq/core/offer/OfferMaker.java +++ b/core/src/test/java/bisq/core/offer/OfferMaker.java @@ -73,6 +73,7 @@ public class OfferMaker { null, 0, null, + null, null)); public static final Maker btcUsdOffer = a(Offer); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 8cfc74f746..069a837b00 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -175,9 +175,9 @@ class GrpcTradesService extends TradesImplBase { .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); - put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); }} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index 6bba28b317..02dd0e20ba 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -224,7 +224,8 @@ class EditOfferDataModel extends MutableOfferDataModel { offerPayload.getExtraDataMap(), offerPayload.getProtocolVersion(), offerPayload.getArbitratorNodeAddress(), - offerPayload.getArbitratorSignature()); + offerPayload.getArbitratorSignature(), + offerPayload.getReserveTxKeyImages()); final Offer editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index a5dd9e7503..3700352c41 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -97,6 +97,7 @@ public class TradesChartsViewModelTest { null, 1, null, + null, null ); diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index f337546a36..654980881e 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -621,6 +621,7 @@ public class OfferBookViewModelTest { null, 1, null, + null, null)); } } diff --git a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java index 4a37c049a1..0752d94e37 100644 --- a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java @@ -77,6 +77,7 @@ public class OfferMaker { null, 0, null, + null, null)); public static final Maker btcUsdOffer = a(Offer); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index be245fcfba..2ae1fa4632 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -177,7 +177,8 @@ message SignOfferRequest { string reserve_tx_hash = 8; string reserve_tx_hex = 9; string reserve_tx_key = 10; - string payout_address = 11; + repeated string reserve_tx_key_images = 11; + string payout_address = 12; } message SignOfferResponse { @@ -940,6 +941,7 @@ message OfferPayload { NodeAddress arbitrator_node_address = 1001; string arbitrator_signature = 1002; + repeated string reserve_tx_key_images = 1003; } message AccountAgeWitness {