Merge pull request #4568 from oscarguindzberg/segwitWallet

Add segwit support to the BTC wallet
This commit is contained in:
sqrrm 2020-10-09 00:22:14 +02:00 committed by GitHub
commit 35e0c34c74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 279 additions and 110 deletions

View File

@ -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'

View File

@ -389,6 +389,8 @@ public class BisqSetup {
if (requestWalletPasswordHandler != null) {
requestWalletPasswordHandler.accept(aesKey -> {
walletsManager.setAesKey(aesKey);
walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(),
aesKey);
if (preferences.isResyncSpvRequested()) {
if (showFirstPopupIfResyncSPVRequestedHandler != null)
showFirstPopupIfResyncSPVRequestedHandler.run();

View File

@ -105,7 +105,7 @@ public class WalletAppSetup {
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<Throwable> walletServiceException = new SimpleObjectProperty<>();
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),

View File

@ -26,8 +26,8 @@ import com.google.protobuf.ByteString;
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 final class AddressEntry implements PersistablePayload {
private long coinLockedInMultiSig;
@Getter
private boolean segwit;
@Nullable
transient private DeterministicKey keyPair;
@Nullable
@ -86,18 +89,24 @@ public final class AddressEntry implements PersistablePayload {
// 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 @@ public final class AddressEntry implements PersistablePayload {
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 final class AddressEntry implements PersistablePayload {
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 final class AddressEntry implements PersistablePayload {
.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 final class AddressEntry implements PersistablePayload {
@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 final class AddressEntry implements PersistablePayload {
", context=" + context +
", offerId='" + offerId + '\'' +
", coinLockedInMultiSig=" + coinLockedInMultiSig +
", segwit=" + segwit +
"}";
}
}

View File

@ -25,7 +25,7 @@ import bisq.common.proto.persistable.PersistedDataHost;
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.inject.Inject;
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 final class AddressEntryList implements PersistableEnvelope, PersistedDat
if (!entrySet.isEmpty()) {
Set<AddressEntry> 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 final class AddressEntryList implements PersistableEnvelope, PersistedDat
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 final class AddressEntryList implements PersistableEnvelope, PersistedDat
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 final class AddressEntryList implements PersistableEnvelope, PersistedDat
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 final class AddressEntryList implements PersistableEnvelope, PersistedDat
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 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
.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);
}

View File

@ -47,10 +47,11 @@ public class BisqKeyChainGroupStructure implements KeyChainGroupStructure {
new ChildNumber(142, true),
ChildNumber.ZERO_HARDENED);
public static final ImmutableList<ChildNumber> 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<ChildNumber> 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 @@ public class BisqKeyChainGroupStructure implements KeyChainGroupStructure {
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());
}

View File

@ -22,11 +22,13 @@ import bisq.core.btc.nodes.ProxySocketFactory;
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 org.bitcoinj.wallet.*;
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 @@ public class WalletConfig extends AbstractIdleService {
vPeerGroup.addWallet(vBsqWallet);
onSetupCompleted();
Futures.addCallback((ListenableFuture<?>) vPeerGroup.startAsync(), new FutureCallback<Object>() {
@Override
public void onSuccess(@Nullable Object result) {
//completeExtensionInitiations(vPeerGroup);
DownloadProgressTracker tracker = downloadListener == null ? new DownloadProgressTracker() : downloadListener;
vPeerGroup.startBlockChainDownload(tracker);
}
if (migratedWalletToSegwit.get()) {
startPeerGroup();
} else {
migratedWalletToSegwit.addListener((observable, oldValue, newValue) -> {
if (newValue) {
startPeerGroup();
}
});
}
@Override
public void onFailure(Throwable t) {
throw new RuntimeException(t);
}
}, MoreExecutors.directExecutor());
} catch (BlockStoreException e) {
throw new IOException(e);
}
}
private void startPeerGroup() {
Futures.addCallback((ListenableFuture<?>) vPeerGroup.startAsync(), new FutureCallback<Object>() {
@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 @@ public class WalletConfig extends AbstractIdleService {
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 @@ public class WalletConfig extends AbstractIdleService {
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 @@ public class WalletConfig extends AbstractIdleService {
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 class WalletConfig extends AbstractIdleService {
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);
}
}

View File

@ -109,6 +109,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
@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 class WalletsSetup {
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);
}
}

View File

@ -64,7 +64,7 @@ class BtcCoinSelector extends BisqDefaultCoinSelector {
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;
}
}

View File

@ -32,6 +32,7 @@ import bisq.common.handlers.ErrorMessageHandler;
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.TransactionOutPoint;
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 class BtcWalletService extends WalletService {
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<AddressEntry> 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> 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> 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> 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 class BtcWalletService extends WalletService {
addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId));
}
private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional<AddressEntry> 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<AddressEntry> findAddressEntry(String address, AddressEntry.Context context) {
return getAddressEntryListAsImmutableList().stream()
.filter(e -> address.equals(e.getAddressString()))
@ -969,7 +990,7 @@ public class BtcWalletService extends WalletService {
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 @@ public class BtcWalletService extends WalletService {
public int getEstimatedFeeTxSize(List<Coin> 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);

View File

@ -44,6 +44,7 @@ import org.bitcoinj.core.Transaction;
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 class TradeWalletService {
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 @@ public class TradeWalletService {
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 class TradeWalletService {
// 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 class TradeWalletService {
// 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 class TradeWalletService {
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 class TradeWalletService {
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 class TradeWalletService {
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 @@ public class TradeWalletService {
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 @@ public class TradeWalletService {
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 @@ public class TradeWalletService {
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);
}

View File

@ -37,12 +37,14 @@ import org.bitcoinj.core.Coin;
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.KeyCrypter;
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 abstract class WalletService {
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 abstract class WalletService {
// 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 abstract class WalletService {
// 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 abstract class WalletService {
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 abstract class WalletService {
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() {

View File

@ -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

View File

@ -41,8 +41,12 @@ import bisq.core.util.coin.CoinFormatter;
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 javax.inject.Named;
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 java.util.concurrent.TimeUnit;
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<VBox, Void> {
@ -102,6 +104,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
private ImageView qrCodeImageView;
private AddressTextField addressTextField;
private Button generateNewAddressButton;
private CheckBox generateNewAddressSegwitCheckbox;
private TitledGroupBg titledGroupBg;
private InputTextField amountTextField;
@ -195,16 +198,26 @@ public class DepositView extends ActivatableView<VBox, Void> {
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()))

View File

@ -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',

View File

@ -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 {