diff --git a/build.gradle b/build.gradle index fe986909e55..56cdb1cc792 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ configure(subprojects) { ext { // in alphabetical order bcVersion = '1.63' - bitcoinjVersion = '44ddbdc' + bitcoinjVersion = 'a733034' btcdCli4jVersion = '27b94333' codecVersion = '1.13' easybindVersion = '1.0.3' diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 6027f7f892a..3660544f60d 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -389,6 +389,8 @@ private void initWallet() { if (requestWalletPasswordHandler != null) { requestWalletPasswordHandler.accept(aesKey -> { walletsManager.setAesKey(aesKey); + walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(), + aesKey); if (preferences.isResyncSpvRequested()) { if (showFirstPopupIfResyncSPVRequestedHandler != null) showFirstPopupIfResyncSPVRequestedHandler.run(); diff --git a/core/src/main/java/bisq/core/app/WalletAppSetup.java b/core/src/main/java/bisq/core/app/WalletAppSetup.java index 74049ae6376..c9b276b2948 100644 --- a/core/src/main/java/bisq/core/app/WalletAppSetup.java +++ b/core/src/main/java/bisq/core/app/WalletAppSetup.java @@ -105,7 +105,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, "44ddbdc"); + VersionMessage.BITCOINJ_VERSION, "a733034"); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(), 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 3b036b47a43..a3a0387be2a 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntry.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntry.java @@ -26,8 +26,8 @@ import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.script.Script; import java.util.Optional; @@ -74,6 +74,9 @@ public enum Context { private long coinLockedInMultiSig; + @Getter + private boolean segwit; + @Nullable transient private DeterministicKey keyPair; @Nullable @@ -86,18 +89,24 @@ public enum Context { // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// - public AddressEntry(DeterministicKey keyPair, Context context) { - this(keyPair, context, null); + public AddressEntry(DeterministicKey keyPair, Context context, boolean segwit) { + this(keyPair, context, null, segwit); } public AddressEntry(@NotNull DeterministicKey keyPair, Context context, - @Nullable String offerId) { + @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; pubKey = keyPair.getPubKey(); pubKeyHash = keyPair.getPubKeyHash(); + this.segwit = segwit; } @@ -109,12 +118,14 @@ private AddressEntry(byte[] pubKey, byte[] pubKeyHash, Context context, @Nullable String offerId, - Coin coinLockedInMultiSig) { + Coin coinLockedInMultiSig, + boolean segwit) { this.pubKey = pubKey; this.pubKeyHash = pubKeyHash; this.context = context; this.offerId = offerId; this.coinLockedInMultiSig = coinLockedInMultiSig.value; + this.segwit = segwit; } public static AddressEntry fromProto(protobuf.AddressEntry proto) { @@ -122,7 +133,8 @@ public static AddressEntry fromProto(protobuf.AddressEntry proto) { proto.getPubKeyHash().toByteArray(), ProtoUtil.enumFromProto(AddressEntry.Context.class, proto.getContext().name()), ProtoUtil.stringOrNullFromProto(proto.getOfferId()), - Coin.valueOf(proto.getCoinLockedInMultiSig())); + Coin.valueOf(proto.getCoinLockedInMultiSig()), + proto.getSegwit()); } @Override @@ -131,7 +143,8 @@ public protobuf.AddressEntry toProtoMessage() { .setPubKey(ByteString.copyFrom(pubKey)) .setPubKeyHash(ByteString.copyFrom(pubKeyHash)) .setContext(protobuf.AddressEntry.Context.valueOf(context.name())) - .setCoinLockedInMultiSig(coinLockedInMultiSig); + .setCoinLockedInMultiSig(coinLockedInMultiSig) + .setSegwit(segwit); Optional.ofNullable(offerId).ifPresent(builder::setOfferId); return builder.build(); } @@ -175,7 +188,7 @@ public String getAddressString() { @Nullable public Address getAddress() { if (address == null && keyPair != null) - address = LegacyAddress.fromKey(Config.baseCurrencyNetworkParameters(), keyPair); + address = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyPair, segwit ? Script.ScriptType.P2WPKH : Script.ScriptType.P2PKH); return address; } @@ -198,6 +211,7 @@ public String toString() { ", context=" + context + ", offerId='" + offerId + '\'' + ", coinLockedInMultiSig=" + coinLockedInMultiSig + + ", segwit=" + segwit + "}"; } } diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index 4827fbc1621..c88ebc40fbd 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -25,7 +25,7 @@ import com.google.protobuf.Message; import org.bitcoinj.core.Address; -import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.script.Script; @@ -35,6 +35,8 @@ import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.tuple.Pair; + import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -107,11 +109,13 @@ public void onWalletReady(Wallet wallet) { if (!entrySet.isEmpty()) { Set toBeRemoved = new HashSet<>(); entrySet.forEach(addressEntry -> { + Script.ScriptType scriptType = addressEntry.isSegwit() ? Script.ScriptType.P2WPKH + : Script.ScriptType.P2PKH; DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash( - addressEntry.getPubKeyHash(), - Script.ScriptType.P2PKH); + addressEntry.getPubKeyHash(), scriptType); if (keyFromPubHash != null) { - Address addressFromKey = LegacyAddress.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash); + Address addressFromKey = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash, + scriptType); // We want to ensure key and address matches in case we have address in entry available already if (addressEntry.getAddress() == null || addressFromKey.equals(addressEntry.getAddress())) { addressEntry.setDeterministicKey(keyFromPubHash); @@ -133,7 +137,8 @@ public void onWalletReady(Wallet wallet) { toBeRemoved.forEach(entrySet::remove); } else { // As long the old arbitration domain is not removed from the code base we still support it here. - entrySet.add(new AddressEntry(wallet.freshReceiveKey(), AddressEntry.Context.ARBITRATOR)); + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); + entrySet.add(new AddressEntry(key, AddressEntry.Context.ARBITRATOR, false)); } // In case we restore from seed words and have balance we need to add the relevant addresses to our list. @@ -147,7 +152,7 @@ public void onWalletReady(Wallet wallet) { DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address); if (key != null) { // Address will be derived from key in getAddress method - entrySet.add(new AddressEntry(key, AddressEntry.Context.AVAILABLE)); + entrySet.add(new AddressEntry(key, AddressEntry.Context.AVAILABLE, address instanceof SegwitAddress)); } }); } @@ -192,7 +197,8 @@ public void addAddressEntry(AddressEntry addressEntry) { public void swapToAvailable(AddressEntry addressEntry) { boolean setChangedByRemove = entrySet.remove(addressEntry); boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), - AddressEntry.Context.AVAILABLE)); + AddressEntry.Context.AVAILABLE, + addressEntry.isSegwit())); if (setChangedByRemove || setChangedByAdd) { requestPersistence(); } @@ -202,7 +208,7 @@ public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressE AddressEntry.Context context, String offerId) { boolean setChangedByRemove = entrySet.remove(addressEntry); - final AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId); + final AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId, addressEntry.isSegwit()); boolean setChangedByAdd = entrySet.add(newAddressEntry); if (setChangedByRemove || setChangedByAdd) requestPersistence(); @@ -225,10 +231,10 @@ private void maybeAddNewAddressEntry(Transaction tx) { .map(output -> output.getScriptPubKey().getToAddress(wallet.getNetworkParameters())) .filter(Objects::nonNull) .filter(this::isAddressNotInEntries) - .map(address -> (DeterministicKey) wallet.findKeyFromPubKeyHash(address.getHash(), - Script.ScriptType.P2PKH)) - .filter(Objects::nonNull) - .map(deterministicKey -> new AddressEntry(deterministicKey, AddressEntry.Context.AVAILABLE)) + .map(address -> Pair.of(address, (DeterministicKey) wallet.findKeyFromAddress(address))) + .filter(pair -> pair.getRight() != null) + .map(pair -> new AddressEntry(pair.getRight(), AddressEntry.Context.AVAILABLE, + pair.getLeft() instanceof SegwitAddress)) .forEach(this::addAddressEntry); } diff --git a/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java index 7c7f6a4e3a8..66ec8b7cea2 100644 --- a/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java +++ b/core/src/main/java/bisq/core/btc/setup/BisqKeyChainGroupStructure.java @@ -47,10 +47,11 @@ public class BisqKeyChainGroupStructure implements KeyChainGroupStructure { new ChildNumber(142, true), ChildNumber.ZERO_HARDENED); - public static final ImmutableList BIP44_BSQ_SEGWIT_ACCOUNT_PATH = ImmutableList.of( - new ChildNumber(44, true), - new ChildNumber(142, true), - ChildNumber.ONE_HARDENED); + // We don't use segwit for BSQ + // public static final ImmutableList BIP44_BSQ_SEGWIT_ACCOUNT_PATH = ImmutableList.of( + // new ChildNumber(44, true), + // new ChildNumber(142, true), + // ChildNumber.ONE_HARDENED); private boolean isBsqWallet; @@ -71,7 +72,8 @@ else if (outputScriptType == Script.ScriptType.P2WPKH) if (outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH) return BIP44_BSQ_NON_SEGWIT_ACCOUNT_PATH; else if (outputScriptType == Script.ScriptType.P2WPKH) - return BIP44_BSQ_SEGWIT_ACCOUNT_PATH; + //return BIP44_BSQ_SEGWIT_ACCOUNT_PATH; + throw new IllegalArgumentException(outputScriptType.toString()); else throw new IllegalArgumentException(outputScriptType.toString()); } diff --git a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java index acc33637023..fc8c6a09506 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java @@ -22,11 +22,13 @@ import bisq.core.btc.wallet.BisqRiskAnalysis; import bisq.common.config.Config; +import bisq.common.file.FileUtil; import com.google.common.io.Closeables; import com.google.common.util.concurrent.*; import org.bitcoinj.core.listeners.*; import org.bitcoinj.core.*; +import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.net.BlockingClientManager; import org.bitcoinj.net.discovery.*; import org.bitcoinj.script.Script; @@ -35,6 +37,11 @@ import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import org.bouncycastle.crypto.params.KeyParameter; + import org.slf4j.*; import javax.annotation.*; @@ -103,6 +110,8 @@ public class WalletConfig extends AbstractIdleService { @Getter @Setter private int minBroadcastConnections; + @Getter + private BooleanProperty migratedWalletToSegwit = new SimpleBooleanProperty(false); /** * Creates a new WalletConfig, with a newly created {@link Context}. Files will be stored in the given directory. @@ -293,25 +302,38 @@ protected void startUp() throws Exception { vPeerGroup.addWallet(vBsqWallet); onSetupCompleted(); - Futures.addCallback((ListenableFuture) vPeerGroup.startAsync(), new FutureCallback() { - @Override - public void onSuccess(@Nullable Object result) { - //completeExtensionInitiations(vPeerGroup); - DownloadProgressTracker tracker = downloadListener == null ? new DownloadProgressTracker() : downloadListener; - vPeerGroup.startBlockChainDownload(tracker); - } - - @Override - public void onFailure(Throwable t) { - throw new RuntimeException(t); + if (migratedWalletToSegwit.get()) { + startPeerGroup(); + } else { + migratedWalletToSegwit.addListener((observable, oldValue, newValue) -> { + if (newValue) { + startPeerGroup(); + } + }); + } - } - }, MoreExecutors.directExecutor()); } catch (BlockStoreException e) { throw new IOException(e); } } + private void startPeerGroup() { + Futures.addCallback((ListenableFuture) vPeerGroup.startAsync(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Object result) { + //completeExtensionInitiations(vPeerGroup); + DownloadProgressTracker tracker = downloadListener == null ? new DownloadProgressTracker() : downloadListener; + vPeerGroup.startBlockChainDownload(tracker); + } + + @Override + public void onFailure(Throwable t) { + throw new RuntimeException(t); + + } + }, MoreExecutors.directExecutor()); + } + private Wallet createOrLoadWallet(boolean shouldReplayWallet, File walletFile, boolean isBsqWallet) throws Exception { Wallet wallet; @@ -321,7 +343,7 @@ private Wallet createOrLoadWallet(boolean shouldReplayWallet, File walletFile, b wallet = loadWallet(shouldReplayWallet, walletFile, isBsqWallet); } else { wallet = createWallet(isBsqWallet); - wallet.freshReceiveKey(); + //wallet.freshReceiveKey(); // Currently the only way we can be sure that an extension is aware of its containing wallet is by // deserializing the extension (see WalletExtension#deserializeWalletExtension(Wallet, byte[])) @@ -341,8 +363,7 @@ protected void setupAutoSave(Wallet wallet, File walletFile) { private Wallet loadWallet(boolean shouldReplayWallet, File walletFile, boolean isBsqWallet) throws Exception { Wallet wallet; - FileInputStream walletStream = new FileInputStream(walletFile); - try { + try (FileInputStream walletStream = new FileInputStream(walletFile)) { WalletExtension[] extArray = new WalletExtension[]{}; Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream); final WalletProtobufSerializer serializer; @@ -352,16 +373,15 @@ private Wallet loadWallet(boolean shouldReplayWallet, File walletFile, boolean i wallet = serializer.readWallet(params, extArray, proto); if (shouldReplayWallet) wallet.reset(); - } finally { - walletStream.close(); + if (!isBsqWallet) { + maybeAddSegwitKeychain(wallet, null); + } } return wallet; } protected Wallet createWallet(boolean isBsqWallet) { - // Change preferredOutputScriptType of btc wallet to P2WPKH to start using segwit - // Script.ScriptType preferredOutputScriptType = isBsqWallet ? Script.ScriptType.P2PKH : Script.ScriptType.P2WPKH; - Script.ScriptType preferredOutputScriptType = Script.ScriptType.P2PKH; + Script.ScriptType preferredOutputScriptType = isBsqWallet ? Script.ScriptType.P2PKH : Script.ScriptType.P2WPKH; KeyChainGroupStructure structure = new BisqKeyChainGroupStructure(isBsqWallet); KeyChainGroup.Builder kcgBuilder = KeyChainGroup.builder(params, structure); if (restoreFromSeed != null) { @@ -488,4 +508,38 @@ public PeerGroup peerGroup() { public File directory() { return directory; } + + public void maybeAddSegwitKeychain(Wallet wallet, KeyParameter aesKey) { + if (BisqKeyChainGroupStructure.BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH.equals(wallet.getActiveKeyChain().getAccountPath())) { + if (wallet.isEncrypted() && aesKey == null) { + // wait for the aesKey to be set and this method to be invoked again. + return; + } + // Do a backup of the wallet + File backup = new File(directory, WalletsSetup.PRE_SEGWIT_WALLET_BACKUP); + try { + FileUtil.copyFile(new File(directory, "bisq_BTC.wallet"), backup); + } catch (IOException e) { + log.error(e.toString(), e); + } + + // Btc wallet does not have a native segwit keychain, we should add one. + DeterministicSeed seed = wallet.getKeyChainSeed(); + if (aesKey != null) { + // If wallet is encrypted, decrypt the seed. + KeyCrypter keyCrypter = wallet.getKeyCrypter(); + seed = seed.decrypt(keyCrypter, DeterministicKeyChain.DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey); + } + DeterministicKeyChain nativeSegwitKeyChain = DeterministicKeyChain.builder().seed(seed) + .outputScriptType(Script.ScriptType.P2WPKH) + .accountPath(new BisqKeyChainGroupStructure(false).accountPathFor(Script.ScriptType.P2WPKH)).build(); + if (aesKey != null) { + // If wallet is encrypted, encrypt the new keychain. + KeyCrypter keyCrypter = wallet.getKeyCrypter(); + nativeSegwitKeyChain = nativeSegwitKeyChain.toEncrypted(keyCrypter, aesKey); + } + wallet.addAndActivateHDChain(nativeSegwitKeyChain); + } + migratedWalletToSegwit.set(true); + } } diff --git a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java index 650f9297853..9b1ce934186 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java @@ -109,6 +109,8 @@ @Slf4j public class WalletsSetup { + public static final String PRE_SEGWIT_WALLET_BACKUP = "pre_segwit_bisq_BTC.wallet.backup"; + @Getter public final BooleanProperty walletsSetupFailed = new SimpleBooleanProperty(); @@ -421,6 +423,13 @@ public void clearBackups() { log.error("Could not delete directory " + e.getMessage()); e.printStackTrace(); } + + File segwitBackup = new File(walletDir, PRE_SEGWIT_WALLET_BACKUP); + try { + FileUtil.deleteFileIfExists(segwitBackup); + } catch (IOException e) { + log.error(e.toString(), e); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java index f0df0adb35d..91b0c84c57b 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java @@ -64,7 +64,7 @@ protected boolean isTxOutputSpendable(TransactionOutput output) { Address address = WalletService.getAddressFromOutput(output); return addresses.contains(address); } else { - log.warn("transactionOutput.getScriptPubKey() not isSentToAddress or isPayToScriptHash"); + log.warn("transactionOutput.getScriptPubKey() is not P2PKH nor P2SH nor P2WH"); return false; } } 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 b5860e00491..e21ce45b99a 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -32,6 +32,7 @@ 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.Transaction; @@ -41,6 +42,7 @@ import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; @@ -576,46 +578,49 @@ 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())) .findAny(); if (emptyAvailableAddressEntry.isPresent()) { return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); } else { - AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context, offerId); + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); + AddressEntry entry = new AddressEntry(key, context, offerId, false); addressEntryList.addAddressEntry(entry); return entry; } } } - private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional addressEntry) { - if (addressEntry.isPresent()) { - return addressEntry.get(); - } else { - AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context); - addressEntryList.addAddressEntry(entry); - return entry; - } - } - public AddressEntry getArbitratorAddressEntry() { AddressEntry.Context context = AddressEntry.Context.ARBITRATOR; Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .findAny(); - return getOrCreateAddressEntry(context, addressEntry); + return getOrCreateAddressEntry(context, addressEntry, false); } public AddressEntry getFreshAddressEntry() { + return getFreshAddressEntry(true); + } + + public AddressEntry getFreshAddressEntry(boolean segwit) { AddressEntry.Context context = AddressEntry.Context.AVAILABLE; Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) + .filter(e -> { + boolean isSegwitOutputScriptType = Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType()); + // We need to ensure that we take only addressEntries which matches our segWit flag + boolean isMatchingOutputScriptType = isSegwitOutputScriptType == segwit; + return isMatchingOutputScriptType; + }) .findAny(); - return getOrCreateAddressEntry(context, addressEntry); + return getOrCreateAddressEntry(context, addressEntry, segwit); } public void recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { @@ -623,6 +628,22 @@ public void recoverAddressEntry(String offerId, String address, AddressEntry.Con addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId)); } + private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional addressEntry, boolean segwit) { + if (addressEntry.isPresent()) { + return addressEntry.get(); + } else { + DeterministicKey key; + if (segwit) { + key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH)); + } else { + key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); + } + AddressEntry entry = new AddressEntry(key, context, segwit); + addressEntryList.addAddressEntry(entry); + return entry; + } + } + private Optional findAddressEntry(String address, AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> address.equals(e.getAddressString())) @@ -969,7 +990,7 @@ public Transaction getFeeEstimationTransactionForMultipleAddresses(Set f counter++; fee = txFeeForWithdrawalPerByte.multiply(txSize); // We use a dummy address for the output - final String dummyReceiver = getFreshAddressEntry().getAddressString(); + final String dummyReceiver = LegacyAddress.fromKey(params, new ECKey()).toBase58(); SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey); wallet.completeTx(sendRequest); tx = sendRequest.tx; @@ -998,7 +1019,7 @@ 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, wallet.currentReceiveKey()); + Address dummyAddress = LegacyAddress.fromKey(params, new ECKey()); outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress)); SendRequest sendRequest = SendRequest.forTx(transaction); 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 f1ea7b042cb..eb7b7485c10 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -44,6 +44,7 @@ import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.TransactionSignature; @@ -153,7 +154,7 @@ public Transaction createBtcTradingFeeTx(Address fundingAddress, Transaction tradingFeeTx = new Transaction(params); SendRequest sendRequest = null; try { - tradingFeeTx.addOutput(tradingFee, LegacyAddress.fromBase58(params, feeReceiverAddress)); + tradingFeeTx.addOutput(tradingFee, Address.fromString(params, feeReceiverAddress)); // the reserved amount we need for the trade we send to our trade reservedForTradeAddress tradingFeeTx.addOutput(reservedFundsForOffer, reservedForTradeAddress); @@ -516,7 +517,7 @@ private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuy TransactionOutput takerTransactionOutput = null; if (takerChangeOutputValue > 0 && takerChangeAddressString != null) { takerTransactionOutput = new TransactionOutput(params, preparedDepositTx, Coin.valueOf(takerChangeOutputValue), - LegacyAddress.fromBase58(params, takerChangeAddressString)); + Address.fromString(params, takerChangeAddressString)); } if (makerIsBuyer) { @@ -599,8 +600,13 @@ public Transaction takerSignsDepositTx(boolean takerIsSeller, // Add buyer inputs and apply signature // We grab the signature from the makersDepositTx and apply it to the new tx input for (int i = 0; i < buyerInputs.size(); i++) { - TransactionInput transactionInput = makersDepositTx.getInputs().get(i); - depositTx.addInput(getTransactionInput(depositTx, getMakersScriptSigProgram(transactionInput), buyerInputs.get(i))); + TransactionInput makersInput = makersDepositTx.getInputs().get(i); + byte[] makersScriptSigProgram = getMakersScriptSigProgram(makersInput); + TransactionInput input = getTransactionInput(depositTx, makersScriptSigProgram, buyerInputs.get(i)); + if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) { + input.setWitness(makersInput.getWitness()); + } + depositTx.addInput(input); } // Add seller inputs @@ -662,9 +668,14 @@ public void sellerAsMakerFinalizesDepositTx(Transaction myDepositTx, // We add takers signature from his inputs and add it to out tx which was already signed earlier. for (int i = 0; i < numTakersInputs; i++) { - TransactionInput input = takersDepositTx.getInput(i); - Script scriptSig = input.getScriptSig(); - myDepositTx.getInput(i).setScriptSig(scriptSig); + TransactionInput takersInput = takersDepositTx.getInput(i); + Script takersScriptSig = takersInput.getScriptSig(); + TransactionInput txInput = myDepositTx.getInput(i); + txInput.setScriptSig(takersScriptSig); + TransactionWitness witness = takersInput.getWitness(); + if (!TransactionWitness.EMPTY.equals(witness)) { + txInput.setWitness(witness); + } } WalletService.printTx("sellerAsMakerFinalizesDepositTx", myDepositTx); @@ -686,7 +697,7 @@ public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, delayedPayoutTx.addInput(p2SHMultiSigOutput); applyLockTime(lockTime, delayedPayoutTx); Coin outputAmount = p2SHMultiSigOutput.getValue().subtract(minerFee); - delayedPayoutTx.addOutput(outputAmount, LegacyAddress.fromBase58(params, donationAddressString)); + delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString)); WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx); WalletService.verifyTransaction(delayedPayoutTx); return delayedPayoutTx; @@ -938,10 +949,10 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali Transaction payoutTx = new Transaction(params); payoutTx.addInput(p2SHMultiSigOutput); if (buyerPayoutAmount.isPositive()) { - payoutTx.addOutput(buyerPayoutAmount, LegacyAddress.fromBase58(params, buyerAddressString)); + payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { - payoutTx.addOutput(sellerPayoutAmount, LegacyAddress.fromBase58(params, sellerAddressString)); + payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } // take care of sorting! @@ -1001,10 +1012,10 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput)); if (buyerPayoutAmount.isPositive()) { - payoutTx.addOutput(buyerPayoutAmount, LegacyAddress.fromBase58(params, buyerAddressString)); + payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { - payoutTx.addOutput(sellerPayoutAmount, LegacyAddress.fromBase58(params, sellerAddressString)); + payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } // take care of sorting! @@ -1082,7 +1093,7 @@ private RawTransactionInput getRawInputFromTransactionInput(@NotNull Transaction checkNotNull(input.getValue(), "input.getValue() must not be null"); return new RawTransactionInput(input.getOutpoint().getIndex(), - input.getConnectedOutput().getParentTransaction().bitcoinSerialize(), + input.getConnectedOutput().getParentTransaction().bitcoinSerialize(false), input.getValue().value); } @@ -1146,10 +1157,10 @@ private Transaction createPayoutTx(Transaction depositTx, Transaction transaction = new Transaction(params); transaction.addInput(p2SHMultiSigOutput); if (buyerPayoutAmount.isPositive()) { - transaction.addOutput(buyerPayoutAmount, LegacyAddress.fromBase58(params, buyerAddressString)); + transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { - transaction.addOutput(sellerPayoutAmount, LegacyAddress.fromBase58(params, sellerAddressString)); + transaction.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } checkArgument(transaction.getOutputs().size() >= 1, "We need at least one output."); return transaction; @@ -1165,13 +1176,27 @@ private void signInput(Transaction transaction, TransactionInput input, int inpu if (sigKey.isEncrypted()) { checkNotNull(aesKey); } - Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false); - ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey); - TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false); - if (ScriptPattern.isP2PK(scriptPubKey)) { - input.setScriptSig(ScriptBuilder.createInputScript(txSig)); - } else if (ScriptPattern.isP2PKH(scriptPubKey)) { - input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey)); + + if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { + Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false); + ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey); + TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false); + if (ScriptPattern.isP2PK(scriptPubKey)) { + input.setScriptSig(ScriptBuilder.createInputScript(txSig)); + } else if (ScriptPattern.isP2PKH(scriptPubKey)) { + 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(); + Coin value = input.getValue(); + TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, scriptCode, value, + Transaction.SigHash.ALL, false); + input.setScriptSig(ScriptBuilder.createEmpty()); + input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey)); } else { throw new SigningException("Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey); } 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 b92cdc24103..920df501e82 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -37,12 +37,14 @@ 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; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.VerificationException; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.core.listeners.TransactionConfidenceEventListener; @@ -51,6 +53,7 @@ import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptException; import org.bitcoinj.script.ScriptPattern; @@ -241,7 +244,7 @@ public static void checkScriptSig(Transaction transaction, int inputIndex) throws TransactionVerificationException { try { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); - input.getScriptSig().correctlySpends(transaction, inputIndex, input.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + input.getScriptSig().correctlySpends(transaction, inputIndex, input.getWitness(), input.getValue(), input.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); } catch (Throwable t) { t.printStackTrace(); log.error(t.getMessage()); @@ -265,7 +268,7 @@ public static void signTransactionInput(Wallet wallet, // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when // we sign missing pieces (to check this would require either assuming any signatures are signing // standard output types or a way to get processed signatures out of script execution) - txIn.getScriptSig().correctlySpends(tx, index, txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); return; } catch (ScriptException e) { @@ -288,7 +291,7 @@ public static void signTransactionInput(Wallet wallet, // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when // we sign missing pieces (to check this would require either assuming any signatures are signing // standard output types or a way to get processed signatures out of script execution) - txIn.getScriptSig().correctlySpends(tx, index, txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); + txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); return; } catch (ScriptException e) { @@ -312,14 +315,37 @@ public static void signTransactionInput(Wallet wallet, Script inputScript = txIn.getScriptSig(); byte[] script = redeemData.redeemScript.getProgram(); - try { - TransactionSignature signature = partialTx.calculateSignature(index, key, script, Transaction.SigHash.ALL, false); - inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), 0); - txIn.setScriptSig(inputScript); - } catch (ECKey.KeyIsEncryptedException e1) { - throw e1; - } catch (ECKey.MissingPrivateKeyException e1) { - log.warn("No private key in keypair for input {}", index); + + if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { + try { + TransactionSignature signature = partialTx.calculateSignature(index, key, script, Transaction.SigHash.ALL, false); + inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), 0); + txIn.setScriptSig(inputScript); + } catch (ECKey.KeyIsEncryptedException e1) { + throw e1; + } catch (ECKey.MissingPrivateKeyException e1) { + log.warn("No private key in keypair for input {}", index); + } + } 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(); + Coin value = txIn.getValue(); + TransactionSignature txSig = tx.calculateWitnessSignature(index, key, scriptCode, value, + Transaction.SigHash.ALL, false); + txIn.setScriptSig(ScriptBuilder.createEmpty()); + txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key)); + } catch (ECKey.KeyIsEncryptedException e1) { + throw e1; + } catch (ECKey.MissingPrivateKeyException e1) { + log.warn("No private key in keypair for input {}", index); + } + } else { + // log.error("Unexpected script type."); + throw new RuntimeException("Unexpected script type."); } } else { log.warn("Missing connected output, assuming input {} is already signed.", index); @@ -585,14 +611,9 @@ public boolean checkAESKey(KeyParameter aesKey) { return wallet.checkAESKey(aesKey); } - @Nullable - public DeterministicKey findKeyFromPubKeyHash(byte[] pubKeyHash) { - return wallet.getActiveKeyChain().findKeyFromPubHash(pubKeyHash); - } - @Nullable public DeterministicKey findKeyFromPubKey(byte[] pubKey) { - return wallet.getActiveKeyChain().findKeyFromPubKey(pubKey); + return (DeterministicKey) wallet.findKeyFromPubKey(pubKey); } public boolean isEncrypted() { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3afb4f273af..32c2813bed9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -992,6 +992,7 @@ funds.deposit.fundWallet=Fund your wallet funds.deposit.withdrawFromWallet=Send funds from wallet funds.deposit.amount=Amount in BTC (optional) funds.deposit.generateAddress=Generate new address +funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Please select an unused address from the table above rather than generating a new one. funds.withdrawal.arbitrationFee=Arbitration fee diff --git a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java index 3d08c7528ce..1195c069369 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/deposit/DepositView.java @@ -41,8 +41,12 @@ import bisq.common.UserThread; import bisq.common.app.DevEnv; +import bisq.common.config.Config; +import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Transaction; import net.glxn.qrgen.QRCode; @@ -54,6 +58,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -85,10 +90,7 @@ import org.jetbrains.annotations.NotNull; -import static bisq.desktop.util.FormBuilder.addAddressTextField; -import static bisq.desktop.util.FormBuilder.addButton; -import static bisq.desktop.util.FormBuilder.addInputTextField; -import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.*; @FxmlView public class DepositView extends ActivatableView { @@ -102,6 +104,7 @@ public class DepositView extends ActivatableView { private ImageView qrCodeImageView; private AddressTextField addressTextField; private Button generateNewAddressButton; + private CheckBox generateNewAddressSegwitCheckbox; private TitledGroupBg titledGroupBg; private InputTextField amountTextField; @@ -195,16 +198,26 @@ public void initialize() { addressTextField.setManaged(false); amountTextField.setManaged(false); + generateNewAddressSegwitCheckbox = addCheckBox(gridPane, ++gridRow, + Res.get("funds.deposit.generateAddressSegwit"), -20); + generateNewAddressSegwitCheckbox.setAllowIndeterminate(false); + generateNewAddressSegwitCheckbox.setSelected(true); + GridPane.setColumnIndex(generateNewAddressSegwitCheckbox, 0); + GridPane.setHalignment(generateNewAddressSegwitCheckbox, HPos.LEFT); + generateNewAddressButton = addButton(gridPane, ++gridRow, Res.get("funds.deposit.generateAddress"), -20); GridPane.setColumnIndex(generateNewAddressButton, 0); GridPane.setHalignment(generateNewAddressButton, HPos.LEFT); generateNewAddressButton.setOnAction(event -> { - boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getNumTxOutputs() == 0); + boolean segwit = generateNewAddressSegwitCheckbox.isSelected(); + NetworkParameters params = Config.baseCurrencyNetworkParameters(); + boolean hasUnUsedAddress = observableList.stream().anyMatch(e -> e.getNumTxOutputs() == 0 + && (Address.fromString(params, e.getAddressString()) instanceof SegwitAddress) == segwit); if (hasUnUsedAddress) { new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); } else { - AddressEntry newSavingsAddressEntry = walletService.getFreshAddressEntry(); + AddressEntry newSavingsAddressEntry = walletService.getFreshAddressEntry(segwit); updateList(); observableList.stream() .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 7ca83933227..f73dba9b81b 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -20,7 +20,7 @@ dependencyVerification { 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', 'com.github.JesusMcCloud:jtorctl:389d61b1b5a85eb2f23c582c3913ede49f80c9f2b553e4762382c836270e57e5', - 'com.github.bisq-network:bitcoinj:85d609e9bbaa93de0a9ca1ab436f578c14f7cfa1876b50878046d9f624b48a6b', + 'com.github.bisq-network:bitcoinj:b8b6e4b8010f2b8d4aac7141c0809dea6d102c3ff3c06ceba78c2626d531b0af', 'com.github.cd2357.netlayer:tor.external:7c70846d36465279c2664f147a0f2d47202c5d67c6a2075225194779c3fbe122', 'com.github.cd2357.netlayer:tor.native:84b449191d535a3c2187f7f7f3bb9bcb7d1097f07c6bf8c4f2b3331c20107d9a', 'com.github.cd2357.netlayer:tor:ff92e4a7b59d1b480e0427fcfcf3f82a6fd69be68eec91c6360774d599e3c2e0', diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index e16746c09be..11c32109811 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1239,6 +1239,7 @@ message AddressEntry { bytes pub_key = 9; bytes pub_key_hash = 10; int64 coin_locked_in_multi_sig = 11; + bool segwit = 12; } message NavigationPath {