filter offers with spent or duplicate funds using key images

reserve tx does not remain in arbitrator pool
This commit is contained in:
woodser 2021-09-17 16:29:54 -04:00
parent b9228585c7
commit 6798630dfc
18 changed files with 166 additions and 71 deletions

View File

@ -17,6 +17,7 @@
package bisq.core.api; package bisq.core.api;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService; import bisq.core.offer.CreateOfferService;
@ -39,14 +40,17 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import static bisq.common.util.MathUtils.exactMultiply; import static bisq.common.util.MathUtils.exactMultiply;
import static bisq.common.util.MathUtils.roundDoubleToLong; import static bisq.common.util.MathUtils.roundDoubleToLong;
@ -77,6 +81,7 @@ class CoreOffersService {
private final OpenOfferManager openOfferManager; private final OpenOfferManager openOfferManager;
private final OfferUtil offerUtil; private final OfferUtil offerUtil;
private final User user; private final User user;
private final XmrWalletService xmrWalletService;
@Inject @Inject
public CoreOffersService(CoreContext coreContext, public CoreOffersService(CoreContext coreContext,
@ -87,7 +92,8 @@ class CoreOffersService {
OfferFilter offerFilter, OfferFilter offerFilter,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
OfferUtil offerUtil, OfferUtil offerUtil,
User user) { User user,
XmrWalletService xmrWalletService) {
this.coreContext = coreContext; this.coreContext = coreContext;
this.keyRing = keyRing; this.keyRing = keyRing;
this.coreWalletsService = coreWalletsService; this.coreWalletsService = coreWalletsService;
@ -97,6 +103,7 @@ class CoreOffersService {
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.user = user; this.user = user;
this.xmrWalletService = xmrWalletService;
} }
Offer getOffer(String id) { Offer getOffer(String id) {
@ -117,20 +124,62 @@ class CoreOffersService {
} }
List<Offer> getOffers(String direction, String currencyCode) { List<Offer> getOffers(String direction, String currencyCode) {
return offerBookService.getOffers().stream() List<Offer> offers = offerBookService.getOffers().stream()
.filter(o -> !o.isMyOffer(keyRing)) .filter(o -> !o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid()) .filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
.sorted(priceComparator(direction)) .sorted(priceComparator(direction))
.collect(Collectors.toList()); .collect(Collectors.toList());
offers.removeAll(getUnreservedOffers(offers));
return offers;
} }
List<Offer> getMyOffers(String direction, String currencyCode) { List<Offer> getMyOffers(String direction, String currencyCode) {
return offerBookService.getOffers().stream() List<Offer> offers = offerBookService.getOffers().stream()
.filter(o -> o.isMyOffer(keyRing)) .filter(o -> o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.sorted(priceComparator(direction)) .sorted(priceComparator(direction))
.collect(Collectors.toList()); .collect(Collectors.toList());
Set<Offer> unreservedOffers = getUnreservedOffers(offers);
offers.removeAll(unreservedOffers);
// remove my unreserved offers from offer manager
List<OpenOffer> unreservedOpenOffers = new ArrayList<OpenOffer>();
for (Offer unreservedOffer : unreservedOffers) {
unreservedOpenOffers.add(openOfferManager.getOpenOfferById(unreservedOffer.getId()).get());
}
openOfferManager.removeOpenOffers(unreservedOpenOffers, null);
return offers;
}
private Set<Offer> getUnreservedOffers(List<Offer> offers) {
Set<Offer> unreservedOffers = new HashSet<Offer>();
// collect reserved key images and check for duplicate funds
List<String> allKeyImages = new ArrayList<String>();
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<String> spentKeyImages = new ArrayList<String>();
List<MoneroKeyImageSpentStatus> spentStatuses = allKeyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : 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) { OpenOffer getMyOpenOffer(String id) {

View File

@ -140,7 +140,7 @@ public class WalletConfig extends AbstractIdleService {
protected volatile BlockChain vChain; protected volatile BlockChain vChain;
protected volatile SPVBlockStore vStore; protected volatile SPVBlockStore vStore;
protected volatile MoneroDaemon vXmrDaemon; protected volatile MoneroDaemon vXmrDaemon;
protected volatile MoneroWallet vXmrWallet; protected volatile MoneroWalletRpc vXmrWallet;
protected volatile Wallet vBtcWallet; protected volatile Wallet vBtcWallet;
protected volatile Wallet vBsqWallet; protected volatile Wallet vBsqWallet;
protected volatile PeerGroup vPeerGroup; protected volatile PeerGroup vPeerGroup;
@ -287,7 +287,7 @@ public class WalletConfig extends AbstractIdleService {
// Meant to be overridden by subclasses // Meant to be overridden by subclasses
} }
public MoneroWallet createWallet(MoneroWalletConfig config) { public MoneroWalletRpc createWallet(MoneroWalletConfig config) {
// start monero-wallet-rpc instance // start monero-wallet-rpc instance
MoneroWalletRpc walletRpc = startWalletRpcInstance(); 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 // start monero-wallet-rpc instance
MoneroWalletRpc walletRpc = startWalletRpcInstance(); 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 path: " + vXmrWallet.getPath());
System.out.println("Monero wallet address: " + vXmrWallet.getPrimaryAddress()); 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.rescanSpent();
// vXmrWallet.rescanBlockchain(); // vXmrWallet.rescanBlockchain();
vXmrWallet.sync(); vXmrWallet.sync();

View File

@ -232,6 +232,7 @@ public class CreateOfferService {
extraDataMap, extraDataMap,
Version.TRADE_PROTOCOL_VERSION, Version.TRADE_PROTOCOL_VERSION,
arbitrator.getNodeAddress(), arbitrator.getNodeAddress(),
null,
null); null);
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);

View File

@ -171,9 +171,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// address and signature of signing arbitrator // address and signature of signing arbitrator
@Setter @Setter
private NodeAddress arbitratorNodeAddress; private NodeAddress arbitratorNodeAddress;
@Nullable
@Setter @Setter
@Nullable
private String arbitratorSignature; private String arbitratorSignature;
@Setter
@Nullable
private List<String> reserveTxKeyImages;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -217,7 +220,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
@Nullable Map<String, String> extraDataMap, @Nullable Map<String, String> extraDataMap,
int protocolVersion, int protocolVersion,
NodeAddress arbitratorSigner, NodeAddress arbitratorSigner,
@Nullable String arbitratorSignature) { @Nullable String arbitratorSignature,
@Nullable List<String> reserveTxKeyImages) {
this.id = id; this.id = id;
this.date = date; this.date = date;
this.ownerNodeAddress = ownerNodeAddress; this.ownerNodeAddress = ownerNodeAddress;
@ -256,6 +260,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.protocolVersion = protocolVersion; this.protocolVersion = protocolVersion;
this.arbitratorNodeAddress = arbitratorSigner; this.arbitratorNodeAddress = arbitratorSigner;
this.arbitratorSignature = arbitratorSignature; this.arbitratorSignature = arbitratorSignature;
this.reserveTxKeyImages = reserveTxKeyImages;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -293,7 +298,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
.setLowerClosePrice(lowerClosePrice) .setLowerClosePrice(lowerClosePrice)
.setUpperClosePrice(upperClosePrice) .setUpperClosePrice(upperClosePrice)
.setIsPrivateOffer(isPrivateOffer) .setIsPrivateOffer(isPrivateOffer)
.setProtocolVersion(protocolVersion); .setProtocolVersion(protocolVersion)
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId, builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
"OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network.")); "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(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature); Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature);
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
} }
@ -358,7 +363,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
extraDataMapMap, extraDataMapMap,
proto.getProtocolVersion(), proto.getProtocolVersion(),
NodeAddress.fromProto(proto.getArbitratorNodeAddress()), NodeAddress.fromProto(proto.getArbitratorNodeAddress()),
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature())); ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()),
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
} }

View File

@ -282,7 +282,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
removeOpenOffers(getObservableList(), completeHandler); removeOpenOffers(getObservableList(), completeHandler);
} }
private void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) { public void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
int size = openOffers.size(); int size = openOffers.size();
// Copy list as we remove in the loop // Copy list as we remove in the loop
List<OpenOffer> openOffersList = new ArrayList<>(openOffers); List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
@ -670,8 +670,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return; return;
} }
// verify reserve tx not signed before
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
Offer offer = new Offer(request.getOfferPayload()); Offer offer = new Offer(request.getOfferPayload());
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
@ -685,6 +683,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
request.getReserveTxHash(), request.getReserveTxHash(),
request.getReserveTxHex(), request.getReserveTxHex(),
request.getReserveTxKey(), request.getReserveTxKey(),
request.getReserveTxKeyImages(),
true); true);
// arbitrator signs offer to certify they have valid reserve tx // arbitrator signs offer to certify they have valid reserve tx
@ -1034,7 +1033,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
updatedExtraDataMap, updatedExtraDataMap,
protocolVersion, protocolVersion,
originalOfferPayload.getArbitratorNodeAddress(), originalOfferPayload.getArbitratorNodeAddress(),
originalOfferPayload.getArbitratorSignature()); originalOfferPayload.getArbitratorSignature(),
originalOfferPayload.getReserveTxKeyImages());
// Save states from original data to use for the updated // Save states from original data to use for the updated
Offer.State originalOfferState = originalOffer.getState(); Offer.State originalOfferState = originalOffer.getState();

View File

@ -21,6 +21,8 @@ import bisq.common.crypto.PubKeyRing;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.network.p2p.DirectMessage; import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import java.util.ArrayList;
import java.util.List;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
@ -35,6 +37,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
private final String reserveTxHash; private final String reserveTxHash;
private final String reserveTxHex; private final String reserveTxHex;
private final String reserveTxKey; private final String reserveTxKey;
private final List<String> reserveTxKeyImages;
private final String payoutAddress; private final String payoutAddress;
public SignOfferRequest(String offerId, public SignOfferRequest(String offerId,
@ -48,6 +51,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
String reserveTxHash, String reserveTxHash,
String reserveTxHex, String reserveTxHex,
String reserveTxKey, String reserveTxKey,
List<String> reserveTxKeyImages,
String payoutAddress) { String payoutAddress) {
super(messageVersion, offerId, uid); super(messageVersion, offerId, uid);
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
@ -58,6 +62,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
this.reserveTxHash = reserveTxHash; this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; this.reserveTxKey = reserveTxKey;
this.reserveTxKeyImages = reserveTxKeyImages;
this.payoutAddress = payoutAddress; this.payoutAddress = payoutAddress;
} }
@ -79,6 +84,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
.setReserveTxHash(reserveTxHash) .setReserveTxHash(reserveTxHash)
.setReserveTxHex(reserveTxHex) .setReserveTxHex(reserveTxHex)
.setReserveTxKey(reserveTxKey) .setReserveTxKey(reserveTxKey)
.addAllReserveTxKeyImages(reserveTxKeyImages)
.setPayoutAddress(payoutAddress); .setPayoutAddress(payoutAddress);
return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build(); return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build();
@ -97,6 +103,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
proto.getReserveTxHash(), proto.getReserveTxHash(),
proto.getReserveTxHex(), proto.getReserveTxHex(),
proto.getReserveTxKey(), proto.getReserveTxKey(),
new ArrayList<String>(proto.getReserveTxKeyImagesList()),
proto.getPayoutAddress()); proto.getPayoutAddress());
} }
@ -109,6 +116,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
",\n reserveTxHash='" + reserveTxHash + ",\n reserveTxHash='" + reserveTxHash +
",\n reserveTxHex='" + reserveTxHex + ",\n reserveTxHex='" + reserveTxHex +
",\n reserveTxKey='" + reserveTxKey + ",\n reserveTxKey='" + reserveTxKey +
",\n reserveTxKeyImages='" + reserveTxKeyImages +
",\n payoutAddress='" + payoutAddress + ",\n payoutAddress='" + payoutAddress +
"\n} " + super.toString(); "\n} " + super.toString();
} }

View File

@ -53,16 +53,17 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
// freeze reserved outputs // freeze reserved outputs
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs // TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
List<String> frozenKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
MoneroWallet wallet = model.getXmrWalletService().getWallet(); MoneroWallet wallet = model.getXmrWalletService().getWallet();
for (MoneroOutput input : reserveTx.getInputs()) { for (MoneroOutput input : reserveTx.getInputs()) {
frozenKeyImages.add(input.getKeyImage().getHex()); reservedKeyImages.add(input.getKeyImage().getHex());
wallet.freezeOutput(input.getKeyImage().getHex()); wallet.freezeOutput(input.getKeyImage().getHex());
} }
// save offer state // save offer state
// TODO (woodser): persist // TODO (woodser): persist
model.setReserveTx(reserveTx); model.setReserveTx(reserveTx);
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id
offer.setState(Offer.State.OFFER_FEE_RESERVED); offer.setState(Offer.State.OFFER_FEE_RESERVED);
complete(); complete();

View File

@ -17,6 +17,8 @@
package bisq.core.offer.placeoffer.tasks; package bisq.core.offer.placeoffer.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.taskrunner.Task; import bisq.common.taskrunner.Task;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
@ -32,8 +34,6 @@ import java.util.UUID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> { public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class); private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class);
@ -64,6 +64,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
model.getReserveTx().getHash(), model.getReserveTx().getHash(),
model.getReserveTx().getFullHex(), model.getReserveTx().getFullHex(),
model.getReserveTx().getKey(), model.getReserveTx().getKey(),
offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress); returnAddress);
// get signing arbitrator // get signing arbitrator

View File

@ -17,14 +17,6 @@
package bisq.core.trade; 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 static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
@ -32,9 +24,20 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig; import bisq.common.crypto.Sig;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import bisq.common.util.Utilities; 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.math.BigInteger;
import java.util.HashSet;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroOutput;
import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
@ -182,7 +185,7 @@ public class TradeUtils {
/** /**
* Process a reserve or deposit transaction used during trading. * Process a reserve or deposit transaction used during trading.
* Checks double spends, deposit amount and destination, trade fee, and mining fee. * 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 daemon is the Monero daemon to check for double spends
* @param wallet is the Monero wallet to verify the tx * @param wallet is the Monero wallet to verify the tx
@ -192,41 +195,58 @@ public class TradeUtils {
* @param txHash is the transaction hash * @param txHash is the transaction hash
* @param txHex is the transaction hex * @param txHex is the transaction hex
* @param txKey is the transaction key * @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) { public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean miningFeePadding) {
boolean submittedToPool = false;
try {
// get tx from daemon // get tx from daemon
MoneroTx tx = daemon.getTx(txHash); MoneroTx tx = daemon.getTx(txHash);
// if tx is not submitted, submit but do not relay // if tx is not submitted, submit but do not relay
if (tx == null) { if (tx == null) {
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? 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)); if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
} else if (tx.isRelayed()) { submittedToPool = true;
throw new RuntimeException("Reserve tx must not be relayed"); 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<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(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 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);
} }
// 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 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());
} }
/** /**

View File

@ -64,6 +64,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask {
request.getReserveTxHash(), request.getReserveTxHash(),
request.getReserveTxHex(), request.getReserveTxHex(),
request.getReserveTxKey(), request.getReserveTxKey(),
null,
true); true);
// save reserve tx to model // save reserve tx to model

View File

@ -90,7 +90,7 @@ public class ProcessDepositRequest extends TradeTask {
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
daemon.flushTxPool(trader.getReserveTxHash()); daemon.flushTxPool(trader.getReserveTxHash());
// process and verify deposit tx which submits to the pool // process and verify deposit tx
TradeUtils.processTradeTx( TradeUtils.processTradeTx(
daemon, daemon,
trade.getXmrWalletService().getWallet(), trade.getXmrWalletService().getWallet(),
@ -100,6 +100,7 @@ public class ProcessDepositRequest extends TradeTask {
trader.getDepositTxHash(), trader.getDepositTxHash(),
request.getDepositTxHex(), request.getDepositTxHex(),
request.getDepositTxKey(), request.getDepositTxKey(),
null,
false); false);
// sychronize to send only one response // sychronize to send only one response
@ -114,8 +115,8 @@ public class ProcessDepositRequest extends TradeTask {
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) { if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
// relay txs // relay txs
daemon.relayTxByHash(processModel.getMaker().getDepositTxHash()); daemon.submitTxHex(processModel.getMaker().getDepositTxHex());
daemon.relayTxByHash(processModel.getTaker().getDepositTxHash()); daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
// create deposit response // create deposit response
DepositResponse response = new DepositResponse( DepositResponse response = new DepositResponse(

View File

@ -73,6 +73,7 @@ public class OfferMaker {
null, null,
0, 0,
null, null,
null,
null)); null));
public static final Maker<Offer> btcUsdOffer = a(Offer); public static final Maker<Offer> btcUsdOffer = a(Offer);

View File

@ -175,9 +175,9 @@ class GrpcTradesService extends TradesImplBase {
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
}} }}

View File

@ -224,7 +224,8 @@ class EditOfferDataModel extends MutableOfferDataModel {
offerPayload.getExtraDataMap(), offerPayload.getExtraDataMap(),
offerPayload.getProtocolVersion(), offerPayload.getProtocolVersion(),
offerPayload.getArbitratorNodeAddress(), offerPayload.getArbitratorNodeAddress(),
offerPayload.getArbitratorSignature()); offerPayload.getArbitratorSignature(),
offerPayload.getReserveTxKeyImages());
final Offer editedOffer = new Offer(editedPayload); final Offer editedOffer = new Offer(editedPayload);
editedOffer.setPriceFeedService(priceFeedService); editedOffer.setPriceFeedService(priceFeedService);

View File

@ -97,6 +97,7 @@ public class TradesChartsViewModelTest {
null, null,
1, 1,
null, null,
null,
null null
); );

View File

@ -621,6 +621,7 @@ public class OfferBookViewModelTest {
null, null,
1, 1,
null, null,
null,
null)); null));
} }
} }

View File

@ -77,6 +77,7 @@ public class OfferMaker {
null, null,
0, 0,
null, null,
null,
null)); null));
public static final Maker<Offer> btcUsdOffer = a(Offer); public static final Maker<Offer> btcUsdOffer = a(Offer);

View File

@ -177,7 +177,8 @@ message SignOfferRequest {
string reserve_tx_hash = 8; string reserve_tx_hash = 8;
string reserve_tx_hex = 9; string reserve_tx_hex = 9;
string reserve_tx_key = 10; string reserve_tx_key = 10;
string payout_address = 11; repeated string reserve_tx_key_images = 11;
string payout_address = 12;
} }
message SignOfferResponse { message SignOfferResponse {
@ -940,6 +941,7 @@ message OfferPayload {
NodeAddress arbitrator_node_address = 1001; NodeAddress arbitrator_node_address = 1001;
string arbitrator_signature = 1002; string arbitrator_signature = 1002;
repeated string reserve_tx_key_images = 1003;
} }
message AccountAgeWitness { message AccountAgeWitness {