diff --git a/build.gradle b/build.gradle index b8e2519e20b..2b7c2853efd 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ configure(subprojects) { ext { // in alphabetical order bcVersion = '1.63' - bitcoinjVersion = 'a733034' + bitcoinjVersion = '60b4f2f' btcdCli4jVersion = '27b94333' codecVersion = '1.13' easybindVersion = '1.0.3' diff --git a/common/src/main/java/bisq/common/app/Version.java b/common/src/main/java/bisq/common/app/Version.java index 33efa828030..1b4ad6339b4 100644 --- a/common/src/main/java/bisq/common/app/Version.java +++ b/common/src/main/java/bisq/common/app/Version.java @@ -92,10 +92,13 @@ private static int getSubVersion(String version, int index) { // The version no. of the current protocol. The offer holds that version. // A taker will check the version of the offers to see if his version is compatible. - // Offers created with the old version will become invalid and have to be canceled. + // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. + // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening + // the Bisq app. // VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1 // Version 1.2.2 -> TRADE_PROTOCOL_VERSION = 2 - public static final int TRADE_PROTOCOL_VERSION = 2; + // Version 1.5.0 -> TRADE_PROTOCOL_VERSION = 3 + public static final int TRADE_PROTOCOL_VERSION = 3; private static int p2pMessageVersion; public static final String BSQ_TX_VERSION = "1"; diff --git a/core/src/main/java/bisq/core/app/WalletAppSetup.java b/core/src/main/java/bisq/core/app/WalletAppSetup.java index c5d8b839f6f..9851996a7df 100644 --- a/core/src/main/java/bisq/core/app/WalletAppSetup.java +++ b/core/src/main/java/bisq/core/app/WalletAppSetup.java @@ -107,7 +107,7 @@ void init(@Nullable Consumer chainFileLockedExceptionHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}", - VersionMessage.BITCOINJ_VERSION, "a733034"); + VersionMessage.BITCOINJ_VERSION, "60b4f2f"); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(), diff --git a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java index c0fbfb5b682..eeb5a545e2a 100644 --- a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java +++ b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java @@ -41,9 +41,23 @@ */ @Slf4j public class TxFeeEstimationService { - public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 260; - private static int DEPOSIT_TX_SIZE = 320; - private static int PAYOUT_TX_SIZE = 380; + +// Size/vsize of typical trade txs +// Real txs size/vsize may vary in 1 or 2 bytes from the estimated values. +// Values calculated with https://gist.github.com/oscarguindzberg/3d1349cb65d9fd9af9de0feaa3fd27ac +// legacy fee tx with 1 input, maker/taker fee paid in btc size/vsize = 258 +// legacy deposit tx without change size/vsize = 381 +// legacy deposit tx with change size/vsize = 414 +// legacy payout tx size/vsize = 337 +// legacy delayed payout tx size/vsize = 302 +// segwit fee tx with 1 input, maker/taker fee paid in btc vsize = 173 +// segwit deposit tx without change vsize = 232 +// segwit deposit tx with change vsize = 263 +// segwit payout tx vsize = 169 +// segwit delayed payout tx vsize = 139 + public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 175; + private static int DEPOSIT_TX_SIZE = 233; + private static int BSQ_INPUT_INCREASE = 150; private static int MAX_ITERATIONS = 10; @@ -87,14 +101,14 @@ private Tuple2 getEstimatedFeeAndTxSize(boolean isTaker, BtcWalletService btcWalletService, Preferences preferences) { Coin txFeePerByte = feeService.getTxFeePerByte(); - // We start with min taker fee size of 260 + // We start with min taker fee size of 175 int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE; try { estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, amount), estimatedTxSize, txFeePerByte, btcWalletService); } catch (InsufficientMoneyException e) { if (isTaker) { - // if we cannot do the estimation we use the payout tx size - estimatedTxSize = PAYOUT_TX_SIZE; + // If we cannot do the estimation, we use the size o the largest of our txs which is the deposit tx. + estimatedTxSize = DEPOSIT_TX_SIZE; } log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + "if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize); @@ -109,13 +123,13 @@ private Tuple2 getEstimatedFeeAndTxSize(boolean isTaker, Coin txFee; int size; if (isTaker) { - int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 320 bytes - // We use at least the size of the payout tx to not underpay at payout. - size = Math.max(PAYOUT_TX_SIZE, averageSize); + int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 233 bytes + // We use at least the size of the deposit tx to not underpay it. + size = Math.max(DEPOSIT_TX_SIZE, averageSize); txFee = txFeePerByte.multiply(size); log.info("Fee estimation resulted in a tx size of {} bytes.\n" + - "We use an average between the taker fee tx and the deposit tx (320 bytes) which results in {} bytes.\n" + - "The payout tx has 380 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" + + "We use an average between the taker fee tx and the deposit tx (233 bytes) which results in {} bytes.\n" + + "The deposit tx has 233 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" + "The tx fee of {} Sat", estimatedTxSize, averageSize, size, txFee.value); } else { size = estimatedTxSize; @@ -130,7 +144,7 @@ public Tuple2 getEstimatedFeeAndTxSize(Coin amount, FeeService feeService, BtcWalletService btcWalletService) { Coin txFeePerByte = feeService.getTxFeePerByte(); - // We start with min taker fee size of 260 + // We start with min taker fee size of 175 int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE; try { estimatedTxSize = getEstimatedTxSize(List.of(amount), estimatedTxSize, txFeePerByte, btcWalletService); @@ -145,7 +159,7 @@ public Tuple2 getEstimatedFeeAndTxSize(Coin amount, return new Tuple2<>(txFee, estimatedTxSize); } - // We start with the initialEstimatedTxSize for a tx with 1 input (260) bytes and get from BitcoinJ a tx back which + // We start with the initialEstimatedTxSize for a tx with 1 input (175) bytes and get from BitcoinJ a tx back which // contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on // the assumption that we only need 1 input. Once we receive back the real tx size from the tx BitcoinJ has created // with the required inputs we compare if the size is not more then 20% different to our assumed tx size. If we are inside diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntry.java b/core/src/main/java/bisq/core/btc/model/AddressEntry.java index a3a0387be2a..5348950b391 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntry.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntry.java @@ -97,10 +97,6 @@ public AddressEntry(@NotNull DeterministicKey keyPair, Context context, @Nullable String offerId, boolean segwit) { - if (segwit && (!Context.AVAILABLE.equals(context) || offerId != null)) { - throw new IllegalArgumentException("Segwit addresses are only allowed for " + - "AVAILABLE entries without an offerId"); - } this.keyPair = keyPair; this.context = context; this.offerId = offerId; diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 27bb49a5b6c..5a3b522d338 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -28,13 +28,14 @@ import bisq.core.user.Preferences; import bisq.common.handlers.ErrorMessageHandler; +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.InsufficientMoneyException; -import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; @@ -44,6 +45,7 @@ import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; @@ -228,7 +230,9 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur preferences.getIgnoreDustThreshold()); List preparedBsqTxInputs = preparedTx.getInputs(); List preparedBsqTxOutputs = preparedTx.getOutputs(); - int numInputs = preparedBsqTxInputs.size(); + Tuple2 numInputs = getNumInputs(preparedTx); + int numLegacyInputs = numInputs.first; + int numSegwitInputs = numInputs.second; Transaction resultTx = null; boolean isFeeOutsideTolerance; do { @@ -249,7 +253,10 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; - sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); + sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); + sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -262,9 +269,14 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur // add OP_RETURN output resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); - numInputs = resultTx.getInputs().size(); - txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; - long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; + numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txSizeWithUnsignedInputs = resultTx.getVsize(); + long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; + // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } @@ -338,7 +350,9 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, preferences.getIgnoreDustThreshold()); List preparedBsqTxInputs = preparedTx.getInputs(); List preparedBsqTxOutputs = preparedTx.getOutputs(); - int numInputs = preparedBsqTxInputs.size(); + Tuple2 numInputs = getNumInputs(preparedTx); + int numLegacyInputs = numInputs.first; + int numSegwitInputs = numInputs.second; Transaction resultTx = null; boolean isFeeOutsideTolerance; do { @@ -359,7 +373,9 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; - sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); + sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -372,9 +388,13 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, // add OP_RETURN output resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); - numInputs = resultTx.getInputs().size(); - txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; - final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; + numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txSizeWithUnsignedInputs = resultTx.getVsize(); + final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } @@ -479,7 +499,10 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, preferences.getIgnoreDustThreshold()); List preparedBsqTxInputs = preparedBsqTx.getInputs(); List preparedBsqTxOutputs = preparedBsqTx.getOutputs(); - int numInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input + // We don't know at this point what type the btc input would be (segwit/legacy). + // We use legacy to be on the safe side. + int numLegacyInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input + int numSegwitInputs = 0; Transaction resultTx = null; boolean isFeeOutsideTolerance; boolean opReturnIsOnlyOutput; @@ -508,7 +531,9 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; - sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); + sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -528,15 +553,19 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, if (opReturnData != null) resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); - numInputs = resultTx.getInputs().size(); - txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; - final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; + Tuple2 numInputs = getNumInputs(resultTx); + numLegacyInputs = numInputs.first; + numSegwitInputs = numInputs.second; + txSizeWithUnsignedInputs = resultTx.getVsize(); + final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } while (opReturnIsOnlyOutput || isFeeOutsideTolerance || - resultTx.getFee().value < txFeePerByte.multiply(resultTx.bitcoinSerialize().length).value); + resultTx.getFee().value < txFeePerByte.multiply(resultTx.getVsize()).value); // Sign all BTC inputs signAllBtcInputs(preparedBsqTxInputs.size(), resultTx); @@ -548,6 +577,25 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, return resultTx; } + private Tuple2 getNumInputs(Transaction tx) { + int numLegacyInputs = 0; + int numSegwitInputs = 0; + for (TransactionInput input : tx.getInputs()) { + TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) || + ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) { + // If connectedOutput is null, we don't know here the input type. To avoid underpaying fees, + // we treat it as a legacy input which will result in a higher fee estimation. + numLegacyInputs++; + } else if (ScriptPattern.isP2WPKH(connectedOutput.getScriptPubKey())) { + numSegwitInputs++; + } else { + throw new IllegalArgumentException("Inputs should spend a P2PKH, P2PK or P2WPKH ouput"); + } + } + return new Tuple2(numLegacyInputs, numSegwitInputs); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Commit tx @@ -579,18 +627,17 @@ public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context if (addressEntry.isPresent()) { return addressEntry.get(); } else { - // We still use non-segwit addresses for the trade protocol. // We try to use available and not yet used entries Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> AddressEntry.Context.AVAILABLE == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) - .filter(e -> Script.ScriptType.P2PKH.equals(e.getAddress().getOutputScriptType())) + .filter(e -> Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType())) .findAny(); if (emptyAvailableAddressEntry.isPresent()) { return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); } else { - DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); - AddressEntry entry = new AddressEntry(key, context, offerId, false); + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH)); + AddressEntry entry = new AddressEntry(key, context, offerId, true); addressEntryList.addAddressEntry(entry); return entry; } @@ -810,7 +857,7 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes ); log.info("newTransaction no. of inputs " + newTransaction.getInputs().size()); - log.info("newTransaction size in kB " + newTransaction.bitcoinSerialize().length / 1024); + log.info("newTransaction size in kB " + newTransaction.getVsize() / 1024); if (!newTransaction.getInputs().isEmpty()) { Coin amount = Coin.valueOf(newTransaction.getInputs().stream() @@ -840,7 +887,7 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes sendRequest.changeAddress = toAddress; wallet.completeTx(sendRequest); tx = sendRequest.tx; - txSize = tx.bitcoinSerialize().length; + txSize = tx.getVsize(); printTx("FeeEstimationTransaction", tx); sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString())); } @@ -948,7 +995,7 @@ public Transaction getFeeEstimationTransaction(String fromAddress, SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context); wallet.completeTx(sendRequest); tx = sendRequest.tx; - txSize = tx.bitcoinSerialize().length; + txSize = tx.getVsize(); printTx("FeeEstimationTransaction", tx); } while (feeEstimationNotSatisfied(counter, tx)); @@ -993,11 +1040,13 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f counter++; fee = txFeeForWithdrawalPerByte.multiply(txSize); // We use a dummy address for the output - final String dummyReceiver = LegacyAddress.fromKey(params, new ECKey()).toBase58(); + // We don't know here whether the output is segwit or not but we don't care too much because the size of + // a segwit ouput is just 3 byte smaller than the size of a legacy ouput. + final String dummyReceiver = SegwitAddress.fromKey(params, new ECKey()).toString(); SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey); wallet.completeTx(sendRequest); tx = sendRequest.tx; - txSize = tx.bitcoinSerialize().length; + txSize = tx.getVsize(); printTx("FeeEstimationTransactionForMultipleAddresses", tx); } while (feeEstimationNotSatisfied(counter, tx)); @@ -1013,7 +1062,7 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f } private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { - long targetFee = getTxFeeForWithdrawalPerByte().multiply(tx.bitcoinSerialize().length).value; + long targetFee = getTxFeeForWithdrawalPerByte().multiply(tx.getVsize()).value; return counter < 10 && (tx.getFee().value < targetFee || tx.getFee().value - targetFee > 1000); @@ -1022,7 +1071,9 @@ private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { public int getEstimatedFeeTxSize(List outputValues, Coin txFee) throws InsufficientMoneyException, AddressFormatException { Transaction transaction = new Transaction(params); - Address dummyAddress = LegacyAddress.fromKey(params, new ECKey()); + // In reality txs have a mix of segwit/legacy ouputs, but we don't care too much because the size of + // a segwit ouput is just 3 byte smaller than the size of a legacy ouput. + Address dummyAddress = SegwitAddress.fromKey(params, new ECKey()); outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress)); SendRequest sendRequest = SendRequest.forTx(transaction); @@ -1035,7 +1086,7 @@ public int getEstimatedFeeTxSize(List outputValues, Coin txFee) sendRequest.ensureMinRequiredFee = false; sendRequest.changeAddress = dummyAddress; wallet.completeTx(sendRequest); - return transaction.bitcoinSerialize().length; + return transaction.getVsize(); } 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 eb7b7485c10..46d9cea8bb4 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -36,8 +36,8 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.SignatureDecodeException; import org.bitcoinj.core.Transaction; @@ -336,7 +336,7 @@ OUT[0] dummyOutputAmount (inputAmount - tx fee) Transaction dummyTX = new Transaction(params); // The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything. // We don't care about fee calculation differences between the real tx and that dummy tx as we use a static tx fee. - TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, LegacyAddress.fromKey(params, new ECKey())); + TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, SegwitAddress.fromKey(params, new ECKey())); dummyTX.addOutput(dummyOutput); // Find the needed inputs to pay the output, optionally add 1 change output. @@ -455,7 +455,7 @@ private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuy // First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx. // Similar to the way we did in the createTakerDepositTxInputs method. Transaction dummyTx = new Transaction(params); - TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, LegacyAddress.fromKey(params, new ECKey())); + TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, SegwitAddress.fromKey(params, new ECKey())); dummyTx.addOutput(dummyOutput); addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress); // Normally we have only 1 input but we support multiple inputs if the user has paid in with several transactions. @@ -502,12 +502,12 @@ private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuy // Add MultiSig output - Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); + Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); // Tx fee for deposit tx will be paid by buyer. - TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount, - p2SHMultiSigOutputScript.getProgram()); - preparedDepositTx.addOutput(p2SHMultiSigOutput); + TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount, + hashedMultiSigOutputScript.getProgram()); + preparedDepositTx.addOutput(hashedMultiSigOutput); // We add the hash ot OP_RETURN with a 0 amount output TransactionOutput contractHashOutput = new TransactionOutput(params, preparedDepositTx, Coin.ZERO, @@ -587,9 +587,9 @@ public Transaction takerSignsDepositTx(boolean takerIsSeller, checkArgument(!sellerInputs.isEmpty()); // Check if maker's MultiSig script is identical to the takers - Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); - if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(p2SHMultiSigOutputScript)) { - throw new TransactionVerificationException("Maker's p2SHMultiSigOutputScript does not match to takers p2SHMultiSigOutputScript"); + Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); + if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(hashedMultiSigOutputScript)) { + throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match to takers hashedMultiSigOutputScript"); } // The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new @@ -601,7 +601,10 @@ public Transaction takerSignsDepositTx(boolean takerIsSeller, // We grab the signature from the makersDepositTx and apply it to the new tx input for (int i = 0; i < buyerInputs.size(); i++) { TransactionInput makersInput = makersDepositTx.getInputs().get(i); - byte[] makersScriptSigProgram = getMakersScriptSigProgram(makersInput); + 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)); if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) { input.setWitness(makersInput.getWitness()); @@ -692,11 +695,11 @@ public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, Coin minerFee, long lockTime) throws AddressFormatException, TransactionVerificationException { - TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction delayedPayoutTx = new Transaction(params); - delayedPayoutTx.addInput(p2SHMultiSigOutput); + delayedPayoutTx.addInput(hashedMultiSigOutput); applyLockTime(lockTime, delayedPayoutTx); - Coin outputAmount = p2SHMultiSigOutput.getValue().subtract(minerFee); + Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee); delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString)); WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx); WalletService.verifyTransaction(delayedPayoutTx); @@ -704,13 +707,17 @@ public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, } public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx, + Transaction preparedDepositTx, DeterministicKey myMultiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); - Sha256Hash sigHash = delayedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + Sha256Hash sigHash; + Coin delayedPayoutTxInputValue = preparedDepositTx.getOutput(0).getValue(); + sigHash = delayedPayoutTx.hashForWitnessSignature(0, redeemScript, + delayedPayoutTxInputValue, Transaction.SigHash.ALL, false); checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); if (myMultiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); @@ -733,9 +740,10 @@ public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx, 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); - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); TransactionInput input = delayedPayoutTx.getInput(0); - input.setScriptSig(inputScript); + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx); WalletService.verifyTransaction(delayedPayoutTx); WalletService.checkWalletConsistency(wallet); @@ -779,7 +787,15 @@ public byte[] buyerSignsPayoutTx(Transaction depositTx, // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 - Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + Sha256Hash sigHash; + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + if (ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey())) { + sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); if (multiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); @@ -822,7 +838,16 @@ public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 - Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); if (multiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); @@ -832,10 +857,16 @@ public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, Transaction.SigHash.ALL, false); TransactionSignature sellerTxSig = new TransactionSignature(sellerSignature, Transaction.SigHash.ALL, false); // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer) - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), - redeemScript); TransactionInput input = payoutTx.getInput(0); - input.setScriptSig(inputScript); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } WalletService.printTx("payoutTx", payoutTx); WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); @@ -863,7 +894,16 @@ public byte[] signMediatedPayoutTx(Transaction depositTx, // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 - Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); if (myMultiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); @@ -895,9 +935,18 @@ public Transaction finalizeMediatedPayoutTx(Transaction depositTx, TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature), Transaction.SigHash.ALL, false); // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer) - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); + boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); TransactionInput input = payoutTx.getInput(0); - input.setScriptSig(inputScript); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } WalletService.printTx("mediated payoutTx", payoutTx); WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); @@ -945,9 +994,9 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali byte[] arbitratorPubKey) throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { Transaction depositTx = new Transaction(params, depositTxSerialized); - TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction payoutTx = new Transaction(params); - payoutTx.addInput(p2SHMultiSigOutput); + payoutTx.addInput(hashedMultiSigOutput); if (buyerPayoutAmount.isPositive()) { payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } @@ -957,7 +1006,15 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali // take care of sorting! Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); - Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + Sha256Hash sigHash; + boolean hashedMultiSigOutputIsLegacy = !ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = hashedMultiSigOutput.getValue(); + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } checkNotNull(tradersMultiSigKeyPair, "tradersMultiSigKeyPair must not be null"); if (tradersMultiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); @@ -966,11 +1023,18 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false); TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature), Transaction.SigHash.ALL, false); - // Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer) - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig), - redeemScript); TransactionInput input = payoutTx.getInput(0); - input.setScriptSig(inputScript); + // Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer) + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript( + ImmutableList.of(arbitratorTxSig, tradersTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, arbitratorTxSig, tradersTxSig); + input.setWitness(witness); + } WalletService.printTx("disputed payoutTx", payoutTx); WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); @@ -995,21 +1059,23 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, String sellerPrivateKeyAsHex, String buyerPubKeyAsHex, String sellerPubKeyAsHex, + boolean hashedMultiSigOutputIsLegacy, TxBroadcaster.Callback callback) throws AddressFormatException, TransactionVerificationException, WalletException { byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); - Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); + Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, + hashedMultiSigOutputIsLegacy); - Coin msOutput = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); - TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, null, msOutput, p2SHMultiSigOutputScript.getProgram()); + Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); + TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, null, msOutputValue, hashedMultiSigOutputScript.getProgram()); Transaction depositTx = new Transaction(params); - depositTx.addOutput(p2SHMultiSigOutput); + depositTx.addOutput(hashedMultiSigOutput); Transaction payoutTx = new Transaction(params); Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex); - payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput)); + payoutTx.addInput(new TransactionInput(params, depositTx, null, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue)); if (buyerPayoutAmount.isPositive()) { payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); @@ -1020,7 +1086,14 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, // take care of sorting! Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); - Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + Sha256Hash sigHash; + if (hashedMultiSigOutputIsLegacy) { + sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + } else { + Coin inputValue = msOutputValue; + sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, + inputValue, Transaction.SigHash.ALL, false); + } ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex)); checkNotNull(buyerPrivateKey, "key must not be null"); @@ -1032,10 +1105,18 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false); TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false); - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); TransactionInput input = payoutTx.getInput(0); - input.setScriptSig(inputScript); + if (hashedMultiSigOutputIsLegacy) { + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); + input.setScriptSig(inputScript); + } else { + input.setScriptSig(ScriptBuilder.createEmpty()); + TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); + input.setWitness(witness); + } + WalletService.printTx("payoutTx", payoutTx); WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); @@ -1092,20 +1173,15 @@ private RawTransactionInput getRawInputFromTransactionInput(@NotNull Transaction "input.getConnectedOutput().getParentTransaction() must not be null"); checkNotNull(input.getValue(), "input.getValue() must not be null"); + // bitcoinSerialize(false) is used just in case the serialized tx is parsed by a bisq node still using + // bitcoinj 0.14. This is not supposed to happen ever since Version.TRADE_PROTOCOL_VERSION was set to 3, + // but it costs nothing to be on the safe side. + // The serialized tx is just used to obtain its hash, so the witness data is not relevant. return new RawTransactionInput(input.getOutpoint().getIndex(), input.getConnectedOutput().getParentTransaction().bitcoinSerialize(false), input.getValue().value); } - private byte[] getMakersScriptSigProgram(TransactionInput transactionInput) throws TransactionVerificationException { - byte[] scriptProgram = transactionInput.getScriptSig().getProgram(); - if (scriptProgram.length == 0) { - throw new TransactionVerificationException("Inputs from maker not signed."); - } - - return scriptProgram; - } - private TransactionInput getTransactionInput(Transaction depositTx, byte[] scriptProgram, RawTransactionInput rawTransactionInput) { @@ -1144,8 +1220,13 @@ private Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubK return ScriptBuilder.createMultiSigOutputScript(2, keys); } - private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey) { - return ScriptBuilder.createP2SHOutputScript(get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey)); + private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) { + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + if (legacy) { + return ScriptBuilder.createP2SHOutputScript(redeemScript); + } else { + return ScriptBuilder.createP2WSHOutputScript(redeemScript); + } } private Transaction createPayoutTx(Transaction depositTx, @@ -1153,9 +1234,9 @@ private Transaction createPayoutTx(Transaction depositTx, Coin sellerPayoutAmount, String buyerAddressString, String sellerAddressString) throws AddressFormatException { - TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); + TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction transaction = new Transaction(params); - transaction.addInput(p2SHMultiSigOutput); + transaction.addInput(hashedMultiSigOutput); if (buyerPayoutAmount.isPositive()) { transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } @@ -1187,11 +1268,8 @@ private void signInput(Transaction transaction, TransactionInput input, int inpu input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey)); } } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { - // TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master) - // Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey) - Script scriptCode = new ScriptBuilder().data( - ScriptBuilder.createOutputScript(LegacyAddress.fromKey(transaction.getParams(), sigKey)).getProgram()) - .build(); + // scriptCode is expected to have the format of a legacy P2PKH output script + Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey); Coin value = input.getValue(); TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, scriptCode, value, Transaction.SigHash.ALL, false); diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index 251aac97172..fa285a3e557 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -37,7 +37,6 @@ import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; @@ -325,11 +324,8 @@ public static void signTransactionInput(Wallet wallet, } } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { try { - // TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master) - // Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key); - Script scriptCode = new ScriptBuilder().data( - ScriptBuilder.createOutputScript(LegacyAddress.fromKey(tx.getParams(), key)).getProgram()) - .build(); + // scriptCode is expected to have the format of a legacy P2PKH output script + Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key); Coin value = txIn.getValue(); TransactionSignature txSig = tx.calculateWitnessSignature(index, key, scriptCode, value, Transaction.SigHash.ALL, false); diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java index cee5d1b9174..996ff207bc3 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -195,7 +195,7 @@ public Tuple2 getMiningFeeAndTxSize(Coin stake) Coin blindVoteFee = BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight()); Transaction dummyTx = getBlindVoteTx(stake, blindVoteFee, new byte[22]); Coin miningFee = dummyTx.getFee(); - int txSize = dummyTx.bitcoinSerialize().length; + int txSize = dummyTx.getVsize(); return new Tuple2<>(miningFee, txSize); } diff --git a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java index 4ffa4772e9e..9a61c09be8d 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/lockup/LockupTxService.java @@ -95,7 +95,7 @@ public Tuple2 getMiningFeeAndTxSize(Coin lockupAmount, int lockTi throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException { Transaction tx = getLockupTx(lockupAmount, lockTime, lockupReason, hash); Coin miningFee = tx.getFee(); - int txSize = tx.bitcoinSerialize().length; + int txSize = tx.getVsize(); return new Tuple2<>(miningFee, txSize); } diff --git a/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java index 0682186d2ec..fa4272d7b5c 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/unlock/UnlockTxService.java @@ -93,7 +93,7 @@ public Tuple2 getMiningFeeAndTxSize(String lockupTxId) throws InsufficientMoneyException, WalletException, TransactionVerificationException { Transaction tx = getUnlockTx(lockupTxId); Coin miningFee = tx.getFee(); - int txSize = tx.bitcoinSerialize().length; + int txSize = tx.getVsize(); return new Tuple2<>(miningFee, txSize); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java index 2008b92b40a..32748a319c3 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java @@ -24,6 +24,7 @@ import bisq.common.taskrunner.TaskRunner; +import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.DeterministicKey; @@ -46,7 +47,11 @@ protected void run() { runInterceptHook(); Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + NetworkParameters params = btcWalletService.getParams(); + Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx()); + String id = processModel.getOffer().getId(); byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); @@ -58,6 +63,7 @@ protected void run() { byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx( preparedDelayedPayoutTx, + preparedDepositTx, myMultiSigKeyPair, buyerMultiSigPubKey, sellerMultiSigPubKey); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java index 0ae1a159201..556e556b418 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java @@ -24,6 +24,7 @@ import bisq.common.taskrunner.TaskRunner; +import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.DeterministicKey; @@ -47,6 +48,9 @@ protected void run() { Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); BtcWalletService btcWalletService = processModel.getBtcWalletService(); + NetworkParameters params = btcWalletService.getParams(); + Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx()); + String id = processModel.getOffer().getId(); byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); @@ -59,6 +63,7 @@ protected void run() { byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx( preparedDelayedPayoutTx, + preparedDepositTx, myMultiSigKeyPair, buyerMultiSigPubKey, sellerMultiSigPubKey); diff --git a/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java b/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java index a71fb8237ba..ca7076bf290 100644 --- a/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java +++ b/core/src/test/java/bisq/core/btc/TxFeeEstimationServiceTest.java @@ -45,14 +45,14 @@ public void testGetEstimatedTxSize_withDefaultTxSize() throws InsufficientMoneyE int realTxSize; Coin txFee; - initialEstimatedTxSize = 260; + initialEstimatedTxSize = 175; txFeePerByte = Coin.valueOf(10); - realTxSize = 260; + realTxSize = 175; txFee = txFeePerByte.multiply(initialEstimatedTxSize); when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); - assertEquals(260, result); + assertEquals(175, result); } // FIXME @Bernard could you have a look? @@ -67,16 +67,16 @@ public void testGetEstimatedTxSize_withLargeTx() throws InsufficientMoneyExcepti int realTxSize; Coin txFee; - initialEstimatedTxSize = 260; + initialEstimatedTxSize = 175; txFeePerByte = Coin.valueOf(10); - realTxSize = 2600; + realTxSize = 1750; txFee = txFeePerByte.multiply(initialEstimatedTxSize); when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); // repeated calls to getEstimatedFeeTxSize do not work (returns 0 at second call in loop which cause test to fail) result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); - assertEquals(2600, result); + assertEquals(1750, result); } // FIXME @Bernard could you have a look? @@ -91,14 +91,14 @@ public void testGetEstimatedTxSize_withSmallTx() throws InsufficientMoneyExcepti int realTxSize; Coin txFee; - initialEstimatedTxSize = 2600; + initialEstimatedTxSize = 1750; txFeePerByte = Coin.valueOf(10); - realTxSize = 260; + realTxSize = 175; txFee = txFeePerByte.multiply(initialEstimatedTxSize); when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); - assertEquals(260, result); + assertEquals(175, result); } @Test diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java index d873ed60d79..f2f2e9b14e3 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -189,7 +189,7 @@ protected void activate() { try { Transaction transaction = assetService.payFee(selectedAsset, listingFee.value); Coin miningFee = transaction.getFee(); - int txSize = transaction.bitcoinSerialize().length; + int txSize = transaction.getVsize(); if (!DevEnv.isDevMode()) { GUIUtil.showBsqFeeInfoPopup(listingFee, miningFee, txSize, bsqFormatter, btcFormatter, diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java index d9b5d7c2813..b8ea8553b3b 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java @@ -172,7 +172,7 @@ protected void activate() { String preImageAsString = preImageTextField.getText(); Transaction transaction = proofOfBurnService.burn(preImageAsString, amount.value); Coin miningFee = transaction.getFee(); - int txSize = transaction.bitcoinSerialize().length; + int txSize = transaction.getVsize(); if (!DevEnv.isDevMode()) { GUIUtil.showBsqFeeInfoPopup(amount, miningFee, txSize, bsqFormatter, btcFormatter, diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java index 89372c93931..8ec6ed6d28b 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -283,7 +283,7 @@ private void publishMyProposal(ProposalType type) { Proposal proposal = proposalWithTransaction.getProposal(); Transaction transaction = proposalWithTransaction.getTransaction(); Coin miningFee = transaction.getFee(); - int txSize = transaction.bitcoinSerialize().length; + int txSize = transaction.getVsize(); Coin fee = daoFacade.getProposalFee(daoFacade.getChainHeight()); if (type.equals(ProposalType.BONDED_ROLE)) { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index 0c8a6cd8418..06861f3002f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -243,7 +243,7 @@ private void addSendBsqGroup() { Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); Coin miningFee = signedTx.getFee(); - int txSize = signedTx.bitcoinSerialize().length; + int txSize = signedTx.getVsize(); showPublishTxPopup(receiverAmount, txWithBtcFee, TxType.TRANSFER_BSQ, @@ -305,7 +305,7 @@ private void addSendBtcGroup() { if (miningFee.getValue() >= receiverAmount.getValue()) GUIUtil.showWantToBurnBTCPopup(miningFee, receiverAmount, btcFormatter); else { - int txSize = signedTx.bitcoinSerialize().length; + int txSize = signedTx.getVsize(); showPublishTxPopup(receiverAmount, txWithBtcFee, TxType.INVALID, diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index 8d3620c67e6..47a5ba31514 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -356,7 +356,7 @@ private void onWithdraw() { } if (areInputsValid()) { - int txSize = feeEstimationTransaction.bitcoinSerialize().length; + int txSize = feeEstimationTransaction.getVsize(); log.info("Fee for tx with size {}: {} " + Res.getBaseCurrencyCode() + "", txSize, fee.toPlainString()); if (receiverAmount.isPositive()) { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 399f45bbef3..bbc060ea0e6 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -109,8 +109,8 @@ class TakeOfferDataModel extends OfferDataModel { private PaymentAccount paymentAccount; private boolean isTabSelected; Price tradePrice; - // 260 kb is size of typical trade fee tx with 1 input but trade tx (deposit and payout) are larger so we adjust to 320 - private int feeTxSize = 320; + // Use an average of a typical trade fee tx with 1 input, deposit tx and payout tx. + private int feeTxSize = 192; // (175+233+169)/3 private boolean freezeFee; private Coin txFeePerByteFromFeeService; @@ -213,13 +213,13 @@ void initWithData(Offer offer) { // multiple batch-signed payout tx with different fees might be an option but RBF is not supported yet in BitcoinJ // and batched txs would add more complexity to the trade protocol. - // A typical trade fee tx has about 260 bytes (if one input). The trade txs has about 336-414 bytes. - // We use 320 as a average value. + // A typical trade fee tx has about 175 bytes (if one input). The trade txs has about 169-263 bytes. + // We use 192 as average value. - // trade fee tx: 260 bytes (1 input) - // deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) - // payout tx: 371 bytes - // disputed payout tx: 408 bytes + // trade fee tx: 175 bytes (1 input) + // deposit tx: 233 bytes (1 MS output+ OP_RETURN) - 263 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) + // payout tx: 169 bytes + // disputed payout tx: 139 bytes // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values) // But the "take offer" happens usually after that so we should have already the value from the estimation service. @@ -351,22 +351,22 @@ public void estimateTxSize() { // there more deterministic. // The trade fee tx can be in the worst case very large if there are many inputs so if we take that tx alone // for the fee estimation we would overpay a lot. - // On the other side if we have the best case of a 1 input tx fee tx then it is only 260 bytes but the - // other 2 txs are larger (320 and 380 bytes) and would get a lower fee/byte as intended. + // On the other side if we have the best case of a 1 input tx fee tx then it is only 175 bytes but the + // other 2 txs are different (233 and 169 bytes) and may get a lower fee/byte as intended. // We apply following model to not overpay too much but be on the safe side as well. // We sum the taker fee tx and the deposit tx together as it can be assumed that both be in the same block and // as they are dependent txs the miner will pick both if the fee in total is good enough. - // We make sure that the fee is sufficient to meet our intended fee/byte for the larger payout tx with 380 bytes. + // We make sure that the fee is sufficient to meet our intended fee/byte for the larger deposit tx with 233 bytes. Tuple2 estimatedFeeAndTxSize = txFeeEstimationService.getEstimatedFeeAndTxSizeForTaker(fundsNeededForTrade, getTakerFee()); txFeeFromFeeService = estimatedFeeAndTxSize.first; feeTxSize = estimatedFeeAndTxSize.second; } else { - feeTxSize = 380; + feeTxSize = 233; txFeeFromFeeService = txFeePerByteFromFeeService.multiply(feeTxSize); log.info("We cannot do the fee estimation because there are no funds in the wallet.\nThis is expected " + "if the user has not funded their wallet yet.\n" + - "In that case we use an estimated tx size of 380 bytes.\n" + + "In that case we use an estimated tx size of 233 bytes.\n" + "txFee based on estimated size of {} bytes. feeTxSize = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)", feeTxSize, feeTxSize, txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); } @@ -531,7 +531,7 @@ public void swapTradeToSavings() { // With that we avoid that we overpay in case that the trade fee has many inputs and we would apply that fee for the // other 2 txs as well. We still might overpay a bit for the payout tx. private int getAverageSize(int txSize) { - return (txSize + 320) / 2; + return (txSize + 233) / 2; } private Coin getTxFeeBySize(int sizeInBytes) { @@ -606,7 +606,7 @@ private Coin getTxFeeForDepositTx() { // Unfortunately we cannot change that to the correct fees as it would break backward compatibility // We still might find a way with offer version or app version checks so lets keep that commented out // code as that shows how it should be. - return txFeeFromFeeService; //feeService.getTxFee(320); + return txFeeFromFeeService; //feeService.getTxFee(233); } private Coin getTxFeeForPayoutTx() { @@ -614,7 +614,7 @@ private Coin getTxFeeForPayoutTx() { // Unfortunately we cannot change that to the correct fees as it would break backward compatibility // We still might find a way with offer version or app version checks so lets keep that commented out // code as that shows how it should be. - return txFeeFromFeeService; //feeService.getTxFee(380); + return txFeeFromFeeService; //feeService.getTxFee(169); } public AddressEntry getAddressEntry() { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index 6b89048980b..609c526fdec 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -40,6 +40,7 @@ import javax.inject.Inject; import javafx.scene.Scene; +import javafx.scene.control.CheckBox; import javafx.scene.input.KeyCode; import org.slf4j.Logger; @@ -47,6 +48,7 @@ import javax.annotation.Nullable; +import static bisq.desktop.util.FormBuilder.addCheckBox; import static bisq.desktop.util.FormBuilder.addInputTextField; // We don't translate here as it is for dev only purpose @@ -116,6 +118,8 @@ private void addContent() { InputTextField buyerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "buyerPubKeyAsHex"); InputTextField sellerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "sellerPubKeyAsHex"); + CheckBox depositTxLegacy = addCheckBox(gridPane, ++rowIndex, "depositTxLegacy"); + // Notes: // Open with alt+g // Priv key is only visible if pw protection is removed (wallet details data (alt+j)) @@ -136,6 +140,9 @@ private void addContent() { sellerPubKeyAsHex.setText(""); sellerPrivateKeyAsHex.setText(""); + depositTxLegacy.setAllowIndeterminate(false); + depositTxLegacy.setSelected(false); + actionButtonText("Sign and publish transaction"); TxBroadcaster.Callback callback = new TxBroadcaster.Callback() { @@ -167,6 +174,7 @@ public void onFailure(TxBroadcastException exception) { sellerPrivateKeyAsHex.getText(), buyerPubKeyAsHex.getText(), sellerPubKeyAsHex.getText(), + depositTxLegacy.isSelected(), callback); } catch (AddressFormatException | WalletException | TransactionVerificationException e) { log.error(e.toString()); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index 55378fd5675..e01b1e0b911 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -205,7 +205,7 @@ private void reviewWithdrawal() { validateWithdrawAddress(); } else if (Restrictions.isAboveDust(receiverAmount)) { CoinFormatter formatter = model.btcFormatter; - int txSize = feeEstimationTransaction.bitcoinSerialize().length; + int txSize = feeEstimationTransaction.getVsize(); double feePerByte = CoinUtil.getFeePerByte(fee, txSize); double kb = txSize / 1000d; String recAmount = formatter.formatCoinWithCode(receiverAmount); diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 8aff7277f1e..339f6bb1064 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -23,7 +23,7 @@ dependencyVerification { 'com.github.bisq-network.netlayer:tor.external:a3606a592d6b6caa6a2fb7db224eaf43c6874c6730da4815bd37ad686b283dcb', 'com.github.bisq-network.netlayer:tor.native:b15aba7fe987185037791c7ec7c529cb001b90d723d047d54aab87aceb3b3d45', 'com.github.bisq-network.netlayer:tor:a974190aa3a031067ccd1dda28a3ae58cad14060792299d86ea38a05fb21afc5', - 'com.github.bisq-network:bitcoinj:b8b6e4b8010f2b8d4aac7141c0809dea6d102c3ff3c06ceba78c2626d531b0af', + 'com.github.bisq-network:bitcoinj:804f587a44b1ce9cd9b8dd1848fc911329634163b7905bafb86f502a3d08264c', 'com.github.cd2357.tor-binary:tor-binary-geoip:ae27b6aca1a3a50a046eb11e38202b6d21c2fcd2b8643bbeb5ea85e065fbc1be', 'com.github.cd2357.tor-binary:tor-binary-linux32:7b5d6770aa442ef6d235e8a9bfbaa7c62560690f9fe69ff03c7a752eae84f7dc', 'com.github.cd2357.tor-binary:tor-binary-linux64:24111fa35027599a750b0176392dc1e9417d919414396d1b221ac2e707eaba76',