Skip to content

Commit

Permalink
Implement WarningTransactionFactory
Browse files Browse the repository at this point in the history
The factory can create, sign, and finalize the warning transaction.
  • Loading branch information
alvasw committed Aug 9, 2023
1 parent cc183ca commit 4938188
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 3 deletions.
67 changes: 64 additions & 3 deletions core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class TradeWalletService {
private final Preferences preferences;
private final NetworkParameters params;

private final WarningTransactionFactory warningTransactionFactory;

@Nullable
private Wallet wallet;
@Nullable
Expand All @@ -99,6 +101,7 @@ public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) {
this.walletsSetup = walletsSetup;
this.preferences = preferences;
this.params = Config.baseCurrencyNetworkParameters();
this.warningTransactionFactory = new WarningTransactionFactory(params);
walletsSetup.addSetupCompletedHandler(() -> {
walletConfig = walletsSetup.getWalletConfig();
wallet = walletsSetup.getBtcWallet();
Expand Down Expand Up @@ -794,6 +797,64 @@ public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx,
return delayedPayoutTx;
}

///////////////////////////////////////////////////////////////////////////////////////////
// Warning tx
///////////////////////////////////////////////////////////////////////////////////////////

public Transaction createUnsignedWarningTx(boolean isBuyer,
Transaction depositTx,
long lockTime,
byte[] buyerPubKey,
byte[] sellerPubKey,
int claimDelay,
long miningFee,
Tuple2<Long, String> feeBumpOutputAmountAndAddress)
throws TransactionVerificationException {
return warningTransactionFactory.createUnsignedWarningTransaction(
isBuyer,
depositTx,
lockTime,
buyerPubKey,
sellerPubKey,
claimDelay,
miningFee,
feeBumpOutputAmountAndAddress
);
}

public byte[] signWarningTx(Transaction warningTx,
Transaction preparedDepositTx,
DeterministicKey myMultiSigKeyPair,
byte[] buyerPubKey,
byte[] sellerPubKey,
KeyParameter aesKey)
throws AddressFormatException, TransactionVerificationException {
return warningTransactionFactory.signWarningTransaction(
warningTx,
preparedDepositTx,
myMultiSigKeyPair,
buyerPubKey,
sellerPubKey,
aesKey
);
}

public Transaction finalizeWarningTx(Transaction warningTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature,
Coin inputValue)
throws AddressFormatException, TransactionVerificationException, SignatureDecodeException {
return warningTransactionFactory.finalizeWarningTransaction(
warningTx,
buyerPubKey,
sellerPubKey,
buyerSignature,
sellerSignature,
inputValue
);
}

///////////////////////////////////////////////////////////////////////////////////////////
// Standard payout tx
Expand Down Expand Up @@ -1371,15 +1432,15 @@ private Script get2of3MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubK
return ScriptBuilder.createMultiSigOutputScript(2, keys);
}

private Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey) {
static Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey) {
ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey);
ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey);
// Take care of sorting! Need to reverse to the order we use normally (buyer, seller)
List<ECKey> keys = ImmutableList.of(sellerKey, buyerKey);
return ScriptBuilder.createMultiSigOutputScript(2, keys);
}

private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) {
static Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
if (legacy) {
return ScriptBuilder.createP2SHOutputScript(redeemScript);
Expand Down Expand Up @@ -1470,7 +1531,7 @@ private void addAvailableInputsAndChangeOutputs(Transaction transaction,
}
}

private void applyLockTime(long lockTime, Transaction tx) {
static void applyLockTime(long lockTime, Transaction tx) {
checkArgument(!tx.getInputs().isEmpty(), "The tx must have inputs. tx={}", tx);
tx.getInputs().forEach(input -> input.setSequenceNumber(TransactionInput.NO_SEQUENCE - 1));
tx.setLockTime(lockTime);
Expand Down
156 changes: 156 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/WarningTransactionFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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.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 static com.google.common.base.Preconditions.checkNotNull;
import static org.bitcoinj.script.ScriptOpCodes.*;

public class WarningTransactionFactory {
private final NetworkParameters params;

public WarningTransactionFactory(NetworkParameters params) {
this.params = params;
}

public Transaction createUnsignedWarningTransaction(boolean isBuyer,
Transaction depositTx,
long lockTime,
byte[] buyerPubKey,
byte[] sellerPubKey,
int claimDelay,
long miningFee,
Tuple2<Long, String> feeBumpOutputAmountAndAddress)
throws TransactionVerificationException {

Transaction warningTx = new Transaction(params);
TradeWalletService.applyLockTime(lockTime, warningTx);

TransactionOutput depositTxOutput = depositTx.getOutput(0);
warningTx.addInput(depositTxOutput);

Coin warningTxOutputCoin = depositTxOutput.getValue()
.subtract(Coin.valueOf(miningFee))
.subtract(Coin.valueOf(feeBumpOutputAmountAndAddress.first));
Script outputScript = createOutputScript(isBuyer, buyerPubKey, sellerPubKey, claimDelay);
warningTx.addOutput(warningTxOutputCoin, outputScript);

warningTx.addOutput(
Coin.valueOf(feeBumpOutputAmountAndAddress.first),
Address.fromString(params, feeBumpOutputAmountAndAddress.second)
);

WalletService.printTx("Unsigned warningTx", warningTx);
WalletService.verifyTransaction(warningTx);
return warningTx;
}

public byte[] signWarningTransaction(Transaction warningTx,
Transaction preparedDepositTx,
DeterministicKey myMultiSigKeyPair,
byte[] buyerPubKey,
byte[] sellerPubKey,
KeyParameter aesKey)
throws AddressFormatException, TransactionVerificationException {

Script redeemScript = TradeWalletService.get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
Coin warningTxInputValue = preparedDepositTx.getOutput(0).getValue();

Sha256Hash sigHash = warningTx.hashForWitnessSignature(0, redeemScript,
warningTxInputValue, 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("warningTx for sig creation", warningTx);
WalletService.verifyTransaction(warningTx);
return mySignature.encodeToDER();
}

public Transaction finalizeWarningTransaction(Transaction warningTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature,
Coin inputValue)
throws AddressFormatException, TransactionVerificationException, SignatureDecodeException {

Script redeemScript = TradeWalletService.get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);

TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);

TransactionInput input = warningTx.getInput(0);
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);

WalletService.printTx("finalizeWarningTransaction", warningTx);
WalletService.verifyTransaction(warningTx);

Script scriptPubKey = TradeWalletService.get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
input.getScriptSig().correctlySpends(warningTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS);
return warningTx;
}

private Script createOutputScript(boolean isBuyer, byte[] buyerPubKey, byte[] sellerPubKey, int claimDelay) {
var scriptBuilder = new ScriptBuilder();
scriptBuilder.op(OP_IF)
.number(2)
.data(buyerPubKey)
.data(sellerPubKey)
.number(2)
.op(OP_CHECKMULTISIG);

scriptBuilder.op(OP_ELSE)
.number(claimDelay)
.op(OP_CHECKSEQUENCEVERIFY)
.op(OP_DROP)
.data(isBuyer ? buyerPubKey : sellerPubKey)
.op(OP_CHECKSIG);

return scriptBuilder.op(OP_ENDIF)
.build();
}
}

0 comments on commit 4938188

Please sign in to comment.