Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unfail with reattach #4157

Merged
merged 7 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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