Merge pull request #4789 from stejbac/fix-remaining-blackmail-vulnerabilities

Fix remaining blackmail vulnerabilities
This commit is contained in:
Christoph Atteneder 2020-11-19 19:01:06 +01:00 committed by GitHub
commit 741425fad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 243 additions and 51 deletions

View File

@ -76,6 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class TradeWalletService {
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
private final WalletsSetup walletsSetup;
private final Preferences preferences;
@ -569,8 +570,9 @@ public class TradeWalletService {
* @param buyerPubKey the public key of the buyer
* @param sellerPubKey the public key of the seller
* @throws SigningException if (one of) the taker input(s) was of an unrecognized type for signing
* @throws TransactionVerificationException if a maker input wasn't signed, their MultiSig script or contract hash
* doesn't match the taker's, or there was an unexpected problem with the final deposit tx or its signatures
* @throws TransactionVerificationException if a non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig
* script or contract hash doesn't match the taker's, or there was an unexpected problem with the final deposit tx
* or its signatures
* @throws WalletException if the taker's wallet is null or structurally inconsistent
*/
public Transaction takerSignsDepositTx(boolean takerIsSeller,
@ -602,10 +604,11 @@ public class TradeWalletService {
for (int i = 0; i < buyerInputs.size(); i++) {
TransactionInput makersInput = makersDepositTx.getInputs().get(i);
byte[] makersScriptSigProgram = makersInput.getScriptSig().getProgram();
if (makersScriptSigProgram.length == 0 && TransactionWitness.EMPTY.equals(makersInput.getWitness())) {
throw new TransactionVerificationException("Inputs from maker not signed.");
}
TransactionInput input = getTransactionInput(depositTx, makersScriptSigProgram, buyerInputs.get(i));
Script scriptPubKey = checkNotNull(input.getConnectedOutput()).getScriptPubKey();
if (makersScriptSigProgram.length == 0 && !ScriptPattern.isP2WH(scriptPubKey)) {
throw new TransactionVerificationException("Non-segwit inputs from maker not signed.");
}
if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) {
input.setWitness(makersInput.getWitness());
}
@ -686,6 +689,21 @@ public class TradeWalletService {
}
public void sellerAddsBuyerWitnessesToDepositTx(Transaction myDepositTx,
Transaction buyersDepositTxWithWitnesses) {
int numberInputs = myDepositTx.getInputs().size();
for (int i = 0; i < numberInputs; i++) {
var txInput = myDepositTx.getInput(i);
var witnessFromBuyer = buyersDepositTxWithWitnesses.getInput(i).getWitness();
if (TransactionWitness.EMPTY.equals(txInput.getWitness()) &&
!TransactionWitness.EMPTY.equals(witnessFromBuyer)) {
txInput.setWitness(witnessFromBuyer);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Delayed payout tx
///////////////////////////////////////////////////////////////////////////////////////////
@ -729,12 +747,14 @@ public class TradeWalletService {
return mySignature.encodeToDER();
}
public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature)
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
public Transaction finalizeUnconnectedDelayedPayoutTx(Transaction delayedPayoutTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature,
Coin inputValue)
throws AddressFormatException, TransactionVerificationException, SignatureDecodeException {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);
@ -746,8 +766,26 @@ public class TradeWalletService {
input.setWitness(witness);
WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);
if (checkNotNull(inputValue).isLessThan(delayedPayoutTx.getOutputSum().add(MIN_DELAYED_PAYOUT_TX_FEE))) {
throw new TransactionVerificationException("Delayed payout tx is paying less than the minimum allowed tx fee");
}
Script scriptPubKey = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
input.getScriptSig().correctlySpends(delayedPayoutTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS);
return delayedPayoutTx;
}
public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature)
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
TransactionInput input = delayedPayoutTx.getInput(0);
finalizeUnconnectedDelayedPayoutTx(delayedPayoutTx, buyerPubKey, sellerPubKey, buyerSignature, sellerSignature, input.getValue());
WalletService.checkWalletConsistency(wallet);
WalletService.checkScriptSig(delayedPayoutTx, input, 0);
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
input.verify(input.getConnectedOutput());
return delayedPayoutTx;
@ -1185,11 +1223,20 @@ public class TradeWalletService {
private TransactionInput getTransactionInput(Transaction depositTx,
byte[] scriptProgram,
RawTransactionInput rawTransactionInput) {
return new TransactionInput(params, depositTx, scriptProgram, new TransactionOutPoint(params,
rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)),
return new TransactionInput(params, depositTx, scriptProgram, getConnectedOutPoint(rawTransactionInput),
Coin.valueOf(rawTransactionInput.value));
}
private TransactionOutPoint getConnectedOutPoint(RawTransactionInput rawTransactionInput) {
return new TransactionOutPoint(params, rawTransactionInput.index,
new Transaction(params, rawTransactionInput.parentTransaction));
}
public boolean isP2WH(RawTransactionInput rawTransactionInput) {
return ScriptPattern.isP2WH(
checkNotNull(getConnectedOutPoint(rawTransactionInput).getConnectedOutput()).getScriptPubKey());
}
// TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well.
// Atm it is still used by traderSignAndFinalizeDisputedPayoutTx which is used by ArbitrationManager.

View File

@ -33,16 +33,19 @@ import lombok.Value;
public final class DelayedPayoutTxSignatureRequest extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] delayedPayoutTx;
private final byte[] delayedPayoutTxSellerSignature;
public DelayedPayoutTxSignatureRequest(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTx) {
byte[] delayedPayoutTx,
byte[] delayedPayoutTxSellerSignature) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
delayedPayoutTx);
delayedPayoutTx,
delayedPayoutTxSellerSignature);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -53,10 +56,12 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTx) {
byte[] delayedPayoutTx,
byte[] delayedPayoutTxSellerSignature) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.delayedPayoutTx = delayedPayoutTx;
this.delayedPayoutTxSellerSignature = delayedPayoutTxSellerSignature;
}
@ -67,16 +72,19 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx)))
.setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx))
.setDelayedPayoutTxSellerSignature(ByteString.copyFrom(delayedPayoutTxSellerSignature)))
.build();
}
public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto, int messageVersion) {
public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto,
int messageVersion) {
return new DelayedPayoutTxSignatureRequest(messageVersion,
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDelayedPayoutTx().toByteArray());
proto.getDelayedPayoutTx().toByteArray(),
proto.getDelayedPayoutTxSellerSignature().toByteArray());
}
@Override
@ -84,6 +92,7 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
return "DelayedPayoutTxSignatureRequest{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) +
",\n delayedPayoutTxSellerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSellerSignature) +
"\n} " + super.toString();
}
}

View File

@ -32,17 +32,20 @@ import lombok.Value;
@Value
public final class DelayedPayoutTxSignatureResponse extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] delayedPayoutTxSignature;
private final byte[] delayedPayoutTxBuyerSignature;
private final byte[] depositTx;
public DelayedPayoutTxSignatureResponse(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTxSignature) {
byte[] delayedPayoutTxBuyerSignature,
byte[] depositTx) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
delayedPayoutTxSignature);
delayedPayoutTxBuyerSignature,
depositTx);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -53,10 +56,12 @@ public final class DelayedPayoutTxSignatureResponse extends TradeMessage impleme
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTxSignature) {
byte[] delayedPayoutTxBuyerSignature,
byte[] depositTx) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.delayedPayoutTxSignature = delayedPayoutTxSignature;
this.delayedPayoutTxBuyerSignature = delayedPayoutTxBuyerSignature;
this.depositTx = depositTx;
}
@ -67,24 +72,28 @@ public final class DelayedPayoutTxSignatureResponse extends TradeMessage impleme
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDelayedPayoutTxSignature(ByteString.copyFrom(delayedPayoutTxSignature))
.setDelayedPayoutTxBuyerSignature(ByteString.copyFrom(delayedPayoutTxBuyerSignature))
.setDepositTx(ByteString.copyFrom(depositTx))
)
.build();
}
public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto, int messageVersion) {
public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto,
int messageVersion) {
return new DelayedPayoutTxSignatureResponse(messageVersion,
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDelayedPayoutTxSignature().toByteArray());
proto.getDelayedPayoutTxBuyerSignature().toByteArray(),
proto.getDepositTx().toByteArray());
}
@Override
public String toString() {
return "DelayedPayoutTxSignatureResponse{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n delayedPayoutTxSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSignature) +
",\n delayedPayoutTxBuyerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxBuyerSignature) +
",\n depositTx=" + Utilities.bytesAsHexString(depositTx) +
"\n} " + super.toString();
}
}

View File

@ -34,17 +34,17 @@ import lombok.Value;
@Value
public final class DepositTxMessage extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] depositTx;
private final byte[] depositTxWithoutWitnesses;
public DepositTxMessage(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] depositTx) {
byte[] depositTxWithoutWitnesses) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
depositTx);
depositTxWithoutWitnesses);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -55,10 +55,10 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] depositTx) {
byte[] depositTxWithoutWitnesses) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.depositTx = depositTx;
this.depositTxWithoutWitnesses = depositTxWithoutWitnesses;
}
@Override
@ -68,7 +68,7 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDepositTx(ByteString.copyFrom(depositTx)))
.setDepositTxWithoutWitnesses(ByteString.copyFrom(depositTxWithoutWitnesses)))
.build();
}
@ -77,14 +77,14 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDepositTx().toByteArray());
proto.getDepositTxWithoutWitnesses().toByteArray());
}
@Override
public String toString() {
return "DepositTxMessage{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n depositTx=" + Utilities.bytesAsHexString(depositTx) +
",\n depositTxWithoutWitnesses=" + Utilities.bytesAsHexString(depositTxWithoutWitnesses) +
"\n} " + super.toString();
}
}

View File

@ -26,6 +26,7 @@ import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener;
@ -103,6 +104,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerRemovesOpenOffer.class,
BuyerVerifiesPreparedDelayedPayoutTx.class,
BuyerSignsDelayedPayoutTx.class,
BuyerFinalizesDelayedPayoutTx.class,
BuyerSendsDelayedPayoutTxSignatureResponse.class)
.withTimeout(30))
.executeTasks();

View File

@ -29,6 +29,7 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener;
@ -119,6 +120,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
BuyerProcessDelayedPayoutTxSignatureRequest.class,
BuyerVerifiesPreparedDelayedPayoutTx.class,
BuyerSignsDelayedPayoutTx.class,
BuyerFinalizesDelayedPayoutTx.class,
BuyerSendsDelayedPayoutTxSignatureResponse.class)
.withTimeout(30))
.executeTasks();

View File

@ -35,6 +35,7 @@ import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime;
import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage;
@ -103,6 +104,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerRemovesOpenOffer.class,
SellerAsMakerFinalizesDepositTx.class,
SellerCreatesDelayedPayoutTx.class,
SellerSignsDelayedPayoutTx.class,
SellerSendDelayedPayoutTxSignatureRequest.class)
.withTimeout(30))
.executeTasks();

View File

@ -30,6 +30,7 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx;
import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx;
@ -98,6 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
TakerPublishFeeTx.class,
SellerAsTakerSignsDepositTx.class,
SellerCreatesDelayedPayoutTx.class,
SellerSignsDelayedPayoutTx.class,
SellerSendDelayedPayoutTxSignatureRequest.class)
.withTimeout(30))
.executeTasks();

View File

@ -33,7 +33,6 @@ import bisq.core.trade.protocol.tasks.seller.SellerPublishesTradeStatistics;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.network.p2p.NodeAddress;
@ -77,7 +76,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
.with(message)
.from(peer))
.setup(tasks(SellerProcessDelayedPayoutTxSignatureResponse.class,
SellerSignsDelayedPayoutTx.class,
SellerFinalizesDelayedPayoutTx.class,
SellerSendsDepositTxAndDelayedPayoutTxMessage.class,
SellerPublishesDepositTx.class,

View File

@ -0,0 +1,61 @@
package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Transaction;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class BuyerFinalizesDelayedPayoutTx extends TradeTask {
public BuyerFinalizesDelayedPayoutTx(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
BtcWalletService btcWalletService = processModel.getBtcWalletService();
String id = processModel.getOffer().getId();
Transaction preparedDepositTx = btcWalletService.getTxFromSerializedTx(processModel.getPreparedDepositTx());
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey();
checkArgument(Arrays.equals(buyerMultiSigPubKey,
btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()),
"buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id);
byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey();
byte[] buyerSignature = processModel.getDelayedPayoutTxSignature();
byte[] sellerSignature = processModel.getTradingPeer().getDelayedPayoutTxSignature();
Transaction signedDelayedPayoutTx = processModel.getTradeWalletService().finalizeUnconnectedDelayedPayoutTx(
preparedDelayedPayoutTx,
buyerMultiSigPubKey,
sellerMultiSigPubKey,
buyerSignature,
sellerSignature,
preparedDepositTx.getOutput(0).getValue());
trade.applyDelayedPayoutTxBytes(signedDelayedPayoutTx.bitcoinSerialize());
log.info("DelayedPayoutTxBytes = {}", Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()));
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View File

@ -46,6 +46,7 @@ public class BuyerProcessDelayedPayoutTxSignatureRequest extends TradeTask {
byte[] delayedPayoutTxAsBytes = checkNotNull(request.getDelayedPayoutTx());
Transaction preparedDelayedPayoutTx = processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxAsBytes);
processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx);
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(request.getDelayedPayoutTxSellerSignature()));
// When we receive that message the taker has published the taker fee, so we apply it to the trade.
// The takerFeeTx was sent in the first message. It should be part of DelayedPayoutTxSignatureRequest

View File

@ -26,12 +26,16 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.wallet.Wallet;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -59,9 +63,11 @@ public class BuyerProcessDepositTxAndDelayedPayoutTxMessage extends TradeTask {
// To access tx confidence we need to add that tx into our wallet.
byte[] delayedPayoutTxBytes = checkNotNull(message.getDelayedPayoutTx());
checkArgument(Arrays.equals(delayedPayoutTxBytes, trade.getDelayedPayoutTxBytes()),
"mismatch between delayedPayoutTx received from peer and our one." +
"\n Expected: " + Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()) +
"\n Received: " + Utilities.bytesAsHexString(delayedPayoutTxBytes));
trade.applyDelayedPayoutTxBytes(delayedPayoutTxBytes);
BtcWalletService.printTx("delayedPayoutTx received from peer",
checkNotNull(trade.getDelayedPayoutTx()));
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());

View File

@ -44,10 +44,15 @@ public class BuyerSendsDelayedPayoutTxSignatureResponse extends TradeTask {
runInterceptHook();
byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature());
byte[] depositTxBytes = processModel.getDepositTx() != null
? processModel.getDepositTx().bitcoinSerialize() // set in BuyerAsTakerSignsDepositTx task
: processModel.getPreparedDepositTx(); // set in BuyerAsMakerCreatesAndSignsDepositTx task
DelayedPayoutTxSignatureResponse message = new DelayedPayoutTxSignatureResponse(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
delayedPayoutTxSignature);
delayedPayoutTxSignature,
depositTxBytes);
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",

View File

@ -25,6 +25,8 @@ import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
public BuyerVerifiesPreparedDelayedPayoutTx(TaskRunner<Trade> taskHandler, Trade trade) {
@ -36,11 +38,22 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
try {
runInterceptHook();
var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx();
TradeDataValidation.validateDelayedPayoutTx(trade,
processModel.getPreparedDelayedPayoutTx(),
preparedDelayedPayoutTx,
processModel.getDaoFacade(),
processModel.getBtcWalletService());
// If the deposit tx is non-malleable, we already know its final ID, so should check that now
// before sending any further data to the seller, to provide extra protection for the buyer.
if (isDepositTxNonMalleable()) {
var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(
processModel.getPreparedDepositTx());
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx));
} else {
log.info("Deposit tx is malleable, so we skip preparedDelayedPayoutTx input validation.");
}
complete();
} catch (TradeDataValidation.ValidationException e) {
failed(e.getMessage());
@ -48,4 +61,12 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
failed(t);
}
}
private boolean isDepositTxNonMalleable() {
var buyerInputs = checkNotNull(processModel.getRawTransactionInputs());
var sellerInputs = checkNotNull(processModel.getTradingPeer().getRawTransactionInputs());
return buyerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH) &&
sellerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH);
}
}

View File

@ -72,6 +72,8 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask {
.add(tradeAmount);
List<RawTransactionInput> takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs());
checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH),
"all takerRawTransactionInputs must be P2WH");
long takerChangeOutputValue = tradingPeer.getChangeOutputValue();
@Nullable String takerChangeAddressString = tradingPeer.getChangeOutputAddress();
Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();

View File

@ -22,6 +22,8 @@ import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse
import bisq.common.taskrunner.TaskRunner;
import org.bitcoinj.core.Transaction;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -32,6 +34,9 @@ public class BuyerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInput
@Override
protected byte[] getPreparedDepositTx() {
return processModel.getPreparedDepositTx();
Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx());
// Remove witnesses from preparedDepositTx, so that the seller can still compute the final
// tx id, but cannot publish it before providing the buyer with a signed delayed payout tx.
return preparedDepositTx.bitcoinSerialize(false);
}
}

View File

@ -41,10 +41,12 @@ public class BuyerAsTakerSendsDepositTxMessage extends TradeTask {
try {
runInterceptHook();
if (processModel.getDepositTx() != null) {
// Remove witnesses from the sent depositTx, so that the seller can still compute the final
// tx id, but cannot publish it before providing the buyer with a signed delayed payout tx.
DepositTxMessage message = new DepositTxMessage(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
processModel.getDepositTx().bitcoinSerialize());
processModel.getDepositTx().bitcoinSerialize(false));
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",

View File

@ -20,6 +20,7 @@ package bisq.core.trade.protocol.tasks.seller;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.governance.param.Param;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
@ -53,6 +54,10 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask {
donationAddressString,
minerFee,
lockTime);
TradeDataValidation.validateDelayedPayoutTx(trade,
preparedDelayedPayoutTx,
processModel.getDaoFacade(),
processModel.getBtcWalletService());
processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx);

View File

@ -42,7 +42,12 @@ public class SellerProcessDelayedPayoutTxSignatureResponse extends TradeTask {
checkNotNull(response);
checkTradeId(processModel.getOfferId(), response);
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(response.getDelayedPayoutTxSignature()));
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(response.getDelayedPayoutTxBuyerSignature()));
processModel.getTradeWalletService().sellerAddsBuyerWitnessesToDepositTx(
processModel.getDepositTx(),
processModel.getBtcWalletService().getTxFromSerializedTx(response.getDepositTx())
);
// update to the latest peer address of our peer if the message is correct
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());

View File

@ -47,10 +47,13 @@ public class SellerSendDelayedPayoutTxSignatureRequest extends TradeTask {
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx(),
"processModel.getPreparedDelayedPayoutTx() must not be null");
byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature(),
"processModel.getDelayedPayoutTxSignature() must not be null");
DelayedPayoutTxSignatureRequest message = new DelayedPayoutTxSignatureRequest(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
preparedDelayedPayoutTx.bitcoinSerialize());
preparedDelayedPayoutTx.bitcoinSerialize(),
delayedPayoutTxSignature);
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",

View File

@ -78,6 +78,8 @@ public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask {
.add(offer.getBuyerSecurityDeposit());
List<RawTransactionInput> takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs());
checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH),
"all takerRawTransactionInputs must be P2WH");
long takerChangeOutputValue = tradingPeer.getChangeOutputValue();
String takerChangeAddressString = tradingPeer.getChangeOutputAddress();
Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();

View File

@ -43,7 +43,7 @@ public class SellerAsMakerProcessDepositTxMessage extends TradeTask {
Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message);
processModel.getTradingPeer().setPreparedDepositTx(checkNotNull(message.getDepositTx()));
processModel.getTradingPeer().setPreparedDepositTx(checkNotNull(message.getDepositTxWithoutWitnesses()));
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
// When we receive that message the taker has published the taker fee, so we apply it to the trade.

View File

@ -44,6 +44,7 @@ public class SellerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInpu
processModel.getTradeManager().requestPersistence();
return preparedDepositTx.bitcoinSerialize();
// Make sure witnesses are removed as well before sending, to cover the segwit case.
return preparedDepositTx.bitcoinSerialize(false);
}
}

View File

@ -271,13 +271,15 @@ message DelayedPayoutTxSignatureRequest {
string trade_id = 2;
NodeAddress sender_node_address = 3;
bytes delayed_payout_tx = 4;
bytes delayed_payout_tx_seller_signature = 5;
}
message DelayedPayoutTxSignatureResponse {
string uid = 1;
string trade_id = 2;
NodeAddress sender_node_address = 3;
bytes delayed_payout_tx_signature = 4;
bytes delayed_payout_tx_buyer_signature = 4;
bytes deposit_tx = 5;
}
message DepositTxAndDelayedPayoutTxMessage {
@ -292,7 +294,7 @@ message DepositTxMessage {
string uid = 1;
string trade_id = 2;
NodeAddress sender_node_address = 3;
bytes deposit_tx = 4;
bytes deposit_tx_without_witnesses = 4;
}
message PeerPublishedDelayedPayoutTxMessage {