Skip to content

Commit

Permalink
Merge pull request #4157 from sqrrm/unfail-with-reattach
Browse files Browse the repository at this point in the history
Unfail with reattach
  • Loading branch information
ripcurlx committed Apr 13, 2020
2 parents edc4df1 + 2bd00c8 commit 2a040f4
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 31 deletions.
10 changes: 10 additions & 0 deletions common/src/main/java/bisq/common/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public class Config {
public static final String GENESIS_TOTAL_SUPPLY = "genesisTotalSupply";
public static final String DAO_ACTIVATED = "daoActivated";
public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs";
public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs";

// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
Expand Down Expand Up @@ -197,6 +198,7 @@ public class Config {
public final int genesisBlockHeight;
public final long genesisTotalSupply;
public final boolean dumpDelayedPayoutTxs;
public final boolean allowFaultyDelayedTxs;

// Properties derived from options but not exposed as options themselves
public final File torDir;
Expand Down Expand Up @@ -606,6 +608,13 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
.ofType(boolean.class)
.defaultsTo(false);

ArgumentAcceptingOptionSpec<Boolean> allowFaultyDelayedTxsOpt =
parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " +
"payout transactions")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);

try {
CompositeOptionSet options = new CompositeOptionSet();

Expand Down Expand Up @@ -717,6 +726,7 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
this.genesisTotalSupply = options.valueOf(genesisTotalSupplyOpt);
this.daoActivated = options.valueOf(daoActivatedOpt);
this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt);
this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,13 @@ public AddressEntry getNewAddressEntry(String offerId, AddressEntry.Context cont
return entry;
}

public AddressEntry recoverAddressEntry(String offerId, String address, AddressEntry.Context context) {
var available = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
if (!available.isPresent())
return null;
return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
}

private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional<AddressEntry> addressEntry) {
if (addressEntry.isPresent()) {
return addressEntry.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public static class InvalidTxException extends Exception {
}
}

public static class AmountMismatchException extends Exception {
AmountMismatchException(String msg) {
super(msg);
}
}

public static class InvalidLockTimeException extends Exception {
InvalidLockTimeException(String msg) {
super(msg);
Expand All @@ -69,7 +75,7 @@ public static void validatePayoutTx(Trade trade,
DaoFacade daoFacade,
BtcWalletService btcWalletService)
throws DonationAddressException, MissingDelayedPayoutTxException,
InvalidTxException, InvalidLockTimeException {
InvalidTxException, InvalidLockTimeException, AmountMismatchException {
String errorMsg;
if (delayedPayoutTx == null) {
errorMsg = "DelayedPayoutTx must not be null";
Expand Down Expand Up @@ -122,7 +128,7 @@ public static void validatePayoutTx(Trade trade,
errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
throw new AmountMismatchException(errorMsg);
}


Expand Down
87 changes: 68 additions & 19 deletions core/src/main/java/bisq/core/trade/TradeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,16 @@
import bisq.network.p2p.SendMailboxMessageListener;

import bisq.common.ClockWatcher;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.storage.Storage;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;

import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
Expand All @@ -72,6 +75,7 @@
import org.bitcoinj.core.TransactionConfidence;

import javax.inject.Inject;
import javax.inject.Named;

import com.google.common.util.concurrent.FutureCallback;

Expand All @@ -89,6 +93,7 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
Expand Down Expand Up @@ -142,6 +147,8 @@ public class TradeManager implements PersistedDataHost {
@Getter
private final ObservableList<Trade> tradesWithoutDepositTx = FXCollections.observableArrayList();
private final DumpDelayedPayoutTx dumpDelayedPayoutTx;
@Getter
private final boolean allowFaultyDelayedTxs;


///////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -169,7 +176,8 @@ public TradeManager(User user,
DaoFacade daoFacade,
ClockWatcher clockWatcher,
Storage<TradableList<Trade>> storage,
DumpDelayedPayoutTx dumpDelayedPayoutTx) {
DumpDelayedPayoutTx dumpDelayedPayoutTx,
@Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) {
this.user = user;
this.keyRing = keyRing;
this.btcWalletService = btcWalletService;
Expand All @@ -190,6 +198,7 @@ public TradeManager(User user,
this.daoFacade = daoFacade;
this.clockWatcher = clockWatcher;
this.dumpDelayedPayoutTx = dumpDelayedPayoutTx;
this.allowFaultyDelayedTxs = allowFaultyDelayedTxs;

tradableListStorage = storage;

Expand Down Expand Up @@ -225,6 +234,7 @@ public TradeManager(User user,
}
}
});
failedTradesManager.setUnfailTradeCallback(this::unfailTrade);
}

@Override
Expand Down Expand Up @@ -279,10 +289,7 @@ private void initPendingTrades() {
tradableList.forEach(trade -> {
if (trade.isDepositPublished() ||
(trade.isTakerFeePublished() && !trade.hasFailed())) {
initTrade(trade, trade.getProcessModel().isUseSavingsWallet(),
trade.getProcessModel().getFundsNeededForTradeAsLong());
trade.updateDepositTxFromWallet();
tradesForStatistics.add(trade);
initPendingTrade(trade);
} else if (trade.isTakerFeePublished() && !trade.isFundsLockedIn()) {
addTradeToFailedTradesList.add(trade);
trade.appendErrorMessage("Invalid state: trade.isTakerFeePublished() && !trade.isFundsLockedIn()");
Expand All @@ -298,20 +305,23 @@ private void initPendingTrades() {
tradesWithoutDepositTx.add(trade);
}

try {
DelayedPayoutTxValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
daoFacade,
btcWalletService);
} catch (DelayedPayoutTxValidation.DonationAddressException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) {
// We move it to failed trades so it cannot be continued.
log.warn("We move the trade with ID '{}' to failed trades because of exception {}",
trade.getId(), e.getMessage());
addTradeToFailedTradesList.add(trade);
}
try {
DelayedPayoutTxValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
daoFacade,
btcWalletService);
} catch (DelayedPayoutTxValidation.DonationAddressException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
DelayedPayoutTxValidation.AmountMismatchException e) {
log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage());
if (!allowFaultyDelayedTxs) {
// We move it to failed trades so it cannot be continued.
log.warn("We move the trade with ID '{}' to failed trades", trade.getId());
addTradeToFailedTradesList.add(trade);
}
}
}
);

Expand All @@ -336,6 +346,13 @@ private void initPendingTrades() {
pendingTradesInitialized.set(true);
}

private void initPendingTrade(Trade trade) {
initTrade(trade, trade.getProcessModel().isUseSavingsWallet(),
trade.getProcessModel().getFundsNeededForTradeAsLong());
trade.updateDepositTxFromWallet();
tradesForStatistics.add(trade);
}

private void onTradesChanged() {
this.numPendingTrades.set(tradableList.getList().size());
}
Expand Down Expand Up @@ -602,6 +619,38 @@ public void addTradeToFailedTrades(Trade trade) {
cleanUpAddressEntries();
}

// If trade still has funds locked up it might come back from failed trades
// Aborts unfailing if the address entries needed are not available
private boolean unfailTrade(Trade trade) {
if (!recoverAddresses(trade)) {
log.warn("Failed to recover address during unfail trade");
return false;
}

initPendingTrade(trade);

if (!tradableList.contains(trade)) {
tradableList.add(trade);
}
return true;
}

// The trade is added to pending trades if the associated address entries are AVAILABLE and
// the relevant entries are changed, otherwise it's not added and no address entries are changed
private boolean recoverAddresses(Trade trade) {
// Find addresses associated with this trade.
var entries = TradeUtils.getAvailableAddresses(trade, btcWalletService, keyRing);
if (entries == null)
return false;

btcWalletService.recoverAddressEntry(trade.getId(), entries.first,
AddressEntry.Context.MULTI_SIG);
btcWalletService.recoverAddressEntry(trade.getId(), entries.second,
AddressEntry.Context.TRADE_PAYOUT);
return true;
}


// If trade is in preparation (if taker role: before taker fee is paid; both roles: before deposit published)
// we just remove the trade from our list. We don't store those trades.
public void removePreparedTrade(Trade trade) {
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/bisq/core/trade/TradeModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import com.google.inject.Singleton;

import static bisq.common.config.Config.ALLOW_FAULTY_DELAYED_TXS;
import static bisq.common.config.Config.DUMP_DELAYED_PAYOUT_TXS;
import static bisq.common.config.Config.DUMP_STATISTICS;
import static com.google.inject.name.Names.named;
Expand All @@ -58,5 +59,6 @@ protected void configure() {
bind(AssetTradeActivityCheck.class).in(Singleton.class);
bindConstant().annotatedWith(named(DUMP_STATISTICS)).to(config.dumpStatistics);
bindConstant().annotatedWith(named(DUMP_DELAYED_PAYOUT_TXS)).to(config.dumpDelayedPayoutTxs);
bindConstant().annotatedWith(named(ALLOW_FAULTY_DELAYED_TXS)).to(config.allowFaultyDelayedTxs);
}
}
79 changes: 79 additions & 0 deletions core/src/main/java/bisq/core/trade/TradeUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.core.trade;

import bisq.core.btc.wallet.BtcWalletService;

import bisq.common.crypto.KeyRing;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;

import java.util.Objects;

public class TradeUtils {

// Returns <MULTI_SIG, TRADE_PAYOUT> if both are AVAILABLE, otherwise null
static Tuple2<String, String> getAvailableAddresses(Trade trade, BtcWalletService btcWalletService,
KeyRing keyRing) {
var addresses = getTradeAddresses(trade, btcWalletService, keyRing);
if (addresses == null)
return null;

if (btcWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first)))
return null;
if (btcWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second)))
return null;

return new Tuple2<>(addresses.first, addresses.second);
}

// Returns <MULTI_SIG, TRADE_PAYOUT> addresses as strings if they're known by the wallet
public static Tuple2<String, String> getTradeAddresses(Trade trade, BtcWalletService btcWalletService,
KeyRing keyRing) {
var contract = trade.getContract();
if (contract == null)
return null;

// Get multisig address
var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing());
var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
if (multiSigPubKey == null)
return null;
var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey);
var multiSigAddress = btcWalletService.getAddressEntryListAsImmutableList().stream()
.filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString))
.findAny()
.orElse(null);
if (multiSigAddress == null)
return null;

// Get payout address
var payoutAddress = isMyRoleBuyer ?
contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString();
var payoutAddressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream()
.filter(e -> Objects.equals(e.getAddressString(), payoutAddress))
.findAny()
.orElse(null);
if (payoutAddressEntry == null)
return null;

return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress);
}
}
Loading

0 comments on commit 2a040f4

Please sign in to comment.