diff --git a/core/src/main/java/bisq/core/btc/wallet/RedirectionTransactionFactory.java b/core/src/main/java/bisq/core/btc/wallet/RedirectionTransactionFactory.java new file mode 100644 index 00000000000..f2dba199089 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/RedirectionTransactionFactory.java @@ -0,0 +1,133 @@ +/* + * 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 . + */ + +package bisq.core.btc.wallet; + +import bisq.core.btc.exceptions.TransactionVerificationException; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; + +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class RedirectionTransactionFactory { + + private final NetworkParameters params; + + public RedirectionTransactionFactory(NetworkParameters params) { + this.params = params; + } + + public Transaction createUnsignedRedirectionTransaction(Transaction warningTx, + List> receivers, + Tuple2 feeBumpOutputAmountAndAddress) + throws AddressFormatException, TransactionVerificationException { + + TransactionOutput warningTxOutput = warningTx.getOutput(0); + + Transaction redirectionTx = new Transaction(params); + redirectionTx.addInput(warningTxOutput); + + checkArgument(!receivers.isEmpty(), "receivers must not be empty"); + receivers.forEach(receiver -> redirectionTx.addOutput(Coin.valueOf(receiver.first), Address.fromString(params, receiver.second))); + + redirectionTx.addOutput( + Coin.valueOf(feeBumpOutputAmountAndAddress.first), + Address.fromString(params, feeBumpOutputAmountAndAddress.second) + ); + + WalletService.printTx("Unsigned redirectionTx", redirectionTx); + WalletService.verifyTransaction(redirectionTx); + + return redirectionTx; + } + + public byte[] signRedirectionTransaction(Transaction redirectionTx, + Transaction warningTx, + DeterministicKey myMultiSigKeyPair, + KeyParameter aesKey) + throws AddressFormatException, TransactionVerificationException { + + TransactionOutput warningTxPayoutOutput = warningTx.getOutput(0); + Script redeemScript = warningTxPayoutOutput.getScriptPubKey(); + Coin redirectionTxInputValue = warningTxPayoutOutput.getValue(); + + Sha256Hash sigHash = redirectionTx.hashForWitnessSignature(0, redeemScript, + redirectionTxInputValue, Transaction.SigHash.ALL, false); + + checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); + if (myMultiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + + ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + WalletService.printTx("redirectionTx for sig creation", redirectionTx); + WalletService.verifyTransaction(redirectionTx); + return mySignature.encodeToDER(); + } + + public Transaction finalizeRedirectionTransaction(Transaction warningTx, + Transaction redirectionTx, + byte[] buyerSignature, + byte[] sellerSignature, + Coin inputValue) + throws AddressFormatException, TransactionVerificationException { + + TransactionInput input = redirectionTx.getInput(0); + input.setScriptSig(ScriptBuilder.createEmpty()); + + Script redeemScript = createRedeemScript(buyerSignature, sellerSignature); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript); + input.setWitness(witness); + + WalletService.printTx("finalizeRedirectionTransaction", redirectionTx); + WalletService.verifyTransaction(redirectionTx); + + Script scriptPubKey = warningTx.getOutput(0).getScriptPubKey(); + input.getScriptSig().correctlySpends(redirectionTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS); + return redirectionTx; + } + + private Script createRedeemScript(byte[] buyerSignature, byte[] sellerSignature) { + return new ScriptBuilder() + .number(0) + .data(buyerSignature) + .data(sellerSignature) + .number(1) + .build(); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index bb670aeb033..2ca230ad1d3 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -83,6 +83,7 @@ public class TradeWalletService { private final NetworkParameters params; private final WarningTransactionFactory warningTransactionFactory; + private final RedirectionTransactionFactory redirectionTransactionFactory; @Nullable private Wallet wallet; @@ -102,6 +103,7 @@ public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) { this.preferences = preferences; this.params = Config.baseCurrencyNetworkParameters(); this.warningTransactionFactory = new WarningTransactionFactory(params); + this.redirectionTransactionFactory = new RedirectionTransactionFactory(params); walletsSetup.addSetupCompletedHandler(() -> { walletConfig = walletsSetup.getWalletConfig(); wallet = walletsSetup.getBtcWallet(); @@ -856,6 +858,49 @@ public Transaction finalizeWarningTx(Transaction warningTx, ); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Redirection tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction createUnsignedRedirectionTx(Transaction warningTx, + List> receivers, + Tuple2 feeBumpOutputAmountAndAddress) + throws AddressFormatException, TransactionVerificationException { + return redirectionTransactionFactory.createUnsignedRedirectionTransaction( + warningTx, + receivers, + feeBumpOutputAmountAndAddress + ); + } + + public byte[] signRedirectionTx(Transaction redirectionTx, + Transaction warningTx, + DeterministicKey myMultiSigKeyPair, + KeyParameter aesKey) + throws AddressFormatException, TransactionVerificationException { + return redirectionTransactionFactory.signRedirectionTransaction( + redirectionTx, + warningTx, + myMultiSigKeyPair, + aesKey + ); + } + + public Transaction finalizeRedirectionTx(Transaction warningTx, + Transaction redirectionTx, + byte[] buyerSignature, + byte[] sellerSignature, + Coin inputValue) + throws AddressFormatException, TransactionVerificationException, SignatureDecodeException { + return redirectionTransactionFactory.finalizeRedirectionTransaction( + warningTx, + redirectionTx, + buyerSignature, + sellerSignature, + inputValue + ); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Standard payout tx ///////////////////////////////////////////////////////////////////////////////////////////