mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-21 12:48:35 -04:00
258 lines
10 KiB
Java
258 lines
10 KiB
Java
/*
|
|
* This file is part of Haveno.
|
|
*
|
|
* Haveno 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.
|
|
*
|
|
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package bisq.core.trade;
|
|
|
|
import bisq.core.btc.wallet.BtcWalletService;
|
|
import bisq.core.offer.Offer;
|
|
import bisq.core.support.dispute.Dispute;
|
|
|
|
import org.bitcoinj.core.Address;
|
|
import org.bitcoinj.core.NetworkParameters;
|
|
import org.bitcoinj.core.Transaction;
|
|
import org.bitcoinj.core.TransactionInput;
|
|
import org.bitcoinj.core.TransactionOutPoint;
|
|
import org.bitcoinj.core.TransactionOutput;
|
|
|
|
import java.math.BigInteger;
|
|
import java.util.function.Consumer;
|
|
|
|
import lombok.Getter;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
|
|
@Slf4j
|
|
public class TradeDataValidation {
|
|
|
|
public static void validateDelayedPayoutTx(Trade trade,
|
|
Transaction delayedPayoutTx,
|
|
BtcWalletService btcWalletService)
|
|
throws AddressException, MissingTxException,
|
|
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
|
|
validateDelayedPayoutTx(trade,
|
|
delayedPayoutTx,
|
|
null,
|
|
btcWalletService,
|
|
null);
|
|
}
|
|
|
|
public static void validateDelayedPayoutTx(Trade trade,
|
|
Transaction delayedPayoutTx,
|
|
@Nullable Dispute dispute,
|
|
BtcWalletService btcWalletService)
|
|
throws AddressException, MissingTxException,
|
|
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
|
|
validateDelayedPayoutTx(trade,
|
|
delayedPayoutTx,
|
|
dispute,
|
|
btcWalletService,
|
|
null);
|
|
}
|
|
|
|
public static void validateDelayedPayoutTx(Trade trade,
|
|
Transaction delayedPayoutTx,
|
|
BtcWalletService btcWalletService,
|
|
@Nullable Consumer<String> addressConsumer)
|
|
throws AddressException, MissingTxException,
|
|
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
|
|
validateDelayedPayoutTx(trade,
|
|
delayedPayoutTx,
|
|
null,
|
|
btcWalletService,
|
|
addressConsumer);
|
|
}
|
|
|
|
public static void validateDelayedPayoutTx(Trade trade,
|
|
Transaction delayedPayoutTx,
|
|
@Nullable Dispute dispute,
|
|
BtcWalletService btcWalletService,
|
|
@Nullable Consumer<String> addressConsumer)
|
|
throws AddressException, MissingTxException,
|
|
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
|
|
String errorMsg;
|
|
if (delayedPayoutTx == null) {
|
|
errorMsg = "DelayedPayoutTx must not be null";
|
|
log.error(errorMsg);
|
|
throw new MissingTxException("DelayedPayoutTx must not be null");
|
|
}
|
|
|
|
// Validate tx structure
|
|
if (delayedPayoutTx.getInputs().size() != 1) {
|
|
errorMsg = "Number of delayedPayoutTx inputs must be 1";
|
|
log.error(errorMsg);
|
|
log.error(delayedPayoutTx.toString());
|
|
throw new InvalidTxException(errorMsg);
|
|
}
|
|
|
|
if (delayedPayoutTx.getOutputs().size() != 1) {
|
|
errorMsg = "Number of delayedPayoutTx outputs must be 1";
|
|
log.error(errorMsg);
|
|
log.error(delayedPayoutTx.toString());
|
|
throw new InvalidTxException(errorMsg);
|
|
}
|
|
|
|
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
|
|
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
|
|
|
|
// Validate lock time
|
|
if (delayedPayoutTx.getLockTime() != trade.getLockTime()) {
|
|
errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime()";
|
|
log.error(errorMsg);
|
|
log.error(delayedPayoutTx.toString());
|
|
throw new InvalidLockTimeException(errorMsg);
|
|
}
|
|
|
|
// Validate seq num
|
|
if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) {
|
|
errorMsg = "Sequence number must be 0xFFFFFFFE";
|
|
log.error(errorMsg);
|
|
log.error(delayedPayoutTx.toString());
|
|
throw new InvalidLockTimeException(errorMsg);
|
|
}
|
|
|
|
// Check amount
|
|
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
|
Offer offer = checkNotNull(trade.getOffer());
|
|
BigInteger msOutputAmount = offer.getBuyerSecurityDeposit()
|
|
.add(offer.getSellerSecurityDeposit())
|
|
.add(checkNotNull(trade.getAmount()));
|
|
|
|
if (!output.getValue().equals(msOutputAmount)) {
|
|
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 InvalidAmountException(errorMsg);
|
|
}
|
|
|
|
NetworkParameters params = btcWalletService.getParams();
|
|
Address address = output.getScriptPubKey().getToAddress(params);
|
|
if (address == null) {
|
|
errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output;
|
|
log.error(errorMsg);
|
|
log.error(delayedPayoutTx.toString());
|
|
throw new AddressException(dispute, errorMsg);
|
|
}
|
|
|
|
String addressAsString = address.toString();
|
|
if (addressConsumer != null) {
|
|
addressConsumer.accept(addressAsString);
|
|
}
|
|
|
|
if (dispute != null) {
|
|
// Verify that address in the dispute matches the one in the trade.
|
|
String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx();
|
|
// Old clients don't have it set yet. Can be removed after a forced update
|
|
if (donationAddressOfDelayedPayoutTx != null) {
|
|
checkArgument(addressAsString.equals(donationAddressOfDelayedPayoutTx),
|
|
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx");
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void validatePayoutTxInput(Transaction depositTx,
|
|
Transaction delayedPayoutTx)
|
|
throws InvalidInputException {
|
|
TransactionInput input = delayedPayoutTx.getInput(0);
|
|
checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null");
|
|
// input.getConnectedOutput() is null as the tx is not committed at that point
|
|
|
|
TransactionOutPoint outpoint = input.getOutpoint();
|
|
if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) {
|
|
throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" +
|
|
"Delayed payout tx=" + delayedPayoutTx + "\n" +
|
|
"Deposit tx=" + depositTx);
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
// Exceptions
|
|
///////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
public static class ValidationException extends Exception {
|
|
@Nullable
|
|
@Getter
|
|
private final Dispute dispute;
|
|
|
|
ValidationException(String msg) {
|
|
this(null, msg);
|
|
}
|
|
|
|
ValidationException(@Nullable Dispute dispute, String msg) {
|
|
super(msg);
|
|
this.dispute = dispute;
|
|
}
|
|
}
|
|
|
|
public static class InvalidPaymentAccountPayloadException extends ValidationException {
|
|
InvalidPaymentAccountPayloadException(@Nullable Dispute dispute, String msg) {
|
|
super(dispute, msg);
|
|
}
|
|
}
|
|
|
|
public static class AddressException extends ValidationException {
|
|
AddressException(@Nullable Dispute dispute, String msg) {
|
|
super(dispute, msg);
|
|
}
|
|
}
|
|
|
|
public static class MissingTxException extends ValidationException {
|
|
MissingTxException(String msg) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
public static class InvalidTxException extends ValidationException {
|
|
InvalidTxException(String msg) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
public static class InvalidAmountException extends ValidationException {
|
|
InvalidAmountException(String msg) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
public static class InvalidLockTimeException extends ValidationException {
|
|
InvalidLockTimeException(String msg) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
public static class InvalidInputException extends ValidationException {
|
|
InvalidInputException(String msg) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
public static class DisputeReplayException extends ValidationException {
|
|
DisputeReplayException(Dispute dispute, String msg) {
|
|
super(dispute, msg);
|
|
}
|
|
}
|
|
|
|
public static class NodeAddressException extends ValidationException {
|
|
NodeAddressException(Dispute dispute, String msg) {
|
|
super(dispute, msg);
|
|
}
|
|
}
|
|
}
|