diff --git a/core/src/main/java/org/bitcoinj/core/ECKey.java b/core/src/main/java/org/bitcoinj/core/ECKey.java index f58032630..17a204fe1 100644 --- a/core/src/main/java/org/bitcoinj/core/ECKey.java +++ b/core/src/main/java/org/bitcoinj/core/ECKey.java @@ -19,6 +19,8 @@ package org.bitcoinj.core; import org.bitcoinj.crypto.*; +import org.bitcoinj.script.Script; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; @@ -1288,10 +1290,15 @@ public class ECKey implements EncryptableItem { } public void formatKeyWithAddress(boolean includePrivateKeys, @Nullable KeyParameter aesKey, StringBuilder builder, - NetworkParameters params) { - final Address address = LegacyAddress.fromKey(params, this); + NetworkParameters params, Script.ScriptType outputScriptType) { builder.append(" addr:"); - builder.append(address.toString()); + if (outputScriptType != null) { + builder.append(Address.fromKey(params, this, outputScriptType)); + } else { + builder.append(LegacyAddress.fromKey(params, this)); + if (isCompressed()) + builder.append(',').append(SegwitAddress.fromKey(params, this)); + } if (!isCompressed()) builder.append(" UNCOMPRESSED"); builder.append(" hash160:"); diff --git a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java index fe69622c6..12d3dac01 100644 --- a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java +++ b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java @@ -81,8 +81,10 @@ public abstract class NetworkParameters { protected int interval; protected int targetTimespan; protected byte[] alertSigningKey; - protected int bip32HeaderPub; - protected int bip32HeaderPriv; + protected int bip32HeaderP2PKHpub; + protected int bip32HeaderP2PKHpriv; + protected int bip32HeaderP2WPKHpub; + protected int bip32HeaderP2WPKHpriv; /** Used to check majorities for block version upgrade */ protected int majorityEnforceBlockUpgrade; @@ -370,16 +372,25 @@ public abstract class NetworkParameters { return alertSigningKey; } - /** Returns the 4 byte header for BIP32 (HD) wallet - public key part. */ - public int getBip32HeaderPub() { - return bip32HeaderPub; + /** Returns the 4 byte header for BIP32 wallet P2PKH - public key part. */ + public int getBip32HeaderP2PKHpub() { + return bip32HeaderP2PKHpub; } - /** Returns the 4 byte header for BIP32 (HD) wallet - private key part. */ - public int getBip32HeaderPriv() { - return bip32HeaderPriv; + /** Returns the 4 byte header for BIP32 wallet P2PKH - private key part. */ + public int getBip32HeaderP2PKHpriv() { + return bip32HeaderP2PKHpriv; } + /** Returns the 4 byte header for BIP32 wallet P2WPKH - public key part. */ + public int getBip32HeaderP2WPKHpub() { + return bip32HeaderP2WPKHpub; + } + + /** Returns the 4 byte header for BIP32 wallet P2WPKH - private key part. */ + public int getBip32HeaderP2WPKHpriv() { + return bip32HeaderP2WPKHpriv; + } /** * Returns the number of coins that will be produced in total, on this * network. Where not applicable, a very large number of coins is returned diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index 2b9c3b971..866aa298c 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -921,17 +921,29 @@ public class Transaction extends ChildMessage { SigHash sigHash, boolean anyoneCanPay) throws ScriptException { // Verify the API user didn't try to do operations out of order. checkState(!outputs.isEmpty(), "Attempting to sign tx without outputs."); - TransactionInput input = new TransactionInput(params, this, new byte[]{}, prevOut); + TransactionInput input = new TransactionInput(params, this, new byte[] {}, prevOut); addInput(input); - Sha256Hash hash = hashForSignature(inputs.size() - 1, scriptPubKey, sigHash, anyoneCanPay); - ECKey.ECDSASignature ecSig = sigKey.sign(hash); - TransactionSignature txSig = new TransactionSignature(ecSig, sigHash, anyoneCanPay); - if (ScriptPattern.isPayToPubKey(scriptPubKey)) - input.setScriptSig(ScriptBuilder.createInputScript(txSig)); - else if (ScriptPattern.isPayToPubKeyHash(scriptPubKey)) - input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey)); - else + int inputIndex = inputs.size() - 1; + if (ScriptPattern.isPayToPubKey(scriptPubKey)) { + TransactionSignature signature = calculateSignature(inputIndex, sigKey, scriptPubKey, sigHash, + anyoneCanPay); + input.setScriptSig(ScriptBuilder.createInputScript(signature)); + input.setWitness(null); + } else if (ScriptPattern.isPayToPubKeyHash(scriptPubKey)) { + TransactionSignature signature = calculateSignature(inputIndex, sigKey, scriptPubKey, sigHash, + anyoneCanPay); + input.setScriptSig(ScriptBuilder.createInputScript(signature, sigKey)); + input.setWitness(null); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(scriptPubKey)) { + Script scriptCode = new ScriptBuilder() + .data(ScriptBuilder.createOutputScript(LegacyAddress.fromKey(params, sigKey)).getProgram()).build(); + TransactionSignature signature = calculateWitnessSignature(inputIndex, sigKey, scriptCode, input.getValue(), + sigHash, anyoneCanPay); + input.setScriptSig(ScriptBuilder.createEmpty()); + input.setWitness(TransactionWitness.redeemP2WPKH(signature, sigKey)); + } else { throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey); + } return input; } diff --git a/core/src/main/java/org/bitcoinj/core/TransactionBag.java b/core/src/main/java/org/bitcoinj/core/TransactionBag.java index 608d782ec..87a4a292a 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionBag.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionBag.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Giannis Dzegoutanis + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +17,8 @@ package org.bitcoinj.core; +import javax.annotation.Nullable; + import org.bitcoinj.script.Script; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletTransaction; @@ -26,8 +29,13 @@ import java.util.Map; * This interface is used to abstract the {@link Wallet} and the {@link Transaction} */ public interface TransactionBag { - /** Returns true if this wallet contains a public key which hashes to the given hash. */ - boolean isPubKeyHashMine(byte[] pubKeyHash); + /** + * Look for a public key which hashes to the given hash and (optionally) is used for a specific script type. + * @param pubKeyHash hash of the public key to look for + * @param scriptType only look for given usage (currently {@link Script.ScriptType#P2PKH} or {@link Script.ScriptType#P2WPKH}) or {@code null} if we don't care + * @return true if hash was found + */ + boolean isPubKeyHashMine(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType); /** Returns true if this wallet is watching transactions for outputs with the script. */ boolean isWatchedScript(Script script); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionInput.java b/core/src/main/java/org/bitcoinj/core/TransactionInput.java index c8a716c73..cc66a0d56 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionInput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionInput.java @@ -474,8 +474,8 @@ public class TransactionInput extends ChildMessage { throw new VerificationException("This input refers to a different output on the given tx."); } Script pubKey = output.getScriptPubKey(); - int myIndex = getParentTransaction().getInputs().indexOf(this); - getScriptSig().correctlySpends(getParentTransaction(), getIndex(), pubKey); + getScriptSig().correctlySpends(getParentTransaction(), getIndex(), getWitness(), getValue(), pubKey, + Script.ALL_VERIFY_FLAGS); } /** diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java b/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java index 6e549c5d4..bcb52a7f4 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java @@ -129,7 +129,7 @@ public class TransactionOutPoint extends ChildMessage { } /** - * Returns the ECKey identified in the connected output, for either P2PKH scripts or P2PK scripts. + * Returns the ECKey identified in the connected output, for either P2PKH, P2WPKH or P2PK scripts. * For P2SH scripts you can use {@link #getConnectedRedeemData(KeyBag)} and then get the * key from RedeemData. * If the script form cannot be understood, throws ScriptException. @@ -143,7 +143,10 @@ public class TransactionOutPoint extends ChildMessage { Script connectedScript = connectedOutput.getScriptPubKey(); if (ScriptPattern.isPayToPubKeyHash(connectedScript)) { byte[] addressBytes = ScriptPattern.extractHashFromPayToPubKeyHash(connectedScript); - return keyBag.findKeyFromPubKeyHash(addressBytes); + return keyBag.findKeyFromPubKeyHash(addressBytes, Script.ScriptType.P2PKH); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(connectedScript)) { + byte[] addressBytes = ScriptPattern.extractHashFromPayToWitnessHash(connectedScript); + return keyBag.findKeyFromPubKeyHash(addressBytes, Script.ScriptType.P2WPKH); } else if (ScriptPattern.isPayToPubKey(connectedScript)) { byte[] pubkeyBytes = ScriptPattern.extractKeyFromPayToPubKey(connectedScript); return keyBag.findKeyFromPubKey(pubkeyBytes); @@ -153,7 +156,7 @@ public class TransactionOutPoint extends ChildMessage { } /** - * Returns the RedeemData identified in the connected output, for either P2PKH scripts, P2PK + * Returns the RedeemData identified in the connected output, for either P2PKH, P2WPKH, P2PK * or P2SH scripts. * If the script forms cannot be understood, throws ScriptException. * @@ -166,7 +169,10 @@ public class TransactionOutPoint extends ChildMessage { Script connectedScript = connectedOutput.getScriptPubKey(); if (ScriptPattern.isPayToPubKeyHash(connectedScript)) { byte[] addressBytes = ScriptPattern.extractHashFromPayToPubKeyHash(connectedScript); - return RedeemData.of(keyBag.findKeyFromPubKeyHash(addressBytes), connectedScript); + return RedeemData.of(keyBag.findKeyFromPubKeyHash(addressBytes, Script.ScriptType.P2PKH), connectedScript); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(connectedScript)) { + byte[] addressBytes = ScriptPattern.extractHashFromPayToWitnessHash(connectedScript); + return RedeemData.of(keyBag.findKeyFromPubKeyHash(addressBytes, Script.ScriptType.P2WPKH), connectedScript); } else if (ScriptPattern.isPayToPubKey(connectedScript)) { byte[] pubkeyBytes = ScriptPattern.extractKeyFromPayToPubKey(connectedScript); return RedeemData.of(keyBag.findKeyFromPubKey(pubkeyBytes), connectedScript); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java index 59fe6d77e..b015e08d1 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java @@ -306,7 +306,11 @@ public class TransactionOutput extends ChildMessage { else if (ScriptPattern.isPayToScriptHash(script)) return transactionBag.isPayToScriptHashMine(ScriptPattern.extractHashFromPayToScriptHash(script)); else if (ScriptPattern.isPayToPubKeyHash(script)) - return transactionBag.isPubKeyHashMine(ScriptPattern.extractHashFromPayToPubKeyHash(script)); + return transactionBag.isPubKeyHashMine(ScriptPattern.extractHashFromPayToPubKeyHash(script), + Script.ScriptType.P2PKH); + else if (ScriptPattern.isPayToWitnessPubKeyHash(script)) + return transactionBag.isPubKeyHashMine(ScriptPattern.extractHashFromPayToWitnessHash(script), + Script.ScriptType.P2WPKH); else return false; } catch (ScriptException e) { @@ -325,7 +329,8 @@ public class TransactionOutput extends ChildMessage { Script script = getScriptPubKey(); StringBuilder buf = new StringBuilder("TxOut of "); buf.append(Coin.valueOf(value).toFriendlyString()); - if (ScriptPattern.isPayToPubKeyHash(script) || ScriptPattern.isPayToScriptHash(script)) + if (ScriptPattern.isPayToPubKeyHash(script) || ScriptPattern.isPayToWitnessPubKeyHash(script) + || ScriptPattern.isPayToScriptHash(script)) buf.append(" to ").append(script.getToAddress(params)); else if (ScriptPattern.isPayToPubKey(script)) buf.append(" to pubkey ").append(Utils.HEX.encode(ScriptPattern.extractKeyFromPayToPubKey(script))); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionWitness.java b/core/src/main/java/org/bitcoinj/core/TransactionWitness.java index ae0b4dec3..9847e2db5 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionWitness.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionWitness.java @@ -14,15 +14,33 @@ package org.bitcoinj.core; +import static com.google.common.base.Preconditions.checkArgument; + import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; + +import org.bitcoinj.crypto.TransactionSignature; + public class TransactionWitness { public static final TransactionWitness EMPTY = new TransactionWitness(0); + /** + * Creates the stack pushes necessary to redeem a P2WPKH output. If given signature is null, an empty push will be + * used as a placeholder. + */ + public static TransactionWitness redeemP2WPKH(@Nullable TransactionSignature signature, ECKey pubKey) { + checkArgument(pubKey.isCompressed(), "only compressed keys allowed"); + TransactionWitness witness = new TransactionWitness(2); + witness.setPush(0, signature != null ? signature.encodeToBitcoin() : new byte[0]); // signature + witness.setPush(1, pubKey.getPubKey()); // pubkey + return witness; + } + private final List pushes; public TransactionWitness(int pushCount) { diff --git a/core/src/main/java/org/bitcoinj/crypto/ChildNumber.java b/core/src/main/java/org/bitcoinj/crypto/ChildNumber.java index 67b9ea980..068145a1e 100644 --- a/core/src/main/java/org/bitcoinj/crypto/ChildNumber.java +++ b/core/src/main/java/org/bitcoinj/crypto/ChildNumber.java @@ -37,8 +37,9 @@ public class ChildNumber implements Comparable { public static final int HARDENED_BIT = 0x80000000; public static final ChildNumber ZERO = new ChildNumber(0); - public static final ChildNumber ONE = new ChildNumber(1); public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true); + public static final ChildNumber ONE = new ChildNumber(1); + public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true); /** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/ private final int i; diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index 5b6b39302..da8d548a8 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -18,6 +18,7 @@ package org.bitcoinj.crypto; import org.bitcoinj.core.*; +import org.bitcoinj.script.Script; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; @@ -460,17 +461,24 @@ public class DeterministicKey extends ECKey { return key; } + @Deprecated public byte[] serializePublic(NetworkParameters params) { - return serialize(params, true); + return serialize(params, true, Script.ScriptType.P2PKH); } + @Deprecated public byte[] serializePrivate(NetworkParameters params) { - return serialize(params, false); + return serialize(params, false, Script.ScriptType.P2PKH); } - private byte[] serialize(NetworkParameters params, boolean pub) { + private byte[] serialize(NetworkParameters params, boolean pub, Script.ScriptType outputScriptType) { ByteBuffer ser = ByteBuffer.allocate(78); - ser.putInt(pub ? params.getBip32HeaderPub() : params.getBip32HeaderPriv()); + if (outputScriptType == Script.ScriptType.P2PKH) + ser.putInt(pub ? params.getBip32HeaderP2PKHpub() : params.getBip32HeaderP2PKHpriv()); + else if (outputScriptType == Script.ScriptType.P2WPKH) + ser.putInt(pub ? params.getBip32HeaderP2WPKHpub() : params.getBip32HeaderP2WPKHpriv()); + else + throw new IllegalStateException(outputScriptType.toString()); ser.put((byte) getDepth()); ser.putInt(getParentFingerprint()); ser.putInt(getChildNumber().i()); @@ -480,12 +488,20 @@ public class DeterministicKey extends ECKey { return ser.array(); } + public String serializePubB58(NetworkParameters params, Script.ScriptType outputScriptType) { + return toBase58(serialize(params, true, outputScriptType)); + } + + public String serializePrivB58(NetworkParameters params, Script.ScriptType outputScriptType) { + return toBase58(serialize(params, false, outputScriptType)); + } + public String serializePubB58(NetworkParameters params) { - return toBase58(serialize(params, true)); + return serializePubB58(params, Script.ScriptType.P2PKH); } public String serializePrivB58(NetworkParameters params) { - return toBase58(serialize(params, false)); + return serializePrivB58(params, Script.ScriptType.P2PKH); } static String toBase58(byte[] ser) { @@ -520,9 +536,10 @@ public class DeterministicKey extends ECKey { public static DeterministicKey deserialize(NetworkParameters params, byte[] serializedKey, @Nullable DeterministicKey parent) { ByteBuffer buffer = ByteBuffer.wrap(serializedKey); int header = buffer.getInt(); - if (header != params.getBip32HeaderPriv() && header != params.getBip32HeaderPub()) + final boolean pub = header == params.getBip32HeaderP2PKHpub() || header == params.getBip32HeaderP2WPKHpub(); + final boolean priv = header == params.getBip32HeaderP2PKHpriv() || header == params.getBip32HeaderP2WPKHpriv(); + if (!(pub || priv)) throw new IllegalArgumentException("Unknown header bytes: " + toBase58(serializedKey).substring(0, 4)); - boolean pub = header == params.getBip32HeaderPub(); int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative final int parentFingerprint = buffer.getInt(); final int i = buffer.getInt(); @@ -615,9 +632,8 @@ public class DeterministicKey extends ECKey { @Override public void formatKeyWithAddress(boolean includePrivateKeys, @Nullable KeyParameter aesKey, StringBuilder builder, - NetworkParameters params) { - final Address address = LegacyAddress.fromKey(params, this); - builder.append(" addr:").append(address); + NetworkParameters params, Script.ScriptType outputScriptType) { + builder.append(" addr:").append(Address.fromKey(params, this, outputScriptType).toString()); builder.append(" hash160:").append(Utils.HEX.encode(getPubKeyHash())); builder.append(" (").append(getPathAsString()).append(")\n"); if (includePrivateKeys) { diff --git a/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java b/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java index 015f28f5c..01680dd2a 100644 --- a/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java +++ b/core/src/main/java/org/bitcoinj/kits/WalletAppKit.java @@ -25,6 +25,7 @@ import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.net.discovery.*; import org.bitcoinj.protocols.channels.*; +import org.bitcoinj.script.Script; import org.bitcoinj.store.*; import org.bitcoinj.wallet.*; import org.slf4j.*; @@ -63,8 +64,10 @@ import static com.google.common.base.Preconditions.*; public class WalletAppKit extends AbstractIdleService { protected static final Logger log = LoggerFactory.getLogger(WalletAppKit.class); - protected final String filePrefix; protected final NetworkParameters params; + protected final Script.ScriptType preferredOutputScriptType; + protected final KeyChainGroupStructure structure; + protected final String filePrefix; protected volatile BlockChain vChain; protected volatile BlockStore vStore; protected volatile Wallet vWallet; @@ -91,15 +94,26 @@ public class WalletAppKit extends AbstractIdleService { * Creates a new WalletAppKit, with a newly created {@link Context}. Files will be stored in the given directory. */ public WalletAppKit(NetworkParameters params, File directory, String filePrefix) { - this(new Context(params), directory, filePrefix); + this(new Context(params), Script.ScriptType.P2PKH, null, directory, filePrefix); + } + + /** + * Creates a new WalletAppKit, with a newly created {@link Context}. Files will be stored in the given directory. + */ + public WalletAppKit(NetworkParameters params, Script.ScriptType preferredOutputScriptType, + @Nullable KeyChainGroupStructure structure, File directory, String filePrefix) { + this(new Context(params), preferredOutputScriptType, structure, directory, filePrefix); } /** * Creates a new WalletAppKit, with the given {@link Context}. Files will be stored in the given directory. */ - public WalletAppKit(Context context, File directory, String filePrefix) { + public WalletAppKit(Context context, Script.ScriptType preferredOutputScriptType, + @Nullable KeyChainGroupStructure structure, File directory, String filePrefix) { this.context = context; this.params = checkNotNull(context.getParams()); + this.preferredOutputScriptType = checkNotNull(preferredOutputScriptType); + this.structure = structure != null ? structure : KeyChainGroupStructure.DEFAULT; this.directory = checkNotNull(directory); this.filePrefix = checkNotNull(filePrefix); } @@ -434,13 +448,13 @@ public class WalletAppKit extends AbstractIdleService { } protected Wallet createWallet() { - KeyChainGroup.Builder kcg = KeyChainGroup.builder(params); + KeyChainGroup.Builder kcg = KeyChainGroup.builder(params, structure); if (restoreFromSeed != null) - kcg.addChain(DeterministicKeyChain.builder().seed(restoreFromSeed).build()); + kcg.addChain(DeterministicKeyChain.builder().seed(restoreFromSeed).outputScriptType(preferredOutputScriptType).build()); else if (restoreFromKey != null) - kcg.addChain(DeterministicKeyChain.builder().spend(restoreFromKey).build()); + kcg.addChain(DeterministicKeyChain.builder().spend(restoreFromKey).outputScriptType(preferredOutputScriptType).build()); else - kcg.fromRandom(); + kcg.fromRandom(preferredOutputScriptType); if (walletFactory != null) { return walletFactory.create(params, kcg.build()); } else { diff --git a/core/src/main/java/org/bitcoinj/params/MainNetParams.java b/core/src/main/java/org/bitcoinj/params/MainNetParams.java index 1fea834cb..caab2a7f8 100644 --- a/core/src/main/java/org/bitcoinj/params/MainNetParams.java +++ b/core/src/main/java/org/bitcoinj/params/MainNetParams.java @@ -43,8 +43,10 @@ public class MainNetParams extends AbstractBitcoinNetParams { segwitAddressHrp = "bc"; port = 8333; packetMagic = 0xf9beb4d9L; - bip32HeaderPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". - bip32HeaderPriv = 0x0488ADE4; //The 4 byte header that serializes in base58 to "xprv" + bip32HeaderP2PKHpub = 0x0488b21e; // The 4 byte header that serializes in base58 to "xpub". + bip32HeaderP2PKHpriv = 0x0488ade4; // The 4 byte header that serializes in base58 to "xprv" + bip32HeaderP2WPKHpub = 0x04b24746; // The 4 byte header that serializes in base58 to "zpub". + bip32HeaderP2WPKHpriv = 0x04b2430c; // The 4 byte header that serializes in base58 to "zprv" majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED; diff --git a/core/src/main/java/org/bitcoinj/params/RegTestParams.java b/core/src/main/java/org/bitcoinj/params/RegTestParams.java index b7bee3220..962916b9b 100644 --- a/core/src/main/java/org/bitcoinj/params/RegTestParams.java +++ b/core/src/main/java/org/bitcoinj/params/RegTestParams.java @@ -45,8 +45,10 @@ public class RegTestParams extends AbstractBitcoinNetParams { checkState(genesisHash.equals("00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008")); dnsSeeds = null; addrSeeds = null; - bip32HeaderPub = 0x043587CF; - bip32HeaderPriv = 0x04358394; + bip32HeaderP2PKHpub = 0x043587cf; // The 4 byte header that serializes in base58 to "tpub". + bip32HeaderP2PKHpriv = 0x04358394; // The 4 byte header that serializes in base58 to "tprv" + bip32HeaderP2WPKHpub = 0x045f1cf6; // The 4 byte header that serializes in base58 to "vpub". + bip32HeaderP2WPKHpriv = 0x045f18bc; // The 4 byte header that serializes in base58 to "vprv" // Difficulty adjustments are disabled for regtest. // By setting the block interval for difficulty adjustments to Integer.MAX_VALUE we make sure difficulty never diff --git a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java index 8f3cb19a7..ee856e660 100644 --- a/core/src/main/java/org/bitcoinj/params/TestNet3Params.java +++ b/core/src/main/java/org/bitcoinj/params/TestNet3Params.java @@ -68,8 +68,10 @@ public class TestNet3Params extends AbstractBitcoinNetParams { "bitcoin-testnet.bloqseeds.net", // Bloq }; addrSeeds = null; - bip32HeaderPub = 0x043587CF; - bip32HeaderPriv = 0x04358394; + bip32HeaderP2PKHpub = 0x043587cf; // The 4 byte header that serializes in base58 to "tpub". + bip32HeaderP2PKHpriv = 0x04358394; // The 4 byte header that serializes in base58 to "tprv" + bip32HeaderP2WPKHpub = 0x045f1cf6; // The 4 byte header that serializes in base58 to "vpub". + bip32HeaderP2WPKHpriv = 0x045f18bc; // The 4 byte header that serializes in base58 to "vprv" majorityEnforceBlockUpgrade = TESTNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; majorityRejectBlockOutdated = TESTNET_MAJORITY_REJECT_BLOCK_OUTDATED; diff --git a/core/src/main/java/org/bitcoinj/params/UnitTestParams.java b/core/src/main/java/org/bitcoinj/params/UnitTestParams.java index d4c0f4cc3..54807ee2c 100644 --- a/core/src/main/java/org/bitcoinj/params/UnitTestParams.java +++ b/core/src/main/java/org/bitcoinj/params/UnitTestParams.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Google Inc. + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,8 +48,10 @@ public class UnitTestParams extends AbstractBitcoinNetParams { subsidyDecreaseBlockCount = 100; dnsSeeds = null; addrSeeds = null; - bip32HeaderPub = 0x043587CF; - bip32HeaderPriv = 0x04358394; + bip32HeaderP2PKHpub = 0x043587cf; // The 4 byte header that serializes in base58 to "tpub". + bip32HeaderP2PKHpriv = 0x04358394; // The 4 byte header that serializes in base58 to "tprv" + bip32HeaderP2WPKHpub = 0x045f1cf6; // The 4 byte header that serializes in base58 to "vpub". + bip32HeaderP2WPKHpriv = 0x045f18bc; // The 4 byte header that serializes in base58 to "vprv" majorityEnforceBlockUpgrade = 3; majorityRejectBlockOutdated = 4; diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index d099f9dbb..89a07d9f5 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -387,6 +387,8 @@ public class Script { if (ScriptPattern.isPayToPubKeyHash(this)) { checkArgument(key != null, "Key required to create P2PKH input script"); return ScriptBuilder.createInputScript(null, key); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(this)) { + return ScriptBuilder.createEmpty(); } else if (ScriptPattern.isPayToPubKey(this)) { return ScriptBuilder.createInputScript(null); } else if (ScriptPattern.isPayToScriptHash(this)) { @@ -397,6 +399,18 @@ public class Script { } } + public TransactionWitness createEmptyWitness(ECKey key) { + if (ScriptPattern.isPayToWitnessPubKeyHash(this)) { + checkArgument(key != null, "Key required to create P2WPKH witness"); + return TransactionWitness.EMPTY; + } else if (ScriptPattern.isPayToPubKey(this) || ScriptPattern.isPayToPubKeyHash(this) + || ScriptPattern.isPayToScriptHash(this)) { + return null; // no witness + } else { + throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Do not understand script type: " + this); + } + } + /** * Returns a copy of the given scriptSig with the signature inserted in the given position. */ @@ -607,6 +621,9 @@ public class Script { // scriptSig: int uncompressedPubKeySize = 65; return SIG_SIZE + (pubKey != null ? pubKey.getPubKey().length : uncompressedPubKeySize); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(this)) { + // scriptSig is empty + return 0; } else { throw new IllegalStateException("Unsupported script type"); } @@ -744,7 +761,7 @@ public class Script { /** * Exposes the script interpreter. Normally you should not use this directly, instead use * {@link TransactionInput#verify(TransactionOutput)} or - * {@link Script#correctlySpends(Transaction, long, Script)}. This method + * {@link Script#correctlySpends(Transaction, int, TransactionWitness, Coin, Script, Set)}. This method * is useful if you need more precise control or access to the final state of the stack. This interface is very * likely to change in future. * @@ -764,7 +781,7 @@ public class Script { /** * Exposes the script interpreter. Normally you should not use this directly, instead use * {@link TransactionInput#verify(TransactionOutput)} or - * {@link Script#correctlySpends(Transaction, long, Script)}. This method + * {@link Script#correctlySpends(Transaction, int, TransactionWitness, Coin, Script, Set)}. This method * is useful if you need more precise control or access to the final state of the stack. This interface is very * likely to change in future. */ @@ -1526,7 +1543,7 @@ public class Script { * Accessing txContainingThis from another thread while this method runs results in undefined behavior. * @param scriptSigIndex The index in txContainingThis of the scriptSig (note: NOT the index of the scriptPubKey). * @param scriptPubKey The connected scriptPubKey containing the conditions needed to claim the value. - * @deprecated Use {@link #correctlySpends(Transaction, long, Script, Set)} + * @deprecated Use {@link #correctlySpends(Transaction, int, TransactionWitness, Coin, Script, Set)} * instead so that verification flags do not change as new verification options * are added. */ @@ -1542,8 +1559,43 @@ public class Script { * Accessing txContainingThis from another thread while this method runs results in undefined behavior. * @param scriptSigIndex The index in txContainingThis of the scriptSig (note: NOT the index of the scriptPubKey). * @param scriptPubKey The connected scriptPubKey containing the conditions needed to claim the value. - * @param verifyFlags Each flag enables one validation rule. If in doubt, use {@link #correctlySpends(Transaction, long, Script)} - * which sets all flags. + * @param witness Transaction witness belonging to the transaction input containing this script. Needed for SegWit. + * @param value Value of the output. Needed for SegWit scripts. + * @param verifyFlags Each flag enables one validation rule. + */ + public void correctlySpends(Transaction txContainingThis, int scriptSigIndex, @Nullable TransactionWitness witness, @Nullable Coin value, + Script scriptPubKey, Set verifyFlags) throws ScriptException { + if (ScriptPattern.isPayToWitnessPubKeyHash(scriptPubKey)) { + // For SegWit, full validation isn't implemented. So we simply check the signature. P2SH_P2WPKH is handled + // by the P2SH code for now. + if (witness.getPushCount() < 2) + throw new ScriptException(ScriptError.SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY, witness.toString()); + TransactionSignature signature; + try { + signature = TransactionSignature.decodeFromBitcoin(witness.getPush(0), true, true); + } catch (SignatureDecodeException x) { + throw new ScriptException(ScriptError.SCRIPT_ERR_SIG_DER, "Cannot decode", x); + } + ECKey pubkey = ECKey.fromPublicOnly(witness.getPush(1)); + Script scriptCode = new ScriptBuilder().data(ScriptBuilder.createP2PKHOutputScript(pubkey).getProgram()) + .build(); + Sha256Hash sigHash = txContainingThis.hashForSignatureWitness(scriptSigIndex, scriptCode, value, + signature.sigHashMode(), false); + boolean validSig = pubkey.verify(sigHash, signature); + if (!validSig) + throw new ScriptException(ScriptError.SCRIPT_ERR_CHECKSIGVERIFY, "Invalid signature"); + } else { + correctlySpends(txContainingThis, scriptSigIndex, scriptPubKey, verifyFlags); + } + } + + /** + * Verifies that this script (interpreted as a scriptSig) correctly spends the given scriptPubKey. + * @param txContainingThis The transaction in which this input scriptSig resides. + * Accessing txContainingThis from another thread while this method runs results in undefined behavior. + * @param scriptSigIndex The index in txContainingThis of the scriptSig (note: NOT the index of the scriptPubKey). + * @param scriptPubKey The connected scriptPubKey containing the conditions needed to claim the value. + * @param verifyFlags Each flag enables one validation rule. */ public void correctlySpends(Transaction txContainingThis, long scriptSigIndex, Script scriptPubKey, Set verifyFlags) throws ScriptException { diff --git a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java index 7f8021a81..78e8ea134 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java @@ -1,6 +1,7 @@ /* * Copyright 2013 Google Inc. * Copyright 2018 Nicola Atzei + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -253,6 +254,11 @@ public class ScriptBuilder { return new Script(chunks); } + /** Creates an empty script. */ + public static Script createEmpty() { + return new ScriptBuilder().build(); + } + /** Creates a scriptPubKey that encodes payment to the given address. */ public static Script createOutputScript(Address to) { if (to instanceof LegacyAddress) { diff --git a/core/src/main/java/org/bitcoinj/signers/CustomTransactionSigner.java b/core/src/main/java/org/bitcoinj/signers/CustomTransactionSigner.java index 20fe991dd..726d18e5d 100644 --- a/core/src/main/java/org/bitcoinj/signers/CustomTransactionSigner.java +++ b/core/src/main/java/org/bitcoinj/signers/CustomTransactionSigner.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Kosta Korenkov + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +71,8 @@ public abstract class CustomTransactionSigner implements TransactionSigner { // We assume if its already signed, its 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, i, txIn.getConnectedOutput().getScriptPubKey()); + txIn.getScriptSig().correctlySpends(tx, i, txIn.getWitness(), txOut.getValue(), txOut.getScriptPubKey(), + Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); continue; } catch (ScriptException e) { diff --git a/core/src/main/java/org/bitcoinj/signers/LocalTransactionSigner.java b/core/src/main/java/org/bitcoinj/signers/LocalTransactionSigner.java index 62e96e08c..98ab4763b 100644 --- a/core/src/main/java/org/bitcoinj/signers/LocalTransactionSigner.java +++ b/core/src/main/java/org/bitcoinj/signers/LocalTransactionSigner.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Kosta Korenkov + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +18,20 @@ package org.bitcoinj.signers; import java.util.EnumSet; + +import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptException; +import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.script.Script.VerifyFlag; import org.bitcoinj.wallet.KeyBag; import org.bitcoinj.wallet.RedeemData; @@ -63,16 +71,19 @@ public class LocalTransactionSigner implements TransactionSigner { int numInputs = tx.getInputs().size(); for (int i = 0; i < numInputs; i++) { TransactionInput txIn = tx.getInput(i); - if (txIn.getConnectedOutput() == null) { + final TransactionOutput connectedOutput = txIn.getConnectedOutput(); + if (connectedOutput == null) { log.warn("Missing connected output, assuming input {} is already signed.", i); continue; } + Script scriptPubKey = connectedOutput.getScriptPubKey(); try { // We assume if its already signed, its 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, i, txIn.getConnectedOutput().getScriptPubKey(), MINIMUM_VERIFY_FLAGS); + txIn.getScriptSig().correctlySpends(tx, i, txIn.getWitness(), connectedOutput.getValue(), + connectedOutput.getScriptPubKey(), MINIMUM_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); continue; } catch (ScriptException e) { @@ -81,8 +92,6 @@ public class LocalTransactionSigner implements TransactionSigner { RedeemData redeemData = txIn.getConnectedRedeemData(keyBag); - Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); - // For P2SH inputs we need to share derivation path of the signing key with other signers, so that they // use correct key to calculate their signatures. // Married keys all have the same derivation path, so we can safely just take first one here. @@ -104,18 +113,35 @@ public class LocalTransactionSigner implements TransactionSigner { // a CHECKMULTISIG program for P2SH inputs byte[] script = redeemData.redeemScript.getProgram(); try { - TransactionSignature signature = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, false); + if (ScriptPattern.isPayToPubKey(scriptPubKey) || ScriptPattern.isPayToPubKeyHash(scriptPubKey) + || ScriptPattern.isPayToScriptHash(scriptPubKey)) { + TransactionSignature signature = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, + false); - // at this point we have incomplete inputScript with OP_0 in place of one or more signatures. We already - // have calculated the signature using the local key and now need to insert it in the correct place - // within inputScript. For P2PKH and P2PK script there is only one signature and it always - // goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to figure out our relative - // position relative to other signers. Since we don't have that information at this point, and since - // we always run first, we have to depend on the other signers rearranging the signatures as needed. - // Therefore, always place as first signature. - int sigIndex = 0; - inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), sigIndex); - txIn.setScriptSig(inputScript); + // at this point we have incomplete inputScript with OP_0 in place of one or more signatures. We + // already have calculated the signature using the local key and now need to insert it in the + // correct place within inputScript. For P2PKH and P2PK script there is only one signature and it + // always goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to figure out + // our relative position relative to other signers. Since we don't have that information at this + // point, and since we always run first, we have to depend on the other signers rearranging the + // signatures as needed. Therefore, always place as first signature. + int sigIndex = 0; + inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), + sigIndex); + txIn.setScriptSig(inputScript); + txIn.setWitness(null); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(scriptPubKey)) { + Script scriptCode = new ScriptBuilder().data( + ScriptBuilder.createOutputScript(LegacyAddress.fromKey(tx.getParams(), key)).getProgram()) + .build(); + Coin value = txIn.getValue(); + TransactionSignature signature = tx.calculateWitnessSignature(i, key, scriptCode, value, + Transaction.SigHash.ALL, false); + txIn.setScriptSig(ScriptBuilder.createEmpty()); + txIn.setWitness(TransactionWitness.redeemP2WPKH(signature, key)); + } else { + throw new IllegalStateException(script.toString()); + } } catch (ECKey.KeyIsEncryptedException e) { throw e; } catch (ECKey.MissingPrivateKeyException e) { diff --git a/core/src/main/java/org/bitcoinj/signers/MissingSigResolutionSigner.java b/core/src/main/java/org/bitcoinj/signers/MissingSigResolutionSigner.java index befde006e..82b403020 100644 --- a/core/src/main/java/org/bitcoinj/signers/MissingSigResolutionSigner.java +++ b/core/src/main/java/org/bitcoinj/signers/MissingSigResolutionSigner.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Kosta Korenkov + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +19,7 @@ package org.bitcoinj.signers; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptChunk; @@ -80,7 +82,7 @@ public class MissingSigResolutionSigner implements TransactionSigner { } } } - } else { + } else if (ScriptPattern.isPayToPubKey(scriptPubKey) || ScriptPattern.isPayToPubKeyHash(scriptPubKey)) { if (inputScript.getChunks().get(0).equalsOpCode(0)) { if (missingSigsMode == Wallet.MissingSigsMode.THROW) { throw new ECKey.MissingPrivateKeyException(); @@ -88,8 +90,20 @@ public class MissingSigResolutionSigner implements TransactionSigner { txIn.setScriptSig(scriptPubKey.getScriptSigWithSignature(inputScript, dummySig, 0)); } } + } else if (ScriptPattern.isPayToWitnessPubKeyHash(scriptPubKey)) { + if (txIn.getWitness() == null || txIn.getWitness().equals(TransactionWitness.EMPTY) + || txIn.getWitness().getPush(0).length == 0) { + if (missingSigsMode == Wallet.MissingSigsMode.THROW) { + throw new ECKey.MissingPrivateKeyException(); + } else if (missingSigsMode == Wallet.MissingSigsMode.USE_DUMMY_SIG) { + ECKey key = keyBag.findKeyFromPubKeyHash( + ScriptPattern.extractHashFromPayToWitnessHash(scriptPubKey), Script.ScriptType.P2WPKH); + txIn.setWitness(TransactionWitness.redeemP2WPKH(TransactionSignature.dummy(), key)); + } + } + } else { + throw new IllegalStateException("cannot handle: " + scriptPubKey); } - // TODO handle non-P2SH multisig } return true; } diff --git a/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java index e71c018f0..31bbd27eb 100644 --- a/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/BasicKeyChain.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Google Inc. + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,19 +205,19 @@ public class BasicKeyChain implements EncryptableKeyChain { } } - public ECKey findKeyFromPubHash(byte[] pubkeyHash) { + public ECKey findKeyFromPubHash(byte[] pubKeyHash) { lock.lock(); try { - return hashToKeys.get(ByteString.copyFrom(pubkeyHash)); + return hashToKeys.get(ByteString.copyFrom(pubKeyHash)); } finally { lock.unlock(); } } - public ECKey findKeyFromPubKey(byte[] pubkey) { + public ECKey findKeyFromPubKey(byte[] pubKey) { lock.lock(); try { - return pubkeyToKeys.get(ByteString.copyFrom(pubkey)); + return pubkeyToKeys.get(ByteString.copyFrom(pubKey)); } finally { lock.unlock(); } @@ -625,7 +626,7 @@ public class BasicKeyChain implements EncryptableKeyChain { List keys = getKeys(); Collections.sort(keys, ECKey.AGE_COMPARATOR); for (ECKey key : keys) - key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params); + key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params, null); return builder.toString(); } } diff --git a/core/src/main/java/org/bitcoinj/wallet/DecryptingKeyBag.java b/core/src/main/java/org/bitcoinj/wallet/DecryptingKeyBag.java index 53a883c84..bf51a4c04 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DecryptingKeyBag.java +++ b/core/src/main/java/org/bitcoinj/wallet/DecryptingKeyBag.java @@ -17,6 +17,7 @@ package org.bitcoinj.wallet; import org.bitcoinj.core.ECKey; +import org.bitcoinj.script.Script; import org.bouncycastle.crypto.params.KeyParameter; import javax.annotation.Nullable; @@ -63,8 +64,8 @@ public class DecryptingKeyBag implements KeyBag { @Nullable @Override - public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash) { - return maybeDecrypt(target.findKeyFromPubKeyHash(pubKeyHash)); + public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType) { + return maybeDecrypt(target.findKeyFromPubKeyHash(pubKeyHash, scriptType)); } @Nullable diff --git a/core/src/main/java/org/bitcoinj/wallet/DefaultKeyChainFactory.java b/core/src/main/java/org/bitcoinj/wallet/DefaultKeyChainFactory.java index af2fdd4ae..1c58d13e1 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DefaultKeyChainFactory.java +++ b/core/src/main/java/org/bitcoinj/wallet/DefaultKeyChainFactory.java @@ -1,5 +1,6 @@ /* * Copyright 2014 devrandom + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ package org.bitcoinj.wallet; import org.bitcoinj.crypto.*; +import org.bitcoinj.script.Script; import com.google.common.collect.ImmutableList; @@ -26,36 +28,39 @@ import com.google.common.collect.ImmutableList; public class DefaultKeyChainFactory implements KeyChainFactory { @Override public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, - KeyCrypter crypter, boolean isMarried, ImmutableList accountPath) { + KeyCrypter crypter, boolean isMarried, Script.ScriptType outputScriptType, + ImmutableList accountPath) { DeterministicKeyChain chain; if (isMarried) - chain = new MarriedKeyChain(seed, crypter, accountPath); + chain = new MarriedKeyChain(seed, crypter, outputScriptType, accountPath); else - chain = new DeterministicKeyChain(seed, crypter, accountPath); + chain = new DeterministicKeyChain(seed, crypter, outputScriptType, accountPath); return chain; } @Override - public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, - boolean isFollowingKey, boolean isMarried) throws UnreadableWalletException { + public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, + DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried, Script.ScriptType outputScriptType) + throws UnreadableWalletException { DeterministicKeyChain chain; if (isMarried) - chain = new MarriedKeyChain(accountKey); + chain = new MarriedKeyChain(accountKey, outputScriptType); else if (isFollowingKey) - chain = DeterministicKeyChain.builder().watchAndFollow(accountKey).build(); + chain = DeterministicKeyChain.builder().watchAndFollow(accountKey).outputScriptType(outputScriptType).build(); else - chain = DeterministicKeyChain.builder().watch(accountKey).build(); + chain = DeterministicKeyChain.builder().watch(accountKey).outputScriptType(outputScriptType).build(); return chain; } @Override - public DeterministicKeyChain makeSpendingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, - boolean isMarried) throws UnreadableWalletException { + public DeterministicKeyChain makeSpendingKeyChain(Protos.Key key, Protos.Key firstSubKey, + DeterministicKey accountKey, boolean isMarried, Script.ScriptType outputScriptType) + throws UnreadableWalletException { DeterministicKeyChain chain; if (isMarried) - chain = new MarriedKeyChain(accountKey); + chain = new MarriedKeyChain(accountKey, outputScriptType); else - chain = DeterministicKeyChain.builder().spend(accountKey).build(); + chain = DeterministicKeyChain.builder().spend(accountKey).outputScriptType(outputScriptType).build(); return chain; } } diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index 87944f4a6..477794674 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -105,7 +105,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain { private DeterministicHierarchy hierarchy; @Nullable private DeterministicKey rootKey; @Nullable private DeterministicSeed seed; - @Nullable private final ImmutableList accountPath; + private final Script.ScriptType outputScriptType; + private final ImmutableList accountPath; // Paths through the key tree. External keys are ones that are communicated to other parties. Internal keys are // keys created for change addresses, coinbases, mixing, etc - anything that isn't communicated. The distinction @@ -113,7 +114,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // that feature yet. In future we might hand out different accounts for cases where we wish to hand payers // a payment request that can generate lots of addresses independently. // The account path may be overridden by subclasses. + // m / 0' public static final ImmutableList ACCOUNT_ZERO_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED); + // m / 1' + public static final ImmutableList ACCOUNT_ONE_PATH = ImmutableList.of(ChildNumber.ONE_HARDENED); // m / 44' / 0' / 0' public static final ImmutableList BIP44_ACCOUNT_ZERO_PATH = ImmutableList.of(new ChildNumber(44, true), ChildNumber.ZERO_HARDENED, ChildNumber.ZERO_HARDENED); @@ -167,6 +171,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { protected long creationTimeSecs = 0; protected byte[] entropy; protected DeterministicSeed seed; + protected Script.ScriptType outputScriptType = Script.ScriptType.P2PKH; protected DeterministicKey watchingKey = null; protected boolean isFollowing = false; protected DeterministicKey spendingKey = null; @@ -255,6 +260,11 @@ public class DeterministicKeyChain implements EncryptableKeyChain { return self(); } + public T outputScriptType(Script.ScriptType outputScriptType) { + this.outputScriptType = outputScriptType; + return self(); + } + /** The passphrase to use with the generated mnemonic, or null if you would like to use the default empty string. Currently must be the empty string. */ public T passphrase(String passphrase) { // FIXME support non-empty passphrase @@ -267,7 +277,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { */ public T accountPath(ImmutableList accountPath) { checkState(watchingKey == null, "either watch or accountPath"); - this.accountPath = accountPath; + this.accountPath = checkNotNull(accountPath); return self(); } @@ -280,16 +290,16 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (random != null) // Default passphrase to "" if not specified return new DeterministicKeyChain(new DeterministicSeed(random, bits, getPassphrase()), null, - accountPath); + outputScriptType, accountPath); else if (entropy != null) return new DeterministicKeyChain(new DeterministicSeed(entropy, getPassphrase(), creationTimeSecs), - null, accountPath); + null, outputScriptType, accountPath); else if (seed != null) - return new DeterministicKeyChain(seed, null, accountPath); + return new DeterministicKeyChain(seed, null, outputScriptType, accountPath); else if (watchingKey != null) - return new DeterministicKeyChain(watchingKey, isFollowing, true); + return new DeterministicKeyChain(watchingKey, isFollowing, true, outputScriptType); else if (spendingKey != null) - return new DeterministicKeyChain(spendingKey, false, false); + return new DeterministicKeyChain(spendingKey, false, false, outputScriptType); else throw new IllegalStateException(); } @@ -318,7 +328,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain { * {@link Builder}. *

*/ - public DeterministicKeyChain(DeterministicKey key, boolean isFollowing, boolean isWatching) { + public DeterministicKeyChain(DeterministicKey key, boolean isFollowing, boolean isWatching, + Script.ScriptType outputScriptType) { if (isWatching) checkArgument(key.isPubKeyOnly(), "Private subtrees not currently supported for watching keys: if you got this key from DKC.getWatchingKey() then use .dropPrivate().dropParent() on it first."); else @@ -331,6 +342,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { basicKeyChain.importKey(key); hierarchy = new DeterministicHierarchy(key); this.accountPath = key.getPath(); + this.outputScriptType = outputScriptType; initializeHierarchyUnencrypted(key); this.isFollowing = isFollowing; } @@ -347,7 +359,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain { *

*/ protected DeterministicKeyChain(DeterministicSeed seed, @Nullable KeyCrypter crypter, - ImmutableList accountPath) { + Script.ScriptType outputScriptType, ImmutableList accountPath) { + checkArgument(outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH + || outputScriptType == Script.ScriptType.P2WPKH, "Only P2PKH or P2WPKH allowed."); + this.outputScriptType = outputScriptType != null ? outputScriptType : Script.ScriptType.P2PKH; this.accountPath = accountPath; this.seed = seed; basicKeyChain = new BasicKeyChain(crypter); @@ -370,7 +385,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { * For use in encryption when {@link #toEncrypted(KeyCrypter, KeyParameter)} is called, so that * subclasses can override that method and create an instance of the right class. * - * See also {@link #makeKeyChainFromSeed(DeterministicSeed, ImmutableList)} + * See also {@link #makeKeyChainFromSeed(DeterministicSeed, ImmutableList, Script.ScriptType)} */ protected DeterministicKeyChain(KeyCrypter crypter, KeyParameter aesKey, DeterministicKeyChain chain) { // Can't encrypt a watching chain. @@ -379,6 +394,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted"); this.accountPath = chain.getAccountPath(); + this.outputScriptType = chain.outputScriptType; this.issuedExternalKeys = chain.issuedExternalKeys; this.issuedInternalKeys = chain.issuedInternalKeys; @@ -413,11 +429,12 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } - /** Override in subclasses to use a different account derivation path */ - protected ImmutableList getAccountPath() { - if (accountPath != null) - return accountPath; - return ACCOUNT_ZERO_PATH; + public ImmutableList getAccountPath() { + return accountPath; + } + + public Script.ScriptType getOutputScriptType() { + return outputScriptType; } private DeterministicKey encryptNonLeaf(KeyParameter aesKey, DeterministicKeyChain chain, @@ -755,6 +772,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (key.getParent() != null) { // HD keys inherit the timestamp of their parent if they have one, so no need to serialize it. proto.clearCreationTimestamp(); + } else { + proto.setOutputScriptType(Protos.Key.OutputScriptType.valueOf(outputScriptType.name())); } entries.add(proto.build()); } @@ -778,6 +797,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { int sigsRequiredToSpend = 1; List accountPath = newArrayList(); + Script.ScriptType outputScriptType = Script.ScriptType.P2PKH; PeekingIterator iter = Iterators.peekingIterator(keys.iterator()); while (iter.hasNext()) { Protos.Key key = iter.next(); @@ -835,6 +855,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // Deserialize the public key and path. LazyECPoint pubkey = new LazyECPoint(ECKey.CURVE.getCurve(), key.getPublicKey().toByteArray()); final ImmutableList immutablePath = ImmutableList.copyOf(path); + if (key.hasOutputScriptType()) + outputScriptType = Script.ScriptType.valueOf(key.getOutputScriptType().name()); // Possibly create the chain, if we didn't already do so yet. boolean isWatchingAccountKey = false; boolean isFollowingKey = false; @@ -860,16 +882,17 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (seed == null && key.hasSecretBytes()) { DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, new BigInteger(1, key.getSecretBytes().toByteArray()), null); accountKey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000); - chain = factory.makeSpendingKeyChain(key, iter.peek(), accountKey, isMarried); + chain = factory.makeSpendingKeyChain(key, iter.peek(), accountKey, isMarried, outputScriptType); isSpendingKey = true; } else if (seed == null) { DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null); accountKey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000); - chain = factory.makeWatchingKeyChain(key, iter.peek(), accountKey, isFollowingKey, isMarried); + chain = factory.makeWatchingKeyChain(key, iter.peek(), accountKey, isFollowingKey, isMarried, + outputScriptType); isWatchingAccountKey = true; } else { chain = factory.makeKeyChain(key, iter.peek(), seed, crypter, isMarried, - ImmutableList. builder().addAll(accountPath).build()); + outputScriptType, ImmutableList. builder().addAll(accountPath).build()); chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD; // If the seed is encrypted, then the chain is incomplete at this point. However, we will load // it up below as we parse in the keys. We just need to check at the end that we've loaded @@ -987,7 +1010,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { checkState(seed.isEncrypted()); String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase DeterministicSeed decSeed = seed.decrypt(getKeyCrypter(), passphrase, aesKey); - DeterministicKeyChain chain = makeKeyChainFromSeed(decSeed, getAccountPath()); + DeterministicKeyChain chain = makeKeyChainFromSeed(decSeed, getAccountPath(), outputScriptType); // Now double check that the keys match to catch the case where the key is wrong but padding didn't catch it. if (!chain.getWatchingKey().getPubKeyPoint().equals(getWatchingKey().getPubKeyPoint())) throw new KeyCrypterException("Provided AES key is wrong"); @@ -1014,8 +1037,9 @@ public class DeterministicKeyChain implements EncryptableKeyChain { * Subclasses should override this to create an instance of the subclass instead of a plain DKC. * This is used in encryption/decryption. */ - protected DeterministicKeyChain makeKeyChainFromSeed(DeterministicSeed seed, ImmutableList accountPath) { - return new DeterministicKeyChain(seed, null, accountPath); + protected DeterministicKeyChain makeKeyChainFromSeed(DeterministicSeed seed, ImmutableList accountPath, + Script.ScriptType outputScriptType) { + return new DeterministicKeyChain(seed, null, outputScriptType, accountPath); } @Override @@ -1337,21 +1361,24 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (seed != null) { if (includePrivateKeys) { DeterministicSeed decryptedSeed = seed.isEncrypted() - ? seed.decrypt(getKeyCrypter(), DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey) : seed; + ? seed.decrypt(getKeyCrypter(), DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey) + : seed; final List words = decryptedSeed.getMnemonicCode(); - builder.append("Seed as words: ").append(Utils.SPACE_JOINER.join(words)).append('\n'); - builder.append("Seed as hex: ").append(decryptedSeed.toHexString()).append('\n'); + builder.append("Seed as words: ").append(Utils.SPACE_JOINER.join(words)).append('\n'); + builder.append("Seed as hex: ").append(decryptedSeed.toHexString()).append('\n'); } else { if (seed.isEncrypted()) builder.append("Seed is encrypted\n"); } - builder.append("Seed birthday: ").append(seed.getCreationTimeSeconds()).append(" [") + builder.append("Seed birthday: ").append(seed.getCreationTimeSeconds()).append(" [") .append(Utils.dateTimeFormat(seed.getCreationTimeSeconds() * 1000)).append("]\n"); } else { - builder.append("Key birthday: ").append(watchingKey.getCreationTimeSeconds()).append(" [") + builder.append("Key birthday: ").append(watchingKey.getCreationTimeSeconds()).append(" [") .append(Utils.dateTimeFormat(watchingKey.getCreationTimeSeconds() * 1000)).append("]\n"); } - builder.append("Key to watch: ").append(watchingKey.serializePubB58(params)).append('\n'); + builder.append("Ouput script type: ").append(outputScriptType).append('\n'); + builder.append("Key to watch: ").append(watchingKey.serializePubB58(params, outputScriptType)) + .append('\n'); formatAddresses(includePrivateKeys, aesKey, params, builder); return builder.toString(); } @@ -1359,7 +1386,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { protected void formatAddresses(boolean includePrivateKeys, @Nullable KeyParameter aesKey, NetworkParameters params, StringBuilder builder) { for (ECKey key : getKeys(false, true)) - key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params); + key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params, outputScriptType); } /** The number of signatures required to spend coins received by this keychain. */ diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyBag.java b/core/src/main/java/org/bitcoinj/wallet/KeyBag.java index 7fc624eb1..599741ef5 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyBag.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyBag.java @@ -17,6 +17,7 @@ package org.bitcoinj.wallet; import org.bitcoinj.core.ECKey; +import org.bitcoinj.script.Script; import javax.annotation.Nullable; @@ -26,13 +27,18 @@ import javax.annotation.Nullable; */ public interface KeyBag { /** - * Locates a keypair from the keychain given the hash of the public key. This is needed when finding out which - * key we need to use to redeem a transaction output. + * Locates a keypair from the keychain given the hash of the public key, and (optionally) by usage for a specific + * script type. This is needed when finding out which key we need to use to redeem a transaction output. * - * @return ECKey object or null if no such key was found. + * @param pubKeyHash + * hash of the keypair to look for + * @param scriptType + * only look for given usage (currently {@link Script.ScriptType#P2PKH} or + * {@link Script.ScriptType#P2WPKH}) or {@code null} if we don't care + * @return found key or null if no such key was found. */ @Nullable - ECKey findKeyFromPubKeyHash(byte[] pubKeyHash); + ECKey findKeyFromPubKeyHash(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType); /** * Locates a keypair from the keychain given the raw public key bytes. diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainFactory.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainFactory.java index 86dc87cde..4d415946b 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainFactory.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainFactory.java @@ -1,5 +1,6 @@ /* * Copyright 2014 devrandom + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +21,7 @@ import com.google.common.collect.ImmutableList; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.script.Script; /** * Factory interface for creation keychains while de-serializing a wallet. @@ -33,11 +35,12 @@ public interface KeyChainFactory { * @param seed the seed * @param crypter the encrypted/decrypter * @param isMarried whether the keychain is leading in a marriage - * @param accountPath the specified account path + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving + * @param accountPath account path to generate receiving addresses on */ DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, - KeyCrypter crypter, boolean isMarried, - ImmutableList accountPath); + KeyCrypter crypter, boolean isMarried, Script.ScriptType outputScriptType, + ImmutableList accountPath); /** * Make a watching keychain. @@ -49,9 +52,11 @@ public interface KeyChainFactory { * @param accountKey the account extended public key * @param isFollowingKey whether the keychain is following in a marriage * @param isMarried whether the keychain is leading in a marriage + * @param outputScriptType type of addresses (aka output scripts) to generate for watching */ DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, - boolean isFollowingKey, boolean isMarried) throws UnreadableWalletException; + boolean isFollowingKey, boolean isMarried, Script.ScriptType outputScriptType) + throws UnreadableWalletException; /** * Make a spending keychain. @@ -62,7 +67,8 @@ public interface KeyChainFactory { * @param firstSubKey the protobuf for the first child key (normally the parent of the external subchain) * @param accountKey the account extended public key * @param isMarried whether the keychain is leading in a marriage + * @param outputScriptType type of addresses (aka output scripts) to generate for spending */ DeterministicKeyChain makeSpendingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, - boolean isMarried) throws UnreadableWalletException; + boolean isMarried, Script.ScriptType outputScriptType) throws UnreadableWalletException; } diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 2dca8b475..e21e86788 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -67,18 +67,22 @@ public class KeyChainGroup implements KeyBag { */ public static class Builder { private final NetworkParameters params; + private final KeyChainGroupStructure structure; private final List chains = new LinkedList(); - private Builder(NetworkParameters params) { + private Builder(NetworkParameters params, KeyChainGroupStructure structure) { this.params = params; + this.structure = structure; } /** * Add chain from a random source. + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving */ - public Builder fromRandom() { + public Builder fromRandom(Script.ScriptType outputScriptType) { this.chains.clear(); - DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()).build(); + DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()) + .outputScriptType(outputScriptType).accountPath(structure.accountPathFor(outputScriptType)).build(); this.chains.add(chain); return this; } @@ -86,10 +90,12 @@ public class KeyChainGroup implements KeyBag { /** * Add chain from a given seed. * @param seed deterministic seed to derive all keys from + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving */ - public Builder fromSeed(DeterministicSeed seed) { + public Builder fromSeed(DeterministicSeed seed, Script.ScriptType outputScriptType) { this.chains.clear(); - DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).build(); + DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).outputScriptType(outputScriptType) + .accountPath(structure.accountPathFor(outputScriptType)).build(); this.chains.add(chain); return this; } @@ -144,7 +150,11 @@ public class KeyChainGroup implements KeyBag { } public static KeyChainGroup.Builder builder(NetworkParameters params) { - return new Builder(params); + return new Builder(params, KeyChainGroupStructure.DEFAULT); + } + + public static KeyChainGroup.Builder builder(NetworkParameters params, KeyChainGroupStructure structure) { + return new Builder(params, structure); } private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, @Nullable List chains, @@ -235,6 +245,7 @@ public class KeyChainGroup implements KeyBag { */ public Address currentAddress(KeyChain.KeyPurpose purpose) { DeterministicKeyChain chain = getActiveKeyChain(); + Script.ScriptType outputScriptType = chain.getOutputScriptType(); if (chain.isMarried()) { Address current = currentAddresses.get(purpose); if (current == null) { @@ -242,8 +253,10 @@ public class KeyChainGroup implements KeyBag { currentAddresses.put(purpose, current); } return current; + } else if (outputScriptType == Script.ScriptType.P2PKH || outputScriptType == Script.ScriptType.P2WPKH) { + return Address.fromKey(params, currentKey(purpose), outputScriptType); } else { - return LegacyAddress.fromKey(params, currentKey(purpose)); + throw new IllegalStateException(chain.getOutputScriptType().toString()); } } @@ -289,6 +302,7 @@ public class KeyChainGroup implements KeyBag { */ public Address freshAddress(KeyChain.KeyPurpose purpose) { DeterministicKeyChain chain = getActiveKeyChain(); + Script.ScriptType outputScriptType = chain.getOutputScriptType(); if (chain.isMarried()) { Script outputScript = chain.freshOutputScript(purpose); checkState(ScriptPattern.isPayToScriptHash(outputScript)); // Only handle P2SH for now @@ -297,8 +311,10 @@ public class KeyChainGroup implements KeyBag { maybeLookaheadScripts(); currentAddresses.put(purpose, freshAddress); return freshAddress; + } else if (outputScriptType == Script.ScriptType.P2PKH || outputScriptType == Script.ScriptType.P2WPKH) { + return Address.fromKey(params, freshKey(purpose), outputScriptType); } else { - return LegacyAddress.fromKey(params, freshKey(purpose)); + throw new IllegalStateException(chain.getOutputScriptType().toString()); } } @@ -432,14 +448,20 @@ public class KeyChainGroup implements KeyBag { @Nullable @Override - public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash) { + public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType) { ECKey result; + // BasicKeyChain can mix output script types. if ((result = basic.findKeyFromPubHash(pubKeyHash)) != null) return result; - if (chains != null) - for (DeterministicKeyChain chain : chains) + if (chains != null) { + for (DeterministicKeyChain chain : chains) { + // This check limits DeterministicKeyChain to specific output script usage. + if (scriptType != null && scriptType != chain.getOutputScriptType()) + continue; if ((result = chain.findKeyFromPubHash(pubKeyHash)) != null) return result; + } + } return null; } diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroupStructure.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroupStructure.java new file mode 100644 index 000000000..5b6be5cbb --- /dev/null +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroupStructure.java @@ -0,0 +1,41 @@ +/* + * Copyright by the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.wallet; + +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.script.Script; + +import com.google.common.collect.ImmutableList; + +/** Defines a structure for hierarchical deterministic wallets. */ +public interface KeyChainGroupStructure { + /** Map desired output script type to an account path */ + ImmutableList accountPathFor(Script.ScriptType outputScriptType); + + /** Default {@link KeyChainGroupStructure} implementation. Based on BIP32 "Wallet structure". */ + public static final KeyChainGroupStructure DEFAULT = new KeyChainGroupStructure() { + @Override + public ImmutableList accountPathFor(Script.ScriptType outputScriptType) { + if (outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH) + return DeterministicKeyChain.ACCOUNT_ZERO_PATH; + else if (outputScriptType == Script.ScriptType.P2WPKH) + return DeterministicKeyChain.ACCOUNT_ONE_PATH; + else + throw new IllegalArgumentException(outputScriptType.toString()); + } + }; +} diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyTimeCoinSelector.java b/core/src/main/java/org/bitcoinj/wallet/KeyTimeCoinSelector.java index cf661fc92..c1c2ecb9b 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyTimeCoinSelector.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyTimeCoinSelector.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Google Inc. + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +66,9 @@ public class KeyTimeCoinSelector implements CoinSelector { if (ScriptPattern.isPayToPubKey(scriptPubKey)) { controllingKey = wallet.findKeyFromPubKey(ScriptPattern.extractKeyFromPayToPubKey(scriptPubKey)); } else if (ScriptPattern.isPayToPubKeyHash(scriptPubKey)) { - controllingKey = wallet.findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(scriptPubKey)); + controllingKey = wallet.findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(scriptPubKey), Script.ScriptType.P2PKH); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(scriptPubKey)) { + controllingKey = wallet.findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToWitnessHash(scriptPubKey), Script.ScriptType.P2WPKH); } else { log.info("Skipping tx output {} because it's not of simple form.", output); continue; diff --git a/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java index d5edca33c..3d8f5afd6 100644 --- a/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java @@ -102,14 +102,14 @@ public class MarriedKeyChain extends DeterministicKeyChain { MarriedKeyChain chain; if (random != null) - chain = new MarriedKeyChain(new DeterministicSeed(random, bits, getPassphrase()), null, accountPath); + chain = new MarriedKeyChain(new DeterministicSeed(random, bits, getPassphrase()), null, outputScriptType, accountPath); else if (entropy != null) chain = new MarriedKeyChain(new DeterministicSeed(entropy, getPassphrase(), creationTimeSecs), null, - accountPath); + outputScriptType, accountPath); else if (seed != null) - chain = new MarriedKeyChain(seed, null, accountPath); + chain = new MarriedKeyChain(seed, null, outputScriptType, accountPath); else if (watchingKey != null) - chain = new MarriedKeyChain(watchingKey); + chain = new MarriedKeyChain(watchingKey, outputScriptType); else throw new IllegalStateException(); chain.addFollowingAccountKeys(followingKeys, threshold); @@ -125,16 +125,16 @@ public class MarriedKeyChain extends DeterministicKeyChain { * This constructor is not stable across releases! If you need a stable API, use {@link #builder()} to use a * {@link Builder}. */ - protected MarriedKeyChain(DeterministicKey accountKey) { - super(accountKey, false, true); + protected MarriedKeyChain(DeterministicKey accountKey, Script.ScriptType outputScriptType) { + super(accountKey, false, true, outputScriptType); } /** * This constructor is not stable across releases! If you need a stable API, use {@link #builder()} to use a * {@link Builder}. */ - protected MarriedKeyChain(DeterministicSeed seed, KeyCrypter crypter, ImmutableList accountPath) { - super(seed, crypter, accountPath); + protected MarriedKeyChain(DeterministicSeed seed, KeyCrypter crypter, Script.ScriptType outputScriptType, ImmutableList accountPath) { + super(seed, crypter, outputScriptType, accountPath); } void setFollowingKeyChains(List followingKeyChains) { @@ -189,7 +189,8 @@ public class MarriedKeyChain extends DeterministicKeyChain { for (DeterministicKey key : followingAccountKeys) { checkArgument(key.getPath().size() == getAccountPath().size(), "Following keys have to be account keys"); - DeterministicKeyChain chain = DeterministicKeyChain.builder().watchAndFollow(key).build(); + DeterministicKeyChain chain = DeterministicKeyChain.builder().watchAndFollow(key) + .outputScriptType(getOutputScriptType()).build(); if (lookaheadSize >= 0) chain.setLookaheadSize(lookaheadSize); if (lookaheadThreshold >= 0) diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index 4f92dcaf3..31415bda8 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -2779,6 +2779,23 @@ public final class Protos { * repeated uint32 account_path = 10 [packed = true]; */ int getAccountPath(int index); + + /** + *
+     * Type of addresses (aka output scripts) to generate for receiving.
+     * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + boolean hasOutputScriptType(); + /** + *
+     * Type of addresses (aka output scripts) to generate for receiving.
+     * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + org.bitcoinj.wallet.Protos.Key.OutputScriptType getOutputScriptType(); } /** *
@@ -2808,6 +2825,7 @@ public final class Protos {
       creationTimestamp_ = 0L;
       deterministicSeed_ = com.google.protobuf.ByteString.EMPTY;
       accountPath_ = java.util.Collections.emptyList();
+      outputScriptType_ = 1;
     }
 
     @java.lang.Override
@@ -2935,6 +2953,17 @@ public final class Protos {
               input.popLimit(limit);
               break;
             }
+            case 88: {
+              int rawValue = input.readEnum();
+              org.bitcoinj.wallet.Protos.Key.OutputScriptType value = org.bitcoinj.wallet.Protos.Key.OutputScriptType.valueOf(rawValue);
+              if (value == null) {
+                unknownFields.mergeVarintField(11, rawValue);
+              } else {
+                bitField0_ |= 0x00000200;
+                outputScriptType_ = rawValue;
+              }
+              break;
+            }
           }
         }
       } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@@ -3116,6 +3145,96 @@ public final class Protos {
       // @@protoc_insertion_point(enum_scope:wallet.Key.Type)
     }
 
+    /**
+     * Protobuf enum {@code wallet.Key.OutputScriptType}
+     */
+    public enum OutputScriptType
+        implements com.google.protobuf.ProtocolMessageEnum {
+      /**
+       * P2PKH = 1;
+       */
+      P2PKH(1),
+      /**
+       * P2WPKH = 2;
+       */
+      P2WPKH(2),
+      ;
+
+      /**
+       * P2PKH = 1;
+       */
+      public static final int P2PKH_VALUE = 1;
+      /**
+       * P2WPKH = 2;
+       */
+      public static final int P2WPKH_VALUE = 2;
+
+
+      public final int getNumber() {
+        return value;
+      }
+
+      /**
+       * @deprecated Use {@link #forNumber(int)} instead.
+       */
+      @java.lang.Deprecated
+      public static OutputScriptType valueOf(int value) {
+        return forNumber(value);
+      }
+
+      public static OutputScriptType forNumber(int value) {
+        switch (value) {
+          case 1: return P2PKH;
+          case 2: return P2WPKH;
+          default: return null;
+        }
+      }
+
+      public static com.google.protobuf.Internal.EnumLiteMap
+          internalGetValueMap() {
+        return internalValueMap;
+      }
+      private static final com.google.protobuf.Internal.EnumLiteMap<
+          OutputScriptType> internalValueMap =
+            new com.google.protobuf.Internal.EnumLiteMap() {
+              public OutputScriptType findValueByNumber(int number) {
+                return OutputScriptType.forNumber(number);
+              }
+            };
+
+      public final com.google.protobuf.Descriptors.EnumValueDescriptor
+          getValueDescriptor() {
+        return getDescriptor().getValues().get(ordinal());
+      }
+      public final com.google.protobuf.Descriptors.EnumDescriptor
+          getDescriptorForType() {
+        return getDescriptor();
+      }
+      public static final com.google.protobuf.Descriptors.EnumDescriptor
+          getDescriptor() {
+        return org.bitcoinj.wallet.Protos.Key.getDescriptor().getEnumTypes().get(1);
+      }
+
+      private static final OutputScriptType[] VALUES = values();
+
+      public static OutputScriptType valueOf(
+          com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+        if (desc.getType() != getDescriptor()) {
+          throw new java.lang.IllegalArgumentException(
+            "EnumValueDescriptor is not for this type.");
+        }
+        return VALUES[desc.getIndex()];
+      }
+
+      private final int value;
+
+      private OutputScriptType(int value) {
+        this.value = value;
+      }
+
+      // @@protoc_insertion_point(enum_scope:wallet.Key.OutputScriptType)
+    }
+
     private int bitField0_;
     public static final int TYPE_FIELD_NUMBER = 1;
     private int type_;
@@ -3409,6 +3528,30 @@ public final class Protos {
     }
     private int accountPathMemoizedSerializedSize = -1;
 
+    public static final int OUTPUT_SCRIPT_TYPE_FIELD_NUMBER = 11;
+    private int outputScriptType_;
+    /**
+     * 
+     * Type of addresses (aka output scripts) to generate for receiving.
+     * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public boolean hasOutputScriptType() { + return ((bitField0_ & 0x00000200) == 0x00000200); + } + /** + *
+     * Type of addresses (aka output scripts) to generate for receiving.
+     * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public org.bitcoinj.wallet.Protos.Key.OutputScriptType getOutputScriptType() { + org.bitcoinj.wallet.Protos.Key.OutputScriptType result = org.bitcoinj.wallet.Protos.Key.OutputScriptType.valueOf(outputScriptType_); + return result == null ? org.bitcoinj.wallet.Protos.Key.OutputScriptType.P2PKH : result; + } + private byte memoizedIsInitialized = -1; public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; @@ -3478,6 +3621,9 @@ public final class Protos { for (int i = 0; i < accountPath_.size(); i++) { output.writeUInt32NoTag(accountPath_.get(i)); } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + output.writeEnum(11, outputScriptType_); + } unknownFields.writeTo(output); } @@ -3535,6 +3681,10 @@ public final class Protos { } accountPathMemoizedSerializedSize = dataSize; } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(11, outputScriptType_); + } size += unknownFields.getSerializedSize(); memoizedSize = size; return size; @@ -3598,6 +3748,10 @@ public final class Protos { } result = result && getAccountPathList() .equals(other.getAccountPathList()); + result = result && (hasOutputScriptType() == other.hasOutputScriptType()); + if (hasOutputScriptType()) { + result = result && outputScriptType_ == other.outputScriptType_; + } result = result && unknownFields.equals(other.unknownFields); return result; } @@ -3650,6 +3804,10 @@ public final class Protos { hash = (37 * hash) + ACCOUNT_PATH_FIELD_NUMBER; hash = (53 * hash) + getAccountPathList().hashCode(); } + if (hasOutputScriptType()) { + hash = (37 * hash) + OUTPUT_SCRIPT_TYPE_FIELD_NUMBER; + hash = (53 * hash) + outputScriptType_; + } hash = (29 * hash) + unknownFields.hashCode(); memoizedHashCode = hash; return hash; @@ -3812,6 +3970,8 @@ public final class Protos { bitField0_ = (bitField0_ & ~0x00000100); accountPath_ = java.util.Collections.emptyList(); bitField0_ = (bitField0_ & ~0x00000200); + outputScriptType_ = 1; + bitField0_ = (bitField0_ & ~0x00000400); return this; } @@ -3889,6 +4049,10 @@ public final class Protos { bitField0_ = (bitField0_ & ~0x00000200); } result.accountPath_ = accountPath_; + if (((from_bitField0_ & 0x00000400) == 0x00000400)) { + to_bitField0_ |= 0x00000200; + } + result.outputScriptType_ = outputScriptType_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -3970,6 +4134,9 @@ public final class Protos { } onChanged(); } + if (other.hasOutputScriptType()) { + setOutputScriptType(other.getOutputScriptType()); + } this.mergeUnknownFields(other.unknownFields); onChanged(); return this; @@ -4888,6 +5055,58 @@ public final class Protos { onChanged(); return this; } + + private int outputScriptType_ = 1; + /** + *
+       * Type of addresses (aka output scripts) to generate for receiving.
+       * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public boolean hasOutputScriptType() { + return ((bitField0_ & 0x00000400) == 0x00000400); + } + /** + *
+       * Type of addresses (aka output scripts) to generate for receiving.
+       * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public org.bitcoinj.wallet.Protos.Key.OutputScriptType getOutputScriptType() { + org.bitcoinj.wallet.Protos.Key.OutputScriptType result = org.bitcoinj.wallet.Protos.Key.OutputScriptType.valueOf(outputScriptType_); + return result == null ? org.bitcoinj.wallet.Protos.Key.OutputScriptType.P2PKH : result; + } + /** + *
+       * Type of addresses (aka output scripts) to generate for receiving.
+       * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public Builder setOutputScriptType(org.bitcoinj.wallet.Protos.Key.OutputScriptType value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000400; + outputScriptType_ = value.getNumber(); + onChanged(); + return this; + } + /** + *
+       * Type of addresses (aka output scripts) to generate for receiving.
+       * 
+ * + * optional .wallet.Key.OutputScriptType output_script_type = 11; + */ + public Builder clearOutputScriptType() { + bitField0_ = (bitField0_ & ~0x00000400); + outputScriptType_ = 1; + onChanged(); + return this; + } public final Builder setUnknownFields( final com.google.protobuf.UnknownFieldSet unknownFields) { return super.setUnknownFields(unknownFields); @@ -20418,7 +20637,7 @@ public final class Protos { "ode\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkey" + "s\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFol" + "lowing\030\005 \001(\010\022\036\n\023sigsRequiredToSpend\030\006 \001(" + - "\r:\0011\"\264\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wallet.Key." + + "\r:\0011\"\231\004\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wallet.Key." + "Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016encrypted_", "data\030\006 \001(\0132\025.wallet.EncryptedData\022\022\n\npub" + "lic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022creation" + @@ -20426,70 +20645,72 @@ public final class Protos { "\001(\0132\030.wallet.DeterministicKey\022\032\n\022determi" + "nistic_seed\030\010 \001(\014\022;\n\034encrypted_determini" + "stic_seed\030\t \001(\0132\025.wallet.EncryptedData\022\030" + - "\n\014account_path\030\n \003(\rB\002\020\001\"a\n\004Type\022\014\n\010ORIG" + - "INAL\020\001\022\030\n\024ENCRYPTED_SCRYPT_AES\020\002\022\032\n\026DETE" + - "RMINISTIC_MNEMONIC\020\003\022\025\n\021DETERMINISTIC_KE" + - "Y\020\004\"5\n\006Script\022\017\n\007program\030\001 \002(\014\022\032\n\022creati", - "on_timestamp\030\002 \002(\003\"\035\n\rScriptWitness\022\014\n\004d" + - "ata\030\001 \003(\014\"\272\001\n\020TransactionInput\022\"\n\032transa" + - "ction_out_point_hash\030\001 \002(\014\022#\n\033transactio" + - "n_out_point_index\030\002 \002(\r\022\024\n\014script_bytes\030" + - "\003 \002(\014\022\020\n\010sequence\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\022&" + - "\n\007witness\030\006 \001(\0132\025.wallet.ScriptWitness\"\177" + - "\n\021TransactionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014sc" + - "ript_bytes\030\002 \002(\014\022!\n\031spent_by_transaction" + - "_hash\030\003 \001(\014\022\"\n\032spent_by_transaction_inde" + - "x\030\004 \001(\005\"\267\003\n\025TransactionConfidence\0220\n\004typ", - "e\030\001 \001(\0162\".wallet.TransactionConfidence.T" + - "ype\022\032\n\022appeared_at_height\030\002 \001(\005\022\036\n\026overr" + - "iding_transaction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)" + - "\n\014broadcast_by\030\006 \003(\0132\023.wallet.PeerAddres" + - "s\022\033\n\023last_broadcasted_at\030\010 \001(\003\0224\n\006source" + - "\030\007 \001(\0162$.wallet.TransactionConfidence.So" + - "urce\"`\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022" + - "\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004D" + - "EAD\020\004\022\017\n\013IN_CONFLICT\020\005\"A\n\006Source\022\022\n\016SOUR" + - "CE_UNKNOWN\020\000\022\022\n\016SOURCE_NETWORK\020\001\022\017\n\013SOUR", - "CE_SELF\020\002\"\303\005\n\013Transaction\022\017\n\007version\030\001 \002" + - "(\005\022\014\n\004hash\030\002 \002(\014\022&\n\004pool\030\003 \001(\0162\030.wallet." + - "Transaction.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nu" + - "pdated_at\030\005 \001(\003\0223\n\021transaction_input\030\006 \003" + - "(\0132\030.wallet.TransactionInput\0225\n\022transact" + - "ion_output\030\007 \003(\0132\031.wallet.TransactionOut" + - "put\022\022\n\nblock_hash\030\010 \003(\014\022 \n\030block_relativ" + - "ity_offsets\030\013 \003(\005\0221\n\nconfidence\030\t \001(\0132\035." + - "wallet.TransactionConfidence\0225\n\007purpose\030" + - "\n \001(\0162\033.wallet.Transaction.Purpose:\007UNKN", - "OWN\022+\n\rexchange_rate\030\014 \001(\0132\024.wallet.Exch" + - "angeRate\022\014\n\004memo\030\r \001(\t\"Y\n\004Pool\022\013\n\007UNSPEN" + - "T\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013" + - "\n\007PENDING\020\020\022\024\n\020PENDING_INACTIVE\020\022\"\243\001\n\007Pu" + - "rpose\022\013\n\007UNKNOWN\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014" + - "KEY_ROTATION\020\002\022\034\n\030ASSURANCE_CONTRACT_CLA" + - "IM\020\003\022\035\n\031ASSURANCE_CONTRACT_PLEDGE\020\004\022\033\n\027A" + - "SSURANCE_CONTRACT_STUB\020\005\022\r\n\tRAISE_FEE\020\006\"" + - "N\n\020ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002" + - " \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"", - "8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021" + - "\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022\014\n" + - "\004data\030\002 \002(\014\"\261\004\n\006Wallet\022\032\n\022network_identi" + - "fier\030\001 \002(\t\022\034\n\024last_seen_block_hash\030\002 \001(\014" + - "\022\036\n\026last_seen_block_height\030\014 \001(\r\022!\n\031last" + - "_seen_block_time_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\013" + - "2\013.wallet.Key\022(\n\013transaction\030\004 \003(\0132\023.wal" + - "let.Transaction\022&\n\016watched_script\030\017 \003(\0132" + - "\016.wallet.Script\022C\n\017encryption_type\030\005 \001(\016" + - "2\035.wallet.Wallet.EncryptionType:\013UNENCRY", - "PTED\0227\n\025encryption_parameters\030\006 \001(\0132\030.wa" + - "llet.ScryptParameters\022\022\n\007version\030\007 \001(\005:\001" + - "1\022$\n\textension\030\n \003(\0132\021.wallet.Extension\022" + - "\023\n\013description\030\013 \001(\t\022\031\n\021key_rotation_tim" + - "e\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013.wallet.Tag\";\n\016En" + - "cryptionType\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPT" + - "ED_SCRYPT_AES\020\002\"R\n\014ExchangeRate\022\022\n\ncoin_" + - "value\030\001 \002(\003\022\022\n\nfiat_value\030\002 \002(\003\022\032\n\022fiat_" + - "currency_code\030\003 \002(\tB\035\n\023org.bitcoinj.wall" + - "etB\006Protos" + "\n\014account_path\030\n \003(\rB\002\020\001\0228\n\022output_scrip" + + "t_type\030\013 \001(\0162\034.wallet.Key.OutputScriptTy" + + "pe\"a\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SC" + + "RYPT_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020\003\022\025", + "\n\021DETERMINISTIC_KEY\020\004\")\n\020OutputScriptTyp" + + "e\022\t\n\005P2PKH\020\001\022\n\n\006P2WPKH\020\002\"5\n\006Script\022\017\n\007pr" + + "ogram\030\001 \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"" + + "\035\n\rScriptWitness\022\014\n\004data\030\001 \003(\014\"\272\001\n\020Trans" + + "actionInput\022\"\n\032transaction_out_point_has" + + "h\030\001 \002(\014\022#\n\033transaction_out_point_index\030\002" + + " \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence\030\004" + + " \001(\r\022\r\n\005value\030\005 \001(\003\022&\n\007witness\030\006 \001(\0132\025.w" + + "allet.ScriptWitness\"\177\n\021TransactionOutput" + + "\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022!\n", + "\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032spen" + + "t_by_transaction_index\030\004 \001(\005\"\267\003\n\025Transac" + + "tionConfidence\0220\n\004type\030\001 \001(\0162\".wallet.Tr" + + "ansactionConfidence.Type\022\032\n\022appeared_at_" + + "height\030\002 \001(\005\022\036\n\026overriding_transaction\030\003" + + " \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcast_by\030\006 \003(" + + "\0132\023.wallet.PeerAddress\022\033\n\023last_broadcast" + + "ed_at\030\010 \001(\003\0224\n\006source\030\007 \001(\0162$.wallet.Tra" + + "nsactionConfidence.Source\"`\n\004Type\022\013\n\007UNK" + + "NOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PENDING\020\002\022\025\n\021NOT", + "_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\022\017\n\013IN_CONFLIC" + + "T\020\005\"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOU" + + "RCE_NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\303\005\n\013Trans" + + "action\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022&\n" + + "\004pool\030\003 \001(\0162\030.wallet.Transaction.Pool\022\021\n" + + "\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021" + + "transaction_input\030\006 \003(\0132\030.wallet.Transac" + + "tionInput\0225\n\022transaction_output\030\007 \003(\0132\031." + + "wallet.TransactionOutput\022\022\n\nblock_hash\030\010" + + " \003(\014\022 \n\030block_relativity_offsets\030\013 \003(\005\0221", + "\n\nconfidence\030\t \001(\0132\035.wallet.TransactionC" + + "onfidence\0225\n\007purpose\030\n \001(\0162\033.wallet.Tran" + + "saction.Purpose:\007UNKNOWN\022+\n\rexchange_rat" + + "e\030\014 \001(\0132\024.wallet.ExchangeRate\022\014\n\004memo\030\r " + + "\001(\t\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010I" + + "NACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PEND" + + "ING_INACTIVE\020\022\"\243\001\n\007Purpose\022\013\n\007UNKNOWN\020\000\022" + + "\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034\n\030A" + + "SSURANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANCE_C" + + "ONTRACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRACT_S", + "TUB\020\005\022\r\n\tRAISE_FEE\020\006\"N\n\020ScryptParameters" + + "\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 " + + "\001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030" + + "\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" " + + "\n\003Tag\022\013\n\003tag\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"\261\004\n\006Wal" + + "let\022\032\n\022network_identifier\030\001 \002(\t\022\034\n\024last_" + + "seen_block_hash\030\002 \001(\014\022\036\n\026last_seen_block" + + "_height\030\014 \001(\r\022!\n\031last_seen_block_time_se" + + "cs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013tr" + + "ansaction\030\004 \003(\0132\023.wallet.Transaction\022&\n\016", + "watched_script\030\017 \003(\0132\016.wallet.Script\022C\n\017" + + "encryption_type\030\005 \001(\0162\035.wallet.Wallet.En" + + "cryptionType:\013UNENCRYPTED\0227\n\025encryption_" + + "parameters\030\006 \001(\0132\030.wallet.ScryptParamete" + + "rs\022\022\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n \003(" + + "\0132\021.wallet.Extension\022\023\n\013description\030\013 \001(" + + "\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003" + + "(\0132\013.wallet.Tag\";\n\016EncryptionType\022\017\n\013UNE" + + "NCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_AES\020\002\"R\n\014" + + "ExchangeRate\022\022\n\ncoin_value\030\001 \002(\003\022\022\n\nfiat", + "_value\030\002 \002(\003\022\032\n\022fiat_currency_code\030\003 \002(\t" + + "B\035\n\023org.bitcoinj.walletB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { @@ -20526,7 +20747,7 @@ public final class Protos { internal_static_wallet_Key_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( internal_static_wallet_Key_descriptor, - new java.lang.String[] { "Type", "SecretBytes", "EncryptedData", "PublicKey", "Label", "CreationTimestamp", "DeterministicKey", "DeterministicSeed", "EncryptedDeterministicSeed", "AccountPath", }); + new java.lang.String[] { "Type", "SecretBytes", "EncryptedData", "PublicKey", "Label", "CreationTimestamp", "DeterministicKey", "DeterministicSeed", "EncryptedDeterministicSeed", "AccountPath", "OutputScriptType", }); internal_static_wallet_Script_descriptor = getDescriptor().getMessageTypes().get(4); internal_static_wallet_Script_fieldAccessorTable = new diff --git a/core/src/main/java/org/bitcoinj/wallet/RedeemData.java b/core/src/main/java/org/bitcoinj/wallet/RedeemData.java index 9191b7d63..243a14450 100644 --- a/core/src/main/java/org/bitcoinj/wallet/RedeemData.java +++ b/core/src/main/java/org/bitcoinj/wallet/RedeemData.java @@ -1,5 +1,6 @@ /* * Copyright 2014 Kosta Korenkov + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,12 +53,13 @@ public class RedeemData { } /** - * Creates RedeemData for P2PKH or P2PK input. Provided key is a single private key needed - * to spend such inputs and provided program should be a proper CHECKSIG program. + * Creates RedeemData for P2PKH, P2WPKH or P2PK input. Provided key is a single private key needed + * to spend such inputs. */ - public static RedeemData of(ECKey key, Script program) { - checkArgument(ScriptPattern.isPayToPubKeyHash(program) || ScriptPattern.isPayToPubKey(program)); - return key != null ? new RedeemData(Collections.singletonList(key), program) : null; + public static RedeemData of(ECKey key, Script redeemScript) { + checkArgument(ScriptPattern.isPayToPubKeyHash(redeemScript) + || ScriptPattern.isPayToWitnessPubKeyHash(redeemScript) || ScriptPattern.isPayToPubKey(redeemScript)); + return key != null ? new RedeemData(Collections.singletonList(key), redeemScript) : null; } /** diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 6c4cab1ee..1ea95ef8b 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -26,6 +26,7 @@ import com.google.protobuf.*; import net.jcip.annotations.*; import org.bitcoinj.core.listeners.*; import org.bitcoinj.core.Address; +import org.bitcoinj.core.Base58; import org.bitcoinj.core.AbstractBlockChain; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.BloomFilter; @@ -75,6 +76,7 @@ import org.bouncycastle.crypto.params.*; import javax.annotation.*; import java.io.*; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; @@ -251,18 +253,44 @@ public class Wallet extends BaseTaggableObject * Creates a new, empty wallet with a randomly chosen seed and no transactions. Make sure to provide for sufficient * backup! Any keys will be derived from the seed. If you want to restore a wallet from disk instead, see * {@link #loadFromFile}. + * @param params network parameters + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving */ - public Wallet(NetworkParameters params) { - this(Context.getOrCreate(params)); + public static Wallet createDeterministic(NetworkParameters params, Script.ScriptType outputScriptType) { + return createDeterministic(Context.getOrCreate(params), outputScriptType); } /** * Creates a new, empty wallet with a randomly chosen seed and no transactions. Make sure to provide for sufficient * backup! Any keys will be derived from the seed. If you want to restore a wallet from disk instead, see * {@link #loadFromFile}. + * @param params network parameters + * @deprecated Use {@link #createDeterministic(NetworkParameters, ScriptType)} */ + @Deprecated + public Wallet(NetworkParameters params) { + this(params, KeyChainGroup.builder(params).fromRandom(Script.ScriptType.P2PKH).build()); + } + + /** + * Creates a new, empty wallet with a randomly chosen seed and no transactions. Make sure to provide for sufficient + * backup! Any keys will be derived from the seed. If you want to restore a wallet from disk instead, see + * {@link #loadFromFile}. + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving + */ + public static Wallet createDeterministic(Context context, Script.ScriptType outputScriptType) { + return new Wallet(context, KeyChainGroup.builder(context.getParams()).fromRandom(outputScriptType).build()); + } + + /** + * Creates a new, empty wallet with a randomly chosen seed and no transactions. Make sure to provide for sufficient + * backup! Any keys will be derived from the seed. If you want to restore a wallet from disk instead, see + * {@link #loadFromFile}. + * @deprecated Use {@link #createDeterministic(Context, ScriptType)} + */ + @Deprecated public Wallet(Context context) { - this(context, KeyChainGroup.builder(context.getParams()).fromRandom().build()); + this(context, KeyChainGroup.builder(context.getParams()).fromRandom(Script.ScriptType.P2PKH).build()); } /** @@ -274,14 +302,52 @@ public class Wallet extends BaseTaggableObject return new Wallet(params, KeyChainGroup.createBasic(params)); } + /** + * @param params network parameters + * @param seed deterministic seed + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving + * @return a wallet from a deterministic seed with a default account path + */ + public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed, + Script.ScriptType outputScriptType) { + return fromSeed(params, seed, outputScriptType, KeyChainGroupStructure.DEFAULT); + } + + /** + * @param params network parameters + * @param seed deterministic seed + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving + * @param structure structure for your wallet + * @return a wallet from a deterministic seed with a default account path + */ + public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed, Script.ScriptType outputScriptType, + KeyChainGroupStructure structure) { + return new Wallet(params, KeyChainGroup.builder(params, structure).fromSeed(seed, outputScriptType).build()); + } + /** * @param params network parameters * @param seed deterministic seed * @return a wallet from a deterministic seed with a * {@link DeterministicKeyChain#ACCOUNT_ZERO_PATH 0 hardened path} + * @deprecated Use {@link #fromSeed(NetworkParameters, DeterministicSeed, ScriptType, KeyChainGroupStructure)} */ + @Deprecated public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed) { - DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).build(); + return fromSeed(params, seed, Script.ScriptType.P2PKH); + } + + /** + * @param params network parameters + * @param seed deterministic seed + * @param outputScriptType type of addresses (aka output scripts) to generate for receiving + * @param accountPath account path to generate receiving addresses on + * @return an instance of a wallet from a deterministic seed. + */ + public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed, Script.ScriptType outputScriptType, + ImmutableList accountPath) { + DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).outputScriptType(outputScriptType) + .accountPath(accountPath).build(); return new Wallet(params, KeyChainGroup.builder(params).addChain(chain).build()); } @@ -290,21 +356,34 @@ public class Wallet extends BaseTaggableObject * @param seed deterministic seed * @param accountPath account path * @return an instance of a wallet from a deterministic seed. + * @deprecated Use {@link #fromSeed(NetworkParameters, DeterministicSeed, ScriptType, ImmutableList)} */ + @Deprecated public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed, ImmutableList accountPath) { - DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).accountPath(accountPath).build(); - return new Wallet(params, KeyChainGroup.builder(params).addChain(chain).build()); + return fromSeed(params, seed, Script.ScriptType.P2PKH, accountPath); } /** * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. This HAS * to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ - public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey) { - DeterministicKeyChain chain = DeterministicKeyChain.builder().watch(watchKey).build(); + public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey, + Script.ScriptType outputScriptType) { + DeterministicKeyChain chain = DeterministicKeyChain.builder().watch(watchKey).outputScriptType(outputScriptType) + .build(); return new Wallet(params, KeyChainGroup.builder(params).addChain(chain).build()); } + /** + * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. This HAS + * to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. + * @deprecated Use {@link #fromWatchingKey(NetworkParameters, DeterministicKey, ScriptType)} + */ + @Deprecated + public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey) { + return fromWatchingKey(params, watchKey, Script.ScriptType.P2PKH); + } + /** * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. The * account path is specified. The key is specified in base58 notation and the creation time of the key. If you don't @@ -313,18 +392,30 @@ public class Wallet extends BaseTaggableObject public static Wallet fromWatchingKeyB58(NetworkParameters params, String watchKeyB58, long creationTimeSeconds) { final DeterministicKey watchKey = DeterministicKey.deserializeB58(null, watchKeyB58, params); watchKey.setCreationTimeSeconds(creationTimeSeconds); - return fromWatchingKey(params, watchKey); + return fromWatchingKey(params, watchKey, outputScriptTypeFromB58(params, watchKeyB58)); } /** * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given spending key. This HAS * to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. This wallet can also spend. */ - public static Wallet fromSpendingKey(NetworkParameters params, DeterministicKey spendKey) { - DeterministicKeyChain chain = DeterministicKeyChain.builder().spend(spendKey).build(); + public static Wallet fromSpendingKey(NetworkParameters params, DeterministicKey spendKey, + Script.ScriptType outputScriptType) { + DeterministicKeyChain chain = DeterministicKeyChain.builder().spend(spendKey).outputScriptType(outputScriptType) + .build(); return new Wallet(params, KeyChainGroup.builder(params).addChain(chain).build()); } + /** + * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given spending key. This HAS + * to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. This wallet can also spend. + * @deprecated Use {@link #fromSpendingKey(NetworkParameters, DeterministicKey, ScriptType)} + */ + @Deprecated + public static Wallet fromSpendingKey(NetworkParameters params, DeterministicKey spendKey) { + return fromSpendingKey(params, spendKey, Script.ScriptType.P2PKH); + } + /** * Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given spending key. * The key is specified in base58 notation and the creation time of the key. If you don't know the creation time, @@ -333,7 +424,7 @@ public class Wallet extends BaseTaggableObject public static Wallet fromSpendingKeyB58(NetworkParameters params, String spendingKeyB58, long creationTimeSeconds) { final DeterministicKey spendKey = DeterministicKey.deserializeB58(null, spendingKeyB58, params); spendKey.setCreationTimeSeconds(creationTimeSeconds); - return fromSpendingKey(params, spendKey); + return fromSpendingKey(params, spendKey, outputScriptTypeFromB58(params, spendingKeyB58)); } /** @@ -341,11 +432,12 @@ public class Wallet extends BaseTaggableObject * to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ public static Wallet fromMasterKey(NetworkParameters params, DeterministicKey masterKey, - ChildNumber accountNumber) { + Script.ScriptType outputScriptType, ChildNumber accountNumber) { DeterministicKey accountKey = HDKeyDerivation.deriveChildKey(masterKey, accountNumber); accountKey = accountKey.dropParent(); accountKey.setCreationTimeSeconds(masterKey.getCreationTimeSeconds()); - DeterministicKeyChain chain = DeterministicKeyChain.builder().spend(accountKey).build(); + DeterministicKeyChain chain = DeterministicKeyChain.builder().spend(accountKey) + .outputScriptType(outputScriptType).build(); return new Wallet(params, KeyChainGroup.builder(params).addChain(chain).build()); } @@ -361,6 +453,16 @@ public class Wallet extends BaseTaggableObject return new Wallet(params, group); } + private static Script.ScriptType outputScriptTypeFromB58(NetworkParameters params, String base58) { + int header = ByteBuffer.wrap(Base58.decodeChecked(base58)).getInt(); + if (header == params.getBip32HeaderP2PKHpub() || header == params.getBip32HeaderP2PKHpriv()) + return Script.ScriptType.P2PKH; + else if (header == params.getBip32HeaderP2WPKHpub() || header == params.getBip32HeaderP2WPKHpriv()) + return Script.ScriptType.P2WPKH; + else + throw new IllegalArgumentException(base58.substring(0, 4)); + } + public Wallet(NetworkParameters params, KeyChainGroup keyChainGroup) { this(Context.getOrCreate(params), keyChainGroup); } @@ -590,11 +692,18 @@ public class Wallet extends BaseTaggableObject * {@link #currentReceiveKey()} or {@link #currentReceiveAddress()}. */ public List
getIssuedReceiveAddresses() { - final List keys = getIssuedReceiveKeys(); - List
addresses = new ArrayList<>(keys.size()); - for (ECKey key : keys) - addresses.add(LegacyAddress.fromKey(getParams(), key)); - return addresses; + keyChainGroupLock.lock(); + try { + final DeterministicKeyChain activeKeyChain = keyChainGroup.getActiveKeyChain(); + final List keys = activeKeyChain.getIssuedReceiveKeys(); + final Script.ScriptType outputScriptType = activeKeyChain.getOutputScriptType(); + List
addresses = new ArrayList<>(keys.size()); + for (ECKey key : keys) + addresses.add(Address.fromKey(getParams(), key, outputScriptType)); + return addresses; + } finally { + keyChainGroupLock.unlock(); + } } /** @@ -1015,10 +1124,10 @@ public class Wallet extends BaseTaggableObject */ @Override @Nullable - public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash) { + public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType) { keyChainGroupLock.lock(); try { - return keyChainGroup.findKeyFromPubKeyHash(pubKeyHash); + return keyChainGroup.findKeyFromPubKeyHash(pubKeyHash, scriptType); } finally { keyChainGroupLock.unlock(); } @@ -1040,14 +1149,14 @@ public class Wallet extends BaseTaggableObject public boolean isAddressMine(Address address) { final ScriptType scriptType = address.getOutputScriptType(); if (scriptType == ScriptType.P2PKH || scriptType == ScriptType.P2WPKH) - return isPubKeyHashMine(address.getHash()); + return isPubKeyHashMine(address.getHash(), scriptType); else throw new IllegalArgumentException(address.toString()); } @Override - public boolean isPubKeyHashMine(byte[] pubKeyHash) { - return findKeyFromPubKeyHash(pubKeyHash) != null; + public boolean isPubKeyHashMine(byte[] pubKeyHash, @Nullable Script.ScriptType scriptType) { + return findKeyFromPubKeyHash(pubKeyHash, scriptType) != null; } @Override @@ -1067,7 +1176,7 @@ public class Wallet extends BaseTaggableObject public ECKey findKeyFromAddress(Address address) { final ScriptType scriptType = address.getOutputScriptType(); if (scriptType == ScriptType.P2PKH || scriptType == ScriptType.P2WPKH) - return findKeyFromPubKeyHash(address.getHash()); + return findKeyFromPubKeyHash(address.getHash(), scriptType); else return null; } @@ -1132,6 +1241,9 @@ public class Wallet extends BaseTaggableObject LegacyAddress a = LegacyAddress.fromScriptHash(tx.getParams(), ScriptPattern.extractHashFromPayToScriptHash(script)); keyChainGroup.markP2SHAddressAsUsed(a); + } else if (ScriptPattern.isPayToWitnessHash(script)) { + byte[] pubkeyHash = ScriptPattern.extractHashFromPayToWitnessHash(script); + keyChainGroup.markPubKeyHashAsUsed(pubkeyHash); } } catch (ScriptException e) { // Just means we didn't understand the output of this transaction: ignore it. @@ -4082,16 +4194,19 @@ public class Wallet extends BaseTaggableObject int numInputs = tx.getInputs().size(); for (int i = 0; i < numInputs; i++) { TransactionInput txIn = tx.getInput(i); - if (txIn.getConnectedOutput() == null) { + TransactionOutput connectedOutput = txIn.getConnectedOutput(); + if (connectedOutput == null) { // Missing connected output, assuming already signed. continue; } + Script scriptPubKey = connectedOutput.getScriptPubKey(); try { // We assume if its already signed, its 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, i, txIn.getConnectedOutput().getScriptPubKey()); + txIn.getScriptSig().correctlySpends(tx, i, txIn.getWitness(), connectedOutput.getValue(), + connectedOutput.getScriptPubKey(), Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); continue; } catch (ScriptException e) { @@ -4099,10 +4214,10 @@ public class Wallet extends BaseTaggableObject // Expected. } - Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); checkNotNull(redeemData, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)); + txIn.setWitness(scriptPubKey.createEmptyWitness(redeemData.keys.get(0))); } TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx); @@ -4181,8 +4296,13 @@ public class Wallet extends BaseTaggableObject RedeemData data = findRedeemDataFromScriptHash(ScriptPattern.extractHashFromPayToScriptHash(script)); return data != null && canSignFor(data.redeemScript); } else if (ScriptPattern.isPayToPubKeyHash(script)) { - ECKey key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(script)); + ECKey key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(script), + Script.ScriptType.P2PKH); return key != null && (key.isEncrypted() || key.hasPrivKey()); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(script)) { + ECKey key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToWitnessHash(script), + Script.ScriptType.P2WPKH); + return key != null && (key.isEncrypted() || key.hasPrivKey()) && key.isCompressed(); } else if (ScriptPattern.isSentToMultisig(script)) { for (ECKey pubkey : script.getPubKeys()) { ECKey key = findKeyFromPubKey(pubkey.getPubKey()); @@ -4645,7 +4765,12 @@ public class Wallet extends BaseTaggableObject // before calling, but because this is public API we must still lock again regardless. keyChainGroupLock.lock(); try { - return !watchedScripts.isEmpty(); + if (!watchedScripts.isEmpty()) + return true; + for (DeterministicKeyChain chain : keyChainGroup.chains) + if (chain.getOutputScriptType() == Script.ScriptType.P2WPKH) + return true; + return false; } finally { keyChainGroupLock.unlock(); } @@ -4701,7 +4826,8 @@ public class Wallet extends BaseTaggableObject // Returns true if the output is one that won't be selected by a data element matching in the scriptSig. private boolean isTxOutputBloomFilterable(TransactionOutput out) { Script script = out.getScriptPubKey(); - boolean isScriptTypeSupported = ScriptPattern.isPayToPubKey(script) || ScriptPattern.isPayToScriptHash(script); + boolean isScriptTypeSupported = ScriptPattern.isPayToPubKey(script) || ScriptPattern.isPayToScriptHash(script) + || ScriptPattern.isPayToWitnessPubKeyHash(script) || ScriptPattern.isPayToWitnessScriptHash(script); return (isScriptTypeSupported && myUnspents.contains(out)) || watchedScripts.contains(script); } @@ -4959,7 +5085,12 @@ public class Wallet extends BaseTaggableObject ECKey key = null; Script redeemScript = null; if (ScriptPattern.isPayToPubKeyHash(script)) { - key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(script)); + key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToPubKeyHash(script), + Script.ScriptType.P2PKH); + checkNotNull(key, "Coin selection includes unspendable outputs"); + } else if (ScriptPattern.isPayToWitnessPubKeyHash(script)) { + key = findKeyFromPubKeyHash(ScriptPattern.extractHashFromPayToWitnessHash(script), + Script.ScriptType.P2WPKH); checkNotNull(key, "Coin selection includes unspendable outputs"); } else if (ScriptPattern.isPayToScriptHash(script)) { redeemScript = findRedeemDataFromScriptHash(ScriptPattern.extractHashFromPayToScriptHash(script)).redeemScript; diff --git a/core/src/main/proto/wallet.proto b/core/src/main/proto/wallet.proto index c3495b5e8..628c8bf85 100644 --- a/core/src/main/proto/wallet.proto +++ b/core/src/main/proto/wallet.proto @@ -137,6 +137,13 @@ message Key { // The path to the root. Only applicable to a DETERMINISTIC_MNEMONIC key entry. repeated uint32 account_path = 10 [packed = true]; + + enum OutputScriptType { + P2PKH = 1; + P2WPKH = 2; + } + // Type of addresses (aka output scripts) to generate for receiving. + optional OutputScriptType output_script_type = 11; } message Script { diff --git a/core/src/test/java/org/bitcoinj/core/PeerGroupTest.java b/core/src/test/java/org/bitcoinj/core/PeerGroupTest.java index eff91ea16..10fe5087c 100644 --- a/core/src/test/java/org/bitcoinj/core/PeerGroupTest.java +++ b/core/src/test/java/org/bitcoinj/core/PeerGroupTest.java @@ -22,8 +22,10 @@ import com.google.common.collect.*; import com.google.common.util.concurrent.*; import org.bitcoinj.core.listeners.*; import org.bitcoinj.net.discovery.*; +import org.bitcoinj.script.Script; import org.bitcoinj.testing.*; import org.bitcoinj.utils.*; +import org.bitcoinj.wallet.KeyChainGroupStructure; import org.bitcoinj.wallet.Wallet; import org.junit.*; import org.junit.runner.*; @@ -781,7 +783,7 @@ public class PeerGroupTest extends TestWithPeerGroup { final int NUM_KEYS = 9; // First, grab a load of keys from the wallet, and then recreate it so it forgets that those keys were issued. - Wallet shadow = Wallet.fromSeed(wallet.getParams(), wallet.getKeyChainSeed()); + Wallet shadow = Wallet.fromSeed(wallet.getParams(), wallet.getKeyChainSeed(), Script.ScriptType.P2PKH); List keys = new ArrayList<>(NUM_KEYS); for (int i = 0; i < NUM_KEYS; i++) { keys.add(shadow.freshReceiveKey()); diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index aa5ecd632..309ece856 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -324,18 +324,21 @@ public class TransactionTest { assertEquals(txHex, HEX.encode(tx.bitcoinSerialize())); assertEquals(2, tx.getInputs().size()); assertEquals(2, tx.getOutputs().size()); + TransactionInput txIn0 = tx.getInput(0); + TransactionInput txIn1 = tx.getInput(1); - ECKey key0 = ECKey.fromPrivate( - HEX.decode("bbc27228ddcb9209d7fd6f36b02f7dfa6252af40bb2f1cbc7a557da8027ff866")); + ECKey key0 = ECKey.fromPrivate(HEX.decode("bbc27228ddcb9209d7fd6f36b02f7dfa6252af40bb2f1cbc7a557da8027ff866")); + Script scriptPubKey0 = ScriptBuilder.createP2PKOutputScript(key0); assertEquals("2103c9f4836b9a4f77fc0d81f7bcb01b7f1b35916864b9476c241ce9fc198bd25432ac", - HEX.encode(ScriptBuilder.createP2PKOutputScript(key0).getProgram())); - ECKey key1 = ECKey.fromPrivate( - HEX.decode("619c335025c7f4012e556c2a58b2506e30b8511b53ade95ea316fd8c3286feb9")); - assertEquals("025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357", - key1.getPublicKeyAsHex()); + HEX.encode(scriptPubKey0.getProgram())); + ECKey key1 = ECKey.fromPrivate(HEX.decode("619c335025c7f4012e556c2a58b2506e30b8511b53ade95ea316fd8c3286feb9")); + assertEquals("025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357", key1.getPublicKeyAsHex()); + Script scriptPubKey1 = ScriptBuilder.createP2WPKHOutputScript(key1); + assertEquals("00141d0f172a0ecb48aee1be1f2687d2963ae33f71a1", HEX.encode(scriptPubKey1.getProgram())); + txIn1.connect(new TransactionOutput(TESTNET, null, Coin.COIN.multiply(6), scriptPubKey1.getProgram())); TransactionSignature txSig0 = tx.calculateSignature(0, key0, - ScriptBuilder.createP2PKOutputScript(key0).getProgram(), + scriptPubKey0, Transaction.SigHash.ALL, false); assertEquals("30450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01", HEX.encode(txSig0.encodeToBitcoin())); @@ -345,24 +348,20 @@ public class TransactionTest { HEX.encode(scriptCode.getProgram())); TransactionSignature txSig1 = tx.calculateWitnessSignature(1, key1, - scriptCode, Coin.COIN.multiply(6), + scriptCode, txIn1.getValue(), Transaction.SigHash.ALL, false); assertEquals("304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee" + "01", HEX.encode(txSig1.encodeToBitcoin())); - TransactionInput txIn0 = tx.getInput(0); - txIn0.setScriptSig(new ScriptBuilder() - .data(txSig0.encodeToBitcoin()) - .build()); + assertFalse(correctlySpends(txIn0, scriptPubKey0, 0)); + txIn0.setScriptSig(new ScriptBuilder().data(txSig0.encodeToBitcoin()).build()); + assertTrue(correctlySpends(txIn0, scriptPubKey0, 0)); - TransactionWitness witness = new TransactionWitness(2); - witness.setPush(0, txSig1.encodeToBitcoin()); - witness.setPush(1, key1.getPubKey()); - - TransactionInput txIn1 = tx.getInput(1); - txIn1.setWitness(witness); + assertFalse(correctlySpends(txIn1, scriptPubKey1, 1)); + txIn1.setWitness(TransactionWitness.redeemP2WPKH(txSig1, key1)); // no redeem script for p2wpkh + assertTrue(correctlySpends(txIn1, scriptPubKey1, 1)); String signedTxHex = "01000000" // version + "00" // marker @@ -400,6 +399,7 @@ public class TransactionTest { assertEquals(txHex, HEX.encode(tx.bitcoinSerialize())); assertEquals(1, tx.getInputs().size()); assertEquals(2, tx.getOutputs().size()); + TransactionInput txIn = tx.getInput(0); ECKey key = ECKey.fromPrivate( HEX.decode("eb696a065ef48a2192da5b28b694f87544b30fae8327c4510137a922f32c6dcf")); @@ -415,10 +415,7 @@ public class TransactionTest { assertEquals("a9144733f37cf4db86fbc2efed2500b4f4e49f31202387", HEX.encode(scriptPubKey.getProgram())); - Script scriptCode = new ScriptBuilder() - .data(ScriptBuilder.createOutputScript(LegacyAddress.fromKey(TESTNET, key)) - .getProgram()) - .build(); + Script scriptCode = new ScriptBuilder().data(ScriptBuilder.createP2PKHOutputScript(key).getProgram()).build(); assertEquals("1976a91479091972186c449eb1ded22b78e40d009bdf008988ac", HEX.encode(scriptCode.getProgram())); @@ -429,13 +426,10 @@ public class TransactionTest { + "01", HEX.encode(txSig.encodeToBitcoin())); - TransactionWitness witness = new TransactionWitness(2); - witness.setPush(0, txSig.encodeToBitcoin()); - witness.setPush(1, key.getPubKey()); - - TransactionInput txIn = tx.getInput(0); - txIn.setWitness(witness); + assertFalse(correctlySpends(txIn, scriptPubKey, 0)); + txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key)); txIn.setScriptSig(new ScriptBuilder().data(redeemScript.getProgram()).build()); + assertTrue(correctlySpends(txIn, scriptPubKey, 0)); String signedTxHex = "01000000" // version + "00" // marker @@ -455,6 +449,16 @@ public class TransactionTest { assertEquals(signedTxHex, HEX.encode(tx.bitcoinSerialize())); } + private boolean correctlySpends(TransactionInput txIn, Script scriptPubKey, int inputIndex) { + try { + txIn.getScriptSig().correctlySpends(txIn.getParentTransaction(), inputIndex, txIn.getWitness(), + txIn.getValue(), scriptPubKey, Script.ALL_VERIFY_FLAGS); + return true; + } catch (ScriptException x) { + return false; + } + } + @Test public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() { Transaction tx = FakeTxBuilder.createFakeTx(UNITTEST); diff --git a/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java b/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java index 7c169a1da..875933a7b 100644 --- a/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java @@ -109,12 +109,10 @@ public class WalletProtobufSerializerTest { Wallet wallet1 = roundTrip(myWallet); assertEquals(0, wallet1.getTransactions(true).size()); assertEquals(Coin.ZERO, wallet1.getBalance()); - assertArrayEquals(myKey.getPubKey(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPubKey()); - assertArrayEquals(myKey.getPrivKeyBytes(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPrivKeyBytes()); - assertEquals(myKey.getCreationTimeSeconds(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getCreationTimeSeconds()); + ECKey foundKey = wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash(), null); + assertArrayEquals(myKey.getPubKey(), foundKey.getPubKey()); + assertArrayEquals(myKey.getPrivKeyBytes(), foundKey.getPrivKeyBytes()); + assertEquals(myKey.getCreationTimeSeconds(), foundKey.getCreationTimeSeconds()); assertEquals(mScriptCreationTime, wallet1.getWatchedScripts().get(0).getCreationTimeSeconds()); assertEquals(1, wallet1.getWatchedScripts().size()); @@ -197,8 +195,9 @@ public class WalletProtobufSerializerTest { myWallet = new Wallet(UNITTEST); myWallet.importKey(myKey); Wallet wallet1 = roundTrip(myWallet); - assertArrayEquals(myKey.getPubKey(), wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPubKey()); - assertArrayEquals(myKey.getPrivKeyBytes(), wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPrivKeyBytes()); + ECKey foundKey = wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash(), null); + assertArrayEquals(myKey.getPubKey(), foundKey.getPubKey()); + assertArrayEquals(myKey.getPrivKeyBytes(), foundKey.getPrivKeyBytes()); } } @@ -333,12 +332,10 @@ public class WalletProtobufSerializerTest { Wallet wallet1 = roundTrip(myWallet); assertEquals(0, wallet1.getTransactions(true).size()); assertEquals(Coin.ZERO, wallet1.getBalance()); - assertArrayEquals(myKey.getPubKey(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPubKey()); - assertArrayEquals(myKey.getPrivKeyBytes(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getPrivKeyBytes()); - assertEquals(myKey.getCreationTimeSeconds(), - wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash()).getCreationTimeSeconds()); + ECKey foundKey = wallet1.findKeyFromPubKeyHash(myKey.getPubKeyHash(), null); + assertArrayEquals(myKey.getPubKey(), foundKey.getPubKey()); + assertArrayEquals(myKey.getPrivKeyBytes(), foundKey.getPrivKeyBytes()); + assertEquals(myKey.getCreationTimeSeconds(), foundKey.getCreationTimeSeconds()); } @Test diff --git a/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java b/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java index 27b01be90..0792be0ad 100644 --- a/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/DeterministicKeyChainTest.java @@ -27,6 +27,7 @@ import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.*; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.UnitTestParams; +import org.bitcoinj.script.Script; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.listeners.AbstractKeyChainEventListener; @@ -50,6 +51,7 @@ import static org.junit.Assert.*; public class DeterministicKeyChainTest { private DeterministicKeyChain chain; + private DeterministicKeyChain segwitChain; private DeterministicKeyChain bip44chain; private final byte[] ENTROPY = Sha256Hash.hash("don't use a string seed like this in real life".getBytes()); private static final NetworkParameters UNITTEST = UnitTestParams.get(); @@ -63,13 +65,17 @@ public class DeterministicKeyChainTest { // You should use a random seed instead. The secs constant comes from the unit test file, so we can compare // serialized data properly. long secs = 1389353062L; - chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs).build(); + chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs) + .accountPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH).outputScriptType(Script.ScriptType.P2PKH).build(); chain.setLookaheadSize(10); - assertEquals(secs, checkNotNull(chain.getSeed()).getCreationTimeSeconds()); - bip44chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs).accountPath(BIP44_ACCOUNT_ONE_PATH).build(); + segwitChain = DeterministicKeyChain.builder().entropy(ENTROPY, secs) + .accountPath(DeterministicKeyChain.ACCOUNT_ONE_PATH).outputScriptType(Script.ScriptType.P2WPKH).build(); + segwitChain.setLookaheadSize(10); + + bip44chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs).accountPath(BIP44_ACCOUNT_ONE_PATH) + .outputScriptType(Script.ScriptType.P2PKH).build(); bip44chain.setLookaheadSize(10); - assertEquals(secs, checkNotNull(bip44chain.getSeed()).getCreationTimeSeconds()); } @Test @@ -167,7 +173,8 @@ public class DeterministicKeyChainTest { // Check that we get the right events at the right time. final List> listenerKeys = Lists.newArrayList(); long secs = 1389353062L; - chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs).build(); + chain = DeterministicKeyChain.builder().entropy(ENTROPY, secs).outputScriptType(Script.ScriptType.P2PKH) + .build(); chain.addEventListener(new AbstractKeyChainEventListener() { @Override public void onKeysAdded(List keys) { @@ -247,6 +254,43 @@ public class DeterministicKeyChainTest { assertEquals(oldLookaheadSize, chain.getLookaheadSize()); } + @Test + public void serializeSegwitUnencrypted() throws UnreadableWalletException { + segwitChain.maybeLookAhead(); + DeterministicKey key1 = segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + DeterministicKey key2 = segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + DeterministicKey key3 = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + List keys = segwitChain.serializeToProtobuf(); + // 1 mnemonic/seed, 1 master key, 1 account key, 2 internal keys, 3 derived, 20 lookahead and 5 lookahead threshold. + int numItems = + 1 // mnemonic/seed + + 1 // master key + + 1 // account key + + 2 // ext/int parent keys + + (segwitChain.getLookaheadSize() + segwitChain.getLookaheadThreshold()) * 2 // lookahead zone on each chain + ; + assertEquals(numItems, keys.size()); + + // Get another key that will be lost during round-tripping, to ensure we can derive it again. + DeterministicKey key4 = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + + final String EXPECTED_SERIALIZATION = checkSerialization(keys, "deterministic-wallet-segwit-serialization.txt"); + + // Round trip the data back and forth to check it is preserved. + int oldLookaheadSize = segwitChain.getLookaheadSize(); + segwitChain = DeterministicKeyChain.fromProtobuf(keys, null).get(0); + assertEquals(EXPECTED_SERIALIZATION, protoToString(segwitChain.serializeToProtobuf())); + assertEquals(key1, segwitChain.findKeyFromPubHash(key1.getPubKeyHash())); + assertEquals(key2, segwitChain.findKeyFromPubHash(key2.getPubKeyHash())); + assertEquals(key3, segwitChain.findKeyFromPubHash(key3.getPubKeyHash())); + assertEquals(key4, segwitChain.getKey(KeyChain.KeyPurpose.CHANGE)); + key1.sign(Sha256Hash.ZERO_HASH); + key2.sign(Sha256Hash.ZERO_HASH); + key3.sign(Sha256Hash.ZERO_HASH); + key4.sign(Sha256Hash.ZERO_HASH); + assertEquals(oldLookaheadSize, segwitChain.getLookaheadSize()); + } + @Test public void serializeUnencryptedBIP44() throws UnreadableWalletException { bip44chain.maybeLookAhead(); @@ -358,7 +402,8 @@ public class DeterministicKeyChainTest { assertEquals("xpub69KR9epSNBM59KLuasxMU5CyKytMJjBP5HEZ5p8YoGUCpM6cM9hqxB9DDPCpUUtqmw5duTckvPfwpoWGQUFPmRLpxs5jYiTf2u6xRMcdhDf", pub58); watchingKey = DeterministicKey.deserializeB58(null, pub58, MAINNET); watchingKey.setCreationTimeSeconds(100000); - chain = DeterministicKeyChain.builder().watch(watchingKey).build(); + chain = DeterministicKeyChain.builder().watch(watchingKey).outputScriptType(chain.getOutputScriptType()) + .build(); assertEquals(100000, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); chain.maybeLookAhead(); @@ -394,7 +439,8 @@ public class DeterministicKeyChainTest { DeterministicKey watchingKey = bip44chain.getWatchingKey(); watchingKey = watchingKey.dropPrivateBytes().dropParent(); watchingKey.setCreationTimeSeconds(100000); - chain = DeterministicKeyChain.builder().watch(watchingKey).build(); + chain = DeterministicKeyChain.builder().watch(watchingKey).outputScriptType(bip44chain.getOutputScriptType()) + .build(); assertEquals(100000, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); chain.maybeLookAhead(); @@ -435,7 +481,8 @@ public class DeterministicKeyChainTest { assertEquals("xpub69KR9epJ2Wp6ywiv4Xu5WfBUpX4GLu6D5NUMd4oUkCFoZoRNyk3ZCxfKPDkkGvCPa16dPgEdY63qoyLqEa5TQQy1nmfSmgWcagRzimyV7uA", pub58); watchingKey = DeterministicKey.deserializeB58(null, pub58, MAINNET); watchingKey.setCreationTimeSeconds(100000); - chain = DeterministicKeyChain.builder().watch(watchingKey).build(); + chain = DeterministicKeyChain.builder().watch(watchingKey).outputScriptType(chain1.getOutputScriptType()) + .build(); assertEquals(accountOne, chain.getAccountPath()); assertEquals(100000, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); @@ -461,6 +508,46 @@ public class DeterministicKeyChainTest { assertEquals(key4.getPubKeyPoint(), rekey4.getPubKeyPoint()); } + @Test + public void watchingSegwitChain() throws UnreadableWalletException { + Utils.setMockClock(); + DeterministicKey key1 = segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + DeterministicKey key2 = segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + DeterministicKey key3 = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + DeterministicKey key4 = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + + DeterministicKey watchingKey = segwitChain.getWatchingKey(); + final String pub58 = watchingKey.serializePubB58(MAINNET, segwitChain.getOutputScriptType()); + assertEquals("zpub6nywkzAGfYS2siEfJtm9mo3hwDk8eUtL8EJ31XeWSd7C7x7esnfMMWmWiSs8od5jRt11arTjKLLbxCXuWNSXcxpi9PMSAphMt2ZE2gLnXGE", pub58); + watchingKey = DeterministicKey.deserializeB58(null, pub58, MAINNET); + watchingKey.setCreationTimeSeconds(100000); + segwitChain = DeterministicKeyChain.builder().watch(watchingKey) + .outputScriptType(segwitChain.getOutputScriptType()).build(); + assertEquals(100000, segwitChain.getEarliestKeyCreationTime()); + segwitChain.setLookaheadSize(10); + segwitChain.maybeLookAhead(); + + assertEquals(key1.getPubKeyPoint(), segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); + assertEquals(key2.getPubKeyPoint(), segwitChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); + final DeterministicKey key = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + assertEquals(key3.getPubKeyPoint(), key.getPubKeyPoint()); + try { + // Can't sign with a key from a watching chain. + key.sign(Sha256Hash.ZERO_HASH); + fail(); + } catch (ECKey.MissingPrivateKeyException e) { + // Ignored. + } + // Test we can serialize and deserialize a watching chain OK. + List serialization = segwitChain.serializeToProtobuf(); + checkSerialization(serialization, "watching-wallet-p2wpkh-serialization.txt"); + final DeterministicKeyChain chain = DeterministicKeyChain.fromProtobuf(serialization, null).get(0); + assertEquals(DeterministicKeyChain.ACCOUNT_ONE_PATH, chain.getAccountPath()); + assertEquals(Script.ScriptType.P2WPKH, chain.getOutputScriptType()); + final DeterministicKey rekey4 = segwitChain.getKey(KeyChain.KeyPurpose.CHANGE); + assertEquals(key4.getPubKeyPoint(), rekey4.getPubKeyPoint()); + } + @Test public void spendingChain() throws UnreadableWalletException { Utils.setMockClock(); @@ -475,7 +562,8 @@ public class DeterministicKeyChainTest { assertEquals("xprv9vL4k9HYXonmvqGSUrRM6wGEmx3ruGTXi4JxHRiwEvwDwYmTocPbQNpjN89gpqPrFofmfvALwgnNFBCH2grse1YDf8ERAwgdvbjRtoMfsbV", prv58); watchingKey = DeterministicKey.deserializeB58(null, prv58, params); watchingKey.setCreationTimeSeconds(100000); - chain = DeterministicKeyChain.builder().spend(watchingKey).build(); + chain = DeterministicKeyChain.builder().spend(watchingKey).outputScriptType(chain.getOutputScriptType()) + .build(); assertEquals(100000, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); chain.maybeLookAhead(); @@ -517,7 +605,8 @@ public class DeterministicKeyChainTest { assertEquals("xprv9vL4k9HYXonmzR7UC1ngJ3hTjxkmjLLUo3RexSfUGSWcACHzghWBLJAwW6xzs59XeFizQxFQWtscoTfrF9PSXrUgAtBgr13Nuojax8xTBRz", prv58); watchingKey = DeterministicKey.deserializeB58(null, prv58, params); watchingKey.setCreationTimeSeconds(secs); - chain = DeterministicKeyChain.builder().spend(watchingKey).build(); + chain = DeterministicKeyChain.builder().spend(watchingKey).outputScriptType(chain.getOutputScriptType()) + .build(); assertEquals(accountTwo, chain.getAccountPath()); assertEquals(secs, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); @@ -544,7 +633,8 @@ public class DeterministicKeyChainTest { assertEquals("xprv9yYQhynAmWWuz62PScx5Q2frBET2F1raaXna5A2E9Lj8XWgmKBL7S98Yand8F736j9UCTNWQeiB4yL5pLZP7JDY2tY8eszGQkiKDwBkezeS", prv58); watchingKey = DeterministicKey.deserializeB58(null, prv58, params); watchingKey.setCreationTimeSeconds(secs); - DeterministicKeyChain fromPrivBase58Chain = DeterministicKeyChain.builder().spend(watchingKey).build(); + DeterministicKeyChain fromPrivBase58Chain = DeterministicKeyChain.builder().spend(watchingKey) + .outputScriptType(bip44chain.getOutputScriptType()).build(); assertEquals(secs, fromPrivBase58Chain.getEarliestKeyCreationTime()); fromPrivBase58Chain.setLookaheadSize(10); fromPrivBase58Chain.maybeLookAhead(); @@ -555,8 +645,8 @@ public class DeterministicKeyChainTest { DeterministicKey accountKey = HDKeyDerivation.deriveChildKey(coinLevelKey, new ChildNumber(0, true)); accountKey = accountKey.dropParent(); accountKey.setCreationTimeSeconds(watchingKey.getCreationTimeSeconds()); - KeyChainGroup group = KeyChainGroup.builder(params) - .addChain(DeterministicKeyChain.builder().spend(accountKey).build()).build(); + KeyChainGroup group = KeyChainGroup.builder(params).addChain(DeterministicKeyChain.builder().spend(accountKey) + .outputScriptType(bip44chain.getOutputScriptType()).build()).build(); DeterministicKeyChain fromMasterKeyChain = group.getActiveKeyChain(); assertEquals(BIP44_ACCOUNT_ONE_PATH, fromMasterKeyChain.getAccountPath()); assertEquals(secs, fromMasterKeyChain.getEarliestKeyCreationTime()); @@ -611,7 +701,8 @@ public class DeterministicKeyChainTest { @Test(expected = IllegalStateException.class) public void watchingCannotEncrypt() throws Exception { final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH); - chain = DeterministicKeyChain.builder().watch(accountKey.dropPrivateBytes().dropParent()).build(); + chain = DeterministicKeyChain.builder().watch(accountKey.dropPrivateBytes().dropParent()) + .outputScriptType(chain.getOutputScriptType()).build(); assertEquals(DeterministicKeyChain.ACCOUNT_ZERO_PATH, chain.getAccountPath()); chain = chain.toEncrypted("this doesn't make any sense"); } @@ -642,7 +733,8 @@ public class DeterministicKeyChainTest { DeterministicKey[] keys = new DeterministicKey[100]; for (int i = 0; i < keys.length; i++) keys[i] = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); - chain = DeterministicKeyChain.builder().watch(chain.getWatchingKey().dropPrivateBytes().dropParent()).build(); + chain = DeterministicKeyChain.builder().watch(chain.getWatchingKey().dropPrivateBytes().dropParent()) + .outputScriptType(chain.getOutputScriptType()).build(); int e = chain.numBloomFilterEntries(); BloomFilter filter = chain.getFilter(e, 0.001, 1); for (DeterministicKey key : keys) diff --git a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java index bfbe4c2c0..bffe721e8 100644 --- a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java @@ -25,9 +25,11 @@ import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.*; import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.script.Script; import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.KeyChain.KeyPurpose; import org.bitcoinj.wallet.listeners.KeyChainEventListener; import com.google.common.collect.ImmutableList; @@ -49,6 +51,7 @@ public class KeyChainGroupTest { private static final int LOOKAHEAD_SIZE = 5; private static final NetworkParameters MAINNET = MainNetParams.get(); private static final String XPUB = "xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi"; + private static final byte[] ENTROPY = Sha256Hash.hash("don't use a string seed like this in real life".getBytes()); private KeyChainGroup group; private DeterministicKey watchingAccountKey; @@ -56,7 +59,7 @@ public class KeyChainGroupTest { public void setup() { BriefLogFormatter.init(); Utils.setMockClock(); - group = KeyChainGroup.builder(MAINNET).fromRandom().build(); + group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build(); group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests. group.getActiveKeyChain(); // Force create a chain. @@ -161,16 +164,16 @@ public class KeyChainGroupTest { assertEquals(a, result); result = group.findKeyFromPubKey(b.getPubKey()); assertEquals(b, result); - result = group.findKeyFromPubKeyHash(a.getPubKeyHash()); + result = group.findKeyFromPubKeyHash(a.getPubKeyHash(), null); assertEquals(a, result); - result = group.findKeyFromPubKeyHash(b.getPubKeyHash()); + result = group.findKeyFromPubKeyHash(b.getPubKeyHash(), null); assertEquals(b, result); result = group.findKeyFromPubKey(c.getPubKey()); assertEquals(c, result); - result = group.findKeyFromPubKeyHash(c.getPubKeyHash()); + result = group.findKeyFromPubKeyHash(c.getPubKeyHash(), null); assertEquals(c, result); assertNull(group.findKeyFromPubKey(d.getPubKey())); - assertNull(group.findKeyFromPubKeyHash(d.getPubKeyHash())); + assertNull(group.findKeyFromPubKeyHash(d.getPubKeyHash(), null)); } @Test @@ -302,7 +305,7 @@ public class KeyChainGroupTest { @Test public void encryptionWhilstEmpty() throws Exception { - group = KeyChainGroup.builder(MAINNET).fromRandom().build(); + group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build(); group.setLookaheadSize(5); KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2); final KeyParameter aesKey = scrypt.deriveKey("password"); @@ -455,8 +458,8 @@ public class KeyChainGroupTest { @Test public void serializeWatching() throws Exception { - group = KeyChainGroup.builder(MAINNET) - .addChain(DeterministicKeyChain.builder().watch(watchingAccountKey).build()).build(); + group = KeyChainGroup.builder(MAINNET).addChain(DeterministicKeyChain.builder().watch(watchingAccountKey) + .outputScriptType(Script.ScriptType.P2PKH).build()).build(); group.setLookaheadSize(LOOKAHEAD_SIZE); group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); group.freshKey(KeyChain.KeyPurpose.CHANGE); @@ -494,7 +497,8 @@ public class KeyChainGroupTest { ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); final DeterministicSeed seed = checkNotNull(group.getActiveKeyChain().getSeed()); KeyChainGroup group2 = KeyChainGroup.builder(MAINNET) - .addChain(DeterministicKeyChain.builder().seed(seed).build()).build(); + .addChain(DeterministicKeyChain.builder().seed(seed).outputScriptType(Script.ScriptType.P2PKH).build()) + .build(); group2.setLookaheadSize(5); ECKey key2 = group2.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals(key1, key2); @@ -597,7 +601,7 @@ public class KeyChainGroupTest { @Test public void isNotWatching() { - group = KeyChainGroup.builder(MAINNET).fromRandom().build(); + group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build(); final ECKey key = ECKey.fromPrivate(BigInteger.TEN); group.importKeys(key); assertFalse(group.isWatching()); @@ -608,7 +612,7 @@ public class KeyChainGroupTest { group = KeyChainGroup.builder(MAINNET) .addChain(DeterministicKeyChain.builder().watch(DeterministicKey.deserializeB58( "xpub69bjfJ91ikC5ghsqsVDHNq2dRGaV2HHVx7Y9LXi27LN9BWWAXPTQr4u8U3wAtap8bLdHdkqPpAcZmhMS5SnrMQC4ccaoBccFhh315P4UYzo", - MAINNET)).build()) + MAINNET)).outputScriptType(Script.ScriptType.P2PKH).build()) .build(); final ECKey watchingKey = ECKey.fromPublicOnly(new ECKey().getPubKeyPoint()); group.importKeys(watchingKey); @@ -626,10 +630,46 @@ public class KeyChainGroupTest { group = KeyChainGroup.builder(MAINNET) .addChain(DeterministicKeyChain.builder().watch(DeterministicKey.deserializeB58( "xpub69bjfJ91ikC5ghsqsVDHNq2dRGaV2HHVx7Y9LXi27LN9BWWAXPTQr4u8U3wAtap8bLdHdkqPpAcZmhMS5SnrMQC4ccaoBccFhh315P4UYzo", - MAINNET)).build()) + MAINNET)).outputScriptType(Script.ScriptType.P2PKH).build()) .build(); final ECKey key = ECKey.fromPrivate(BigInteger.TEN); group.importKeys(key); group.isWatching(); } + + @Test + public void segwitKeyChainGroup() throws Exception { + group = KeyChainGroup.builder(MAINNET).addChain(DeterministicKeyChain.builder().entropy(ENTROPY, 0) + .outputScriptType(Script.ScriptType.P2WPKH).accountPath(DeterministicKeyChain.ACCOUNT_ONE_PATH).build()) + .build(); + group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests. + assertEquals(Script.ScriptType.P2WPKH, group.getActiveKeyChain().getOutputScriptType()); + assertEquals("bc1qhcurdec849thpjjp3e27atvya43gy2snrechd9", + group.currentAddress(KeyPurpose.RECEIVE_FUNDS).toString()); + assertEquals("bc1qw8sf3mwuwn74qnhj83gjg0cwkk78fun2pxl9t2", group.currentAddress(KeyPurpose.CHANGE).toString()); + + // round-trip through protobuf + group = KeyChainGroup.fromProtobufUnencrypted(MAINNET, group.serializeToProtobuf()); + assertEquals(Script.ScriptType.P2WPKH, group.getActiveKeyChain().getOutputScriptType()); + assertEquals("bc1qhcurdec849thpjjp3e27atvya43gy2snrechd9", + group.currentAddress(KeyPurpose.RECEIVE_FUNDS).toString()); + assertEquals("bc1qw8sf3mwuwn74qnhj83gjg0cwkk78fun2pxl9t2", group.currentAddress(KeyPurpose.CHANGE).toString()); + + // encryption + KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2); + KeyParameter aesKey = scrypt.deriveKey("password"); + group.encrypt(scrypt, aesKey); + assertEquals(Script.ScriptType.P2WPKH, group.getActiveKeyChain().getOutputScriptType()); + assertEquals("bc1qhcurdec849thpjjp3e27atvya43gy2snrechd9", + group.currentAddress(KeyPurpose.RECEIVE_FUNDS).toString()); + assertEquals("bc1qw8sf3mwuwn74qnhj83gjg0cwkk78fun2pxl9t2", group.currentAddress(KeyPurpose.CHANGE).toString()); + + // round-trip encrypted again, then dectypt + group = KeyChainGroup.fromProtobufEncrypted(MAINNET, group.serializeToProtobuf(), scrypt); + group.decrypt(aesKey); + assertEquals(Script.ScriptType.P2WPKH, group.getActiveKeyChain().getOutputScriptType()); + assertEquals("bc1qhcurdec849thpjjp3e27atvya43gy2snrechd9", + group.currentAddress(KeyPurpose.RECEIVE_FUNDS).toString()); + assertEquals("bc1qw8sf3mwuwn74qnhj83gjg0cwkk78fun2pxl9t2", group.currentAddress(KeyPurpose.CHANGE).toString()); + } } diff --git a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java index accb597a6..dd61402de 100644 --- a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java @@ -27,6 +27,7 @@ import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; @@ -62,6 +63,7 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.ByteString; +import org.bitcoinj.wallet.KeyChain.KeyPurpose; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.junit.After; import org.junit.Before; @@ -178,10 +180,11 @@ public class WalletTest extends TestWithWallet { } @Test - public void encryptDecryptWalletWithArbitraryPath() throws Exception { + public void encryptDecryptWalletWithArbitraryPathAndScriptType() throws Exception { final byte[] ENTROPY = Sha256Hash.hash("don't use a string seed like this in real life".getBytes()); KeyChainGroup keyChainGroup = KeyChainGroup.builder(UNITTEST) .addChain(DeterministicKeyChain.builder().seed(new DeterministicSeed(ENTROPY, "", 1389353062L)) + .outputScriptType(Script.ScriptType.P2WPKH) .accountPath(DeterministicKeyChain.BIP44_ACCOUNT_ZERO_PATH).build()) .build(); Wallet encryptedWallet = new Wallet(UNITTEST, keyChainGroup); @@ -1618,7 +1621,8 @@ public class WalletTest extends TestWithWallet { @Test public void isWatching() { assertFalse(wallet.isWatching()); - Wallet watchingWallet = Wallet.fromWatchingKey(UNITTEST, wallet.getWatchingKey().dropPrivateBytes().dropParent()); + Wallet watchingWallet = Wallet.fromWatchingKey(UNITTEST, + wallet.getWatchingKey().dropPrivateBytes().dropParent(), Script.ScriptType.P2PKH); assertTrue(watchingWallet.isWatching()); wallet.encrypt(PASSWORD1); assertFalse(wallet.isWatching()); @@ -1630,7 +1634,8 @@ public class WalletTest extends TestWithWallet { String serialized = watchKey.serializePubB58(UNITTEST); // Construct watching wallet. - Wallet watchingWallet = Wallet.fromWatchingKey(UNITTEST, DeterministicKey.deserializeB58(null, serialized, UNITTEST)); + Wallet watchingWallet = Wallet.fromWatchingKey(UNITTEST, + DeterministicKey.deserializeB58(null, serialized, UNITTEST), Script.ScriptType.P2PKH); DeterministicKey key2 = watchingWallet.freshReceiveKey(); assertEquals(myKey, key2); @@ -2954,7 +2959,7 @@ public class WalletTest extends TestWithWallet { assertEquals(THREE_CENTS.subtract(tx.getFee()), tx.getValueSentToMe(wallet)); // TX sends to one of our addresses (for now we ignore married wallets). final Address toAddress = tx.getOutput(0).getScriptPubKey().getToAddress(UNITTEST); - final ECKey rotatingToKey = wallet.findKeyFromPubKeyHash(toAddress.getHash()); + final ECKey rotatingToKey = wallet.findKeyFromPubKeyHash(toAddress.getHash(), toAddress.getOutputScriptType()); assertNotNull(rotatingToKey); assertFalse(wallet.isKeyRotating(rotatingToKey)); assertEquals(3, tx.getInputs().size()); @@ -2970,7 +2975,8 @@ public class WalletTest extends TestWithWallet { sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, LegacyAddress.fromKey(UNITTEST, key1)); wallet.doMaintenance(null, true); tx = broadcaster.waitForTransactionAndSucceed(); - assertNotNull(wallet.findKeyFromPubKeyHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash())); + assertNotNull(wallet.findKeyFromPubKeyHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash(), + toAddress.getOutputScriptType())); log.info("Unexpected thing: {}", tx); assertEquals(Coin.valueOf(19300), tx.getFee()); assertEquals(1, tx.getInputs().size()); @@ -3055,7 +3061,7 @@ public class WalletTest extends TestWithWallet { List txns = wallet.doMaintenance(null, false).get(); assertEquals(1, txns.size()); Address output = txns.get(0).getOutput(0).getScriptPubKey().getToAddress(UNITTEST); - ECKey usedKey = wallet.findKeyFromPubKeyHash(output.getHash()); + ECKey usedKey = wallet.findKeyFromPubKeyHash(output.getHash(), output.getOutputScriptType()); assertEquals(goodKey.getCreationTimeSeconds(), usedKey.getCreationTimeSeconds()); assertEquals(goodKey.getCreationTimeSeconds(), wallet.freshReceiveKey().getCreationTimeSeconds()); assertEquals("mrM3TpCnav5YQuVA1xLercCGJH4DXujMtv", LegacyAddress.fromKey(UNITTEST, usedKey).toString()); @@ -3129,7 +3135,8 @@ public class WalletTest extends TestWithWallet { // Delete the sigs for (TransactionInput input : req.tx.getInputs()) input.clearScriptBytes(); - Wallet watching = Wallet.fromWatchingKey(UNITTEST, wallet.getWatchingKey().dropParent().dropPrivateBytes()); + Wallet watching = Wallet.fromWatchingKey(UNITTEST, wallet.getWatchingKey().dropParent().dropPrivateBytes(), + Script.ScriptType.P2PKH); watching.completeTx(SendRequest.forTx(req.tx)); } @@ -3531,4 +3538,39 @@ public class WalletTest extends TestWithWallet { wallet = roundTrip(wallet); assertTrue(wallet.isConsistent()); } + + @Test + public void scriptTypeKeyChainRestrictions() { + // Set up chains: basic chain, P2PKH deterministric chain, P2WPKH deterministic chain. + DeterministicKeyChain p2pkhChain = DeterministicKeyChain.builder().random(new SecureRandom()) + .outputScriptType(Script.ScriptType.P2PKH).build(); + DeterministicKeyChain p2wpkhChain = DeterministicKeyChain.builder().random(new SecureRandom()) + .outputScriptType(Script.ScriptType.P2WPKH).build(); + KeyChainGroup kcg = KeyChainGroup.builder(UNITTEST).addChain(p2pkhChain).addChain(p2wpkhChain).build(); + Wallet wallet = new Wallet(UNITTEST, kcg); + + // Set up one key from each chain. + ECKey importedKey = new ECKey(); + wallet.importKey(importedKey); + ECKey p2pkhKey = p2pkhChain.getKey(KeyPurpose.RECEIVE_FUNDS); + ECKey p2wpkhKey = p2wpkhChain.getKey(KeyPurpose.RECEIVE_FUNDS); + + // Test imported key: it's not limited to script type. + assertTrue(wallet.isAddressMine(LegacyAddress.fromKey(UNITTEST, importedKey))); + assertTrue(wallet.isAddressMine(SegwitAddress.fromKey(UNITTEST, importedKey))); + assertEquals(importedKey, wallet.findKeyFromAddress(LegacyAddress.fromKey(UNITTEST, importedKey))); + assertEquals(importedKey, wallet.findKeyFromAddress(SegwitAddress.fromKey(UNITTEST, importedKey))); + + // Test key from P2PKH chain: it's limited to P2PKH addresses + assertTrue(wallet.isAddressMine(LegacyAddress.fromKey(UNITTEST, p2pkhKey))); + assertFalse(wallet.isAddressMine(SegwitAddress.fromKey(UNITTEST, p2pkhKey))); + assertEquals(p2pkhKey, wallet.findKeyFromAddress(LegacyAddress.fromKey(UNITTEST, p2pkhKey))); + assertNull(wallet.findKeyFromAddress(SegwitAddress.fromKey(UNITTEST, p2pkhKey))); + + // Test key from P2WPKH chain: it's limited to P2WPKH addresses + assertFalse(wallet.isAddressMine(LegacyAddress.fromKey(UNITTEST, p2wpkhKey))); + assertTrue(wallet.isAddressMine(SegwitAddress.fromKey(UNITTEST, p2wpkhKey))); + assertNull(wallet.findKeyFromAddress(LegacyAddress.fromKey(UNITTEST, p2wpkhKey))); + assertEquals(p2wpkhKey, wallet.findKeyFromAddress(SegwitAddress.fromKey(UNITTEST, p2wpkhKey))); + } } diff --git a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-bip44-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-bip44-serialization.txt index c1e6374ca..7323b59c9 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-bip44-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-bip44-serialization.txt @@ -13,6 +13,7 @@ creation_timestamp: 1389353062000 deterministic_key { chain_code: "XL\240FW\203\316\230\334\374J\003\357=\215\001\206\365\207Z\006m\334X`\236,;_\304\000^" } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\370\\\267\361L\021K\214\215\272yRP\234(\304\365\303h\251\250\0236\270\344\210\300\330\363(=\332" diff --git a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-segwit-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-segwit-serialization.txt new file mode 100644 index 000000000..49b12469f --- /dev/null +++ b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-segwit-serialization.txt @@ -0,0 +1,280 @@ +type: DETERMINISTIC_MNEMONIC +secret_bytes: "aerobic toe save section draw warm cute upon raccoon mother priority pilot taste sweet next traffic fatal sword dentist original crisp team caution rebel" +creation_timestamp: 1389353062000 +deterministic_seed: "E\032\356\206\230,\275\263\364=\334^f\307\037\350\321X7R\262z\205\3564\371tp\2639R\342\027 J\266\253\250\320\022\031\233\271~O$\330\260\214\fz\231tI\353\215*\037\355\205\213.\224?" +account_path: 2147483649 + +type: DETERMINISTIC_KEY +secret_bytes: "\270E0\202(\362b\023\276\264\347\226E2\360\221\347\325\233L\203\3276\272\213\2436&\304\373\221\025" +public_key: "\002\342$\253\332\031\352\324q\316M\251}\274\267\370X$\366>Q\316\005\330\376\353f!WHLL\a" +creation_timestamp: 1389353062000 +deterministic_key { + chain_code: "XL\240FW\203\316\230\334\374J\003\357=\215\001\206\365\207Z\006m\334X`\236,;_\304\000^" +} +output_script_type: P2WPKH + +type: DETERMINISTIC_KEY +secret_bytes: "\267y\204N)\270Ysr\315?t\213.g\224\332\354#\262\216\341%\217B\322%v\365\357\301\202" +public_key: "\002\270\032\242\r\210\310\316\361\360s3\233A\374\002\r\035m;w~\032\201\237\367/O5\231\270\216k" +deterministic_key { + chain_code: "\255\v_(\027?\036\353\373\212\346\360\276\364q/\246\351z\331\332a\207\177&\335\206\220\276f+\211" + path: 2147483649 +} + +type: DETERMINISTIC_KEY +secret_bytes: "\322j5\033\375\257\371-\211~d)\"{\30629\3446\025=H\370Q\227\325=\002\240|r\343" +public_key: "\002\237\202\357O\355r\020(]\235\b\3162\263N\331\320\216\246\262\376\260\352\224\313Bw\347~\310\336X" +deterministic_key { + chain_code: "*\210c\235x\377\001\277\b\"\321\363\r*\'ci\310\317\344\220\025\341\v\r\337\034\200H\366\312P" + path: 2147483649 + path: 0 + issued_subkeys: 2 + lookahead_size: 10 + sigsRequiredToSpend: 1 +} + +type: DETERMINISTIC_KEY +secret_bytes: "(USw\200Yh\313F|o\213\240u\265@n\000r\033\305\376\211\a\304D\034\342\316\021\374\207" +public_key: "\002\031\335\304L?`\236\224c\252\305\375\2678\275\342\236\242\230\266C\363\327U\341\340\337GeW\2544" +deterministic_key { + chain_code: "\336m6\270\332/\363\016\340=;\003\225\341S\024\262\332vM\201L\207\342\306=!\352A\f\272{" + path: 2147483649 + path: 1 + issued_subkeys: 1 + lookahead_size: 10 + sigsRequiredToSpend: 1 +} + +type: DETERMINISTIC_KEY +public_key: "\002P\022\037\025\003\264\302/\321\314\243\246(\371\006\245\203\255\363o\237\250\b\233O?\225\316T)n\357" +deterministic_key { + chain_code: "9\032\260\204\177\362\362\310`\243)\2067\351\324O\355\337V\245\375\t\375\214mw\353\355\316\246bA" + path: 2147483649 + path: 0 + path: 0 +} + +type: DETERMINISTIC_KEY +public_key: "\003\246\306%*\023\234\0316\273;9\341\033\370 \341\220\312\216\003\300\316`\201\352\357\327\216\234\0254\031" +deterministic_key { + chain_code: "\335\267\305\177 \255ew\bH\210@\033\267\224f\375\325\205\362`\022.3\224\f\206\260\361z\260\371" + path: 2147483649 + path: 0 + path: 1 +} + +type: DETERMINISTIC_KEY +public_key: "\002\am\036\377\ff8\003\222\351#?\356e\000\367\254i0\324H~\251E/\352B\2616\0030+" +deterministic_key { + chain_code: "{#\231\t\034\023T@\317\322\344^w\254)\350!\327\221\033\263C\347\277zb\301^`\004\277\350" + path: 2147483649 + path: 0 + path: 2 +} + +type: DETERMINISTIC_KEY +public_key: "\003\307\214\031\332\032\205oH\346\272\v\267\n#9ivZ\247\327L)3\254\"\201\331K\340\211\026`" +deterministic_key { + chain_code: "@}\204\033\347\346\270\345\376M\027\263\262h\242\322[l\254\263\351O\334^\273k\003\225\2065\017\031" + path: 2147483649 + path: 0 + path: 3 +} + +type: DETERMINISTIC_KEY +public_key: "\002\025N\\\177\273\325\224\353{5G\273\304J\234t\354\260f\036!L\202\246\361\223\356T.\200\223\206" +deterministic_key { + chain_code: "\230\244\354A\316\272\223j\226\341\023-\327\302>\317\275\022\252\366R\230\312a\310\347u,\004\233\b9" + path: 2147483649 + path: 0 + path: 4 +} + +type: DETERMINISTIC_KEY +public_key: "\002\b\035p\217z\250\".;C\0313>\366\251\"\246Y\307\034\265\273V\202P\207\024\322\316\340$7" +deterministic_key { + chain_code: "W&\026N5\205:1\216d\256m\357\2248\341\227\204\352\177P\265\"\036\v\204p1\177BC\236" + path: 2147483649 + path: 0 + path: 5 +} + +type: DETERMINISTIC_KEY +public_key: "\002\220L\270j\r\270\277\336\016\370\332\203\260\356\322\263<\357$\003\017k\nU<6\235\n\253\230;\205" +deterministic_key { + chain_code: "\025\304d\363\376\aA\275p&_\336\355\374\224\315\264\222m\363m\222S\200\017-\274\330\322\244A\302" + path: 2147483649 + path: 0 + path: 6 +} + +type: DETERMINISTIC_KEY +public_key: "\003\232\370\302\261\006\200\347-\257[\205\v\273\352\v8_\270\304\331Z.Ud\351\264s\251*8_\375" +deterministic_key { + chain_code: "0\345\362I\317=\033I\270\b\322J\316\275D\220\335\334\320\367\275\334\336\367\237l\377\376\3638\203\233" + path: 2147483649 + path: 0 + path: 7 +} + +type: DETERMINISTIC_KEY +public_key: "\002E\317\324\302\345\b\021\275qQ\303\030\257-\352\2744>H\026\340\371\022\"X\025\255\357\367\271\316D" +deterministic_key { + chain_code: "\313\"m\227\025\343o\2252\377\320\200FF~T\322\363\2330\021\251 \276\317\274\0227\322\a96" + path: 2147483649 + path: 0 + path: 8 +} + +type: DETERMINISTIC_KEY +public_key: "\003\376k!\002\320\037\352\030\211?\031/H*\242\326\365\022\374\273\254\323)3\370\1770\340nv\333\244" +deterministic_key { + chain_code: "\315u1\361c\030\266@+\344\225\022\337\034\325\272\223r\251\262\357\211\255.\356\005\031Ds\217,\367" + path: 2147483649 + path: 0 + path: 9 +} + +type: DETERMINISTIC_KEY +public_key: "\002\003O\356\2103\362\253-\016\211\276\023i\233\256\3044\236RP<\253\367\346.R/\330\006\331\017\n" +deterministic_key { + chain_code: "\231\026I\a1\'\350^]THS\323\355\v\347\237\356\302\226\343\216K,omM\237\272\373\234\207" + path: 2147483649 + path: 0 + path: 10 +} + +type: DETERMINISTIC_KEY +public_key: "\0035\304\365\255c\376\260\366\213l\246o\v\342\300\232f\301\306\233\245\373Z\300\230\231\206\201L\231\235\306" +deterministic_key { + chain_code: "\256\216Pe\026\317\313\a\376\031\021\302\207&\363N\271\'\357\275|\334\315\215\272\237\202\231\277\370\264\356" + path: 2147483649 + path: 0 + path: 11 +} + +type: DETERMINISTIC_KEY +public_key: "\002g\340hs\337]\302$\275\225\277\326\233\3016`\027\267Y+\323\376\360\353\266\350X\247\320\273\247\026" +deterministic_key { + chain_code: " \217\242P&\336p\226W\200I+\346\t\213\347d\260-\243\241+\213+\304\365^\227\203\206\231\031" + path: 2147483649 + path: 0 + path: 12 +} + +type: DETERMINISTIC_KEY +public_key: "\002\036\220\032\376\211\327G\236\371\032\276\004l\341\206\235\001\336\2248\314\027\226u*j\251\262c\252\346\265" +deterministic_key { + chain_code: "\363\345\031D\by@\353zGS\267o{ c`c\300NH\006T%miB\210\325x$\306" + path: 2147483649 + path: 1 + path: 0 +} + +type: DETERMINISTIC_KEY +public_key: "\00269\201\317\302t\016Q\247\205n\204\263MI\345X\330da\006>\251\204h\372+u.>\321v" +deterministic_key { + chain_code: "\317j\363\36773\254u\037\3663\243\n-Z\237T\210\263\202u\315\337/\245.H\364\004\253+o" + path: 2147483649 + path: 1 + path: 1 +} + +type: DETERMINISTIC_KEY +public_key: "\002\025#\214(\322\374C\000\234KO\1776\276T\354\217\372g:\340\035c\271\275\375RF`\357\260u" +deterministic_key { + chain_code: "\351\345 \224\354I\201\254\275\207\300\357\240\216\326P\031f\232Ll@\363\207\236\203\027t\350\370Kn" + path: 2147483649 + path: 1 + path: 2 +} + +type: DETERMINISTIC_KEY +public_key: "\002\250+_^\241\353Z%ie\237\233\241.#\322\"\373W\276eJ\255\327\250\024v\016\0340\343\t" +deterministic_key { + chain_code: "\211\220?,\212\350\001\233[>\f\202~&\034\376\206\360\006\314\035\243\261#*r@\"\037\275\004H" +deterministic_key { + chain_code: "\335\030j\262\306\024\fg\346n\025\254\322U\331\323\270\004\017\023\240>\260\307_\343\235\304\222L\212\372" + path: 2147483649 + path: 1 + path: 6 +} + +type: DETERMINISTIC_KEY +public_key: "\003!)\246\275\r\201\246\\\233\260\345\222\215v\371g\314\211\n\0375\203m\305\001`\177 zu4O" +deterministic_key { + chain_code: "\333h\304\004\366\346\351*\366\205\236\377L_\200a\354N\241\235B\272\\\004|\245=\266\324W\352\'" + path: 2147483649 + path: 1 + path: 7 +} + +type: DETERMINISTIC_KEY +public_key: "\003\026o\366\273n\366\371Krn\335\216\246\022T\005\f\265t\377\036\217.\\\bV!\fs\251\365\243" +deterministic_key { + chain_code: "\305\257\344_\027\025\033=>\000 \tm\273\":\265\214\023\267\203me\264*P\222\2247+Ow" + path: 2147483649 + path: 1 + path: 8 +} + +type: DETERMINISTIC_KEY +public_key: "\003\345M0`s\370\3617\n\254\'H<\301}n\326\374\253\240\273wT\t\201r|*\331\361\315\277" +deterministic_key { + chain_code: "V\310\t\226\301\254\f\230\024\332\341a\271hG\331\232\024\266\235\377\004\021\212\016\257\227V\342c\212\203" + path: 2147483649 + path: 1 + path: 9 +} + +type: DETERMINISTIC_KEY +public_key: "\002|h\274DS-\330\035\366o\203x\3343\020q\225\024\005c\326B\200q2\213f\346N?V\330" +deterministic_key { + chain_code: "\233\211\254E\347\036\352\262\027\363WF\324\345\232\247j\277\f\344\031\\\177\263\223j\333?\003\025W\020" + path: 2147483649 + path: 1 + path: 10 +} + +type: DETERMINISTIC_KEY +public_key: "\002\256[\025\037L\371\365\300\357\315\224v\006\r\236\335BIO\210vvT\n\266\243{G\321`\366\377" +deterministic_key { + chain_code: ":\240\372\237\034\033DR1\220\t\022\200\033\307/\200wBy\365\223\244\0170\327\277>Z9\365\277" + path: 2147483649 + path: 1 + path: 11 +} + +type: DETERMINISTIC_KEY +public_key: "\002\375\023ByLoCf\260\230\001\334\373\031\327\325\347\353Ad\253\244\246A=\217vf\034D5\250" +deterministic_key { + chain_code: "\277\265\332jOb\260G6,u\342\263A]\214KF\360\337\217\231:\342x\225\330\250Fa*n" + path: 2147483649 + path: 1 + path: 12 +} \ No newline at end of file diff --git a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt index 3f71fedf9..7bd8a0bbc 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt @@ -11,6 +11,7 @@ creation_timestamp: 1389353062000 deterministic_key { chain_code: "XL\240FW\203\316\230\334\374J\003\357=\215\001\206\365\207Z\006m\334X`\236,;_\304\000^" } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\354B\331\275;\000\254?\3428\006\220G\365\243\333s\260s\213R\313\307\377f\331B\351\327=\001\333" diff --git a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-account-two-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-account-two-serialization.txt index 510f837e2..75fe33630 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-account-two-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-account-two-serialization.txt @@ -6,6 +6,7 @@ deterministic_key { chain_code: "_\377Y\344\324\234\263\356_\002\325\222\241B\307C\302\021L\345f\2731u\364~\325\306\333\367\233\257" path: 2147483650 } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\214E-\031jfj\351\324\206;\304\034v93\035[\251\374\t_@\262\273%q.r\276\016\241" diff --git a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization-two.txt b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization-two.txt index 7c6256ef5..eccbc7ee0 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization-two.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization-two.txt @@ -8,6 +8,7 @@ deterministic_key { path: 2147483649 path: 2147483648 } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\362\305\242\3637\2748Z?]\035s\272\253J\300\033\250\022r\350\020\277U\036K<\335\237\333/\303" diff --git a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization.txt index 67d6f0e0f..1d1c9f7e4 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-from-bip44-serialization.txt @@ -6,6 +6,7 @@ deterministic_key { chain_code: "F\336\2067\377M\026)%\357ZL#\203\320\324\217-\3305\310\244\n\205\277E\323L\250ww\314" path: 2147483648 } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\362\305\242\3637\2748Z?]\035s\272\253J\300\033\250\022r\350\020\277U\036K<\335\237\333/\303" diff --git a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-serialization.txt index 1b008f934..f9a014020 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/spending-wallet-serialization.txt @@ -6,6 +6,7 @@ deterministic_key { chain_code: "\370\017\223\021O?.@gZ|\233j\3437\317q-\241!\177J \323\'\264s\203\314\321\v\346" path: 2147483648 } +output_script_type: P2PKH type: DETERMINISTIC_KEY secret_bytes: "\317\275\022\252\366R\230\312a\310\347u,\004\233\b9" + path: 2147483649 + path: 0 + path: 4 +} + +type: DETERMINISTIC_KEY +public_key: "\002\b\035p\217z\250\".;C\0313>\366\251\"\246Y\307\034\265\273V\202P\207\024\322\316\340$7" +deterministic_key { + chain_code: "W&\026N5\205:1\216d\256m\357\2248\341\227\204\352\177P\265\"\036\v\204p1\177BC\236" + path: 2147483649 + path: 0 + path: 5 +} + +type: DETERMINISTIC_KEY +public_key: "\002\220L\270j\r\270\277\336\016\370\332\203\260\356\322\263<\357$\003\017k\nU<6\235\n\253\230;\205" +deterministic_key { + chain_code: "\025\304d\363\376\aA\275p&_\336\355\374\224\315\264\222m\363m\222S\200\017-\274\330\322\244A\302" + path: 2147483649 + path: 0 + path: 6 +} + +type: DETERMINISTIC_KEY +public_key: "\003\232\370\302\261\006\200\347-\257[\205\v\273\352\v8_\270\304\331Z.Ud\351\264s\251*8_\375" +deterministic_key { + chain_code: "0\345\362I\317=\033I\270\b\322J\316\275D\220\335\334\320\367\275\334\336\367\237l\377\376\3638\203\233" + path: 2147483649 + path: 0 + path: 7 +} + +type: DETERMINISTIC_KEY +public_key: "\002E\317\324\302\345\b\021\275qQ\303\030\257-\352\2744>H\026\340\371\022\"X\025\255\357\367\271\316D" +deterministic_key { + chain_code: "\313\"m\227\025\343o\2252\377\320\200FF~T\322\363\2330\021\251 \276\317\274\0227\322\a96" + path: 2147483649 + path: 0 + path: 8 +} + +type: DETERMINISTIC_KEY +public_key: "\003\376k!\002\320\037\352\030\211?\031/H*\242\326\365\022\374\273\254\323)3\370\1770\340nv\333\244" +deterministic_key { + chain_code: "\315u1\361c\030\266@+\344\225\022\337\034\325\272\223r\251\262\357\211\255.\356\005\031Ds\217,\367" + path: 2147483649 + path: 0 + path: 9 +} + +type: DETERMINISTIC_KEY +public_key: "\002\003O\356\2103\362\253-\016\211\276\023i\233\256\3044\236RP<\253\367\346.R/\330\006\331\017\n" +deterministic_key { + chain_code: "\231\026I\a1\'\350^]THS\323\355\v\347\237\356\302\226\343\216K,omM\237\272\373\234\207" + path: 2147483649 + path: 0 + path: 10 +} + +type: DETERMINISTIC_KEY +public_key: "\0035\304\365\255c\376\260\366\213l\246o\v\342\300\232f\301\306\233\245\373Z\300\230\231\206\201L\231\235\306" +deterministic_key { + chain_code: "\256\216Pe\026\317\313\a\376\031\021\302\207&\363N\271\'\357\275|\334\315\215\272\237\202\231\277\370\264\356" + path: 2147483649 + path: 0 + path: 11 +} + +type: DETERMINISTIC_KEY +public_key: "\002g\340hs\337]\302$\275\225\277\326\233\3016`\027\267Y+\323\376\360\353\266\350X\247\320\273\247\026" +deterministic_key { + chain_code: " \217\242P&\336p\226W\200I+\346\t\213\347d\260-\243\241+\213+\304\365^\227\203\206\231\031" + path: 2147483649 + path: 0 + path: 12 +} + +type: DETERMINISTIC_KEY +public_key: "\002\036\220\032\376\211\327G\236\371\032\276\004l\341\206\235\001\336\2248\314\027\226u*j\251\262c\252\346\265" +deterministic_key { + chain_code: "\363\345\031D\by@\353zGS\267o{ c`c\300NH\006T%miB\210\325x$\306" + path: 2147483649 + path: 1 + path: 0 +} + +type: DETERMINISTIC_KEY +public_key: "\00269\201\317\302t\016Q\247\205n\204\263MI\345X\330da\006>\251\204h\372+u.>\321v" +deterministic_key { + chain_code: "\317j\363\36773\254u\037\3663\243\n-Z\237T\210\263\202u\315\337/\245.H\364\004\253+o" + path: 2147483649 + path: 1 + path: 1 +} + +type: DETERMINISTIC_KEY +public_key: "\002\025#\214(\322\374C\000\234KO\1776\276T\354\217\372g:\340\035c\271\275\375RF`\357\260u" +deterministic_key { + chain_code: "\351\345 \224\354I\201\254\275\207\300\357\240\216\326P\031f\232Ll@\363\207\236\203\027t\350\370Kn" + path: 2147483649 + path: 1 + path: 2 +} + +type: DETERMINISTIC_KEY +public_key: "\002\250+_^\241\353Z%ie\237\233\241.#\322\"\373W\276eJ\255\327\250\024v\016\0340\343\t" +deterministic_key { + chain_code: "\211\220?,\212\350\001\233[>\f\202~&\034\376\206\360\006\314\035\243\261#*r@\"\037\275\004H" +deterministic_key { + chain_code: "\335\030j\262\306\024\fg\346n\025\254\322U\331\323\270\004\017\023\240>\260\307_\343\235\304\222L\212\372" + path: 2147483649 + path: 1 + path: 6 +} + +type: DETERMINISTIC_KEY +public_key: "\003!)\246\275\r\201\246\\\233\260\345\222\215v\371g\314\211\n\0375\203m\305\001`\177 zu4O" +deterministic_key { + chain_code: "\333h\304\004\366\346\351*\366\205\236\377L_\200a\354N\241\235B\272\\\004|\245=\266\324W\352\'" + path: 2147483649 + path: 1 + path: 7 +} + +type: DETERMINISTIC_KEY +public_key: "\003\026o\366\273n\366\371Krn\335\216\246\022T\005\f\265t\377\036\217.\\\bV!\fs\251\365\243" +deterministic_key { + chain_code: "\305\257\344_\027\025\033=>\000 \tm\273\":\265\214\023\267\203me\264*P\222\2247+Ow" + path: 2147483649 + path: 1 + path: 8 +} + +type: DETERMINISTIC_KEY +public_key: "\003\345M0`s\370\3617\n\254\'H<\301}n\326\374\253\240\273wT\t\201r|*\331\361\315\277" +deterministic_key { + chain_code: "V\310\t\226\301\254\f\230\024\332\341a\271hG\331\232\024\266\235\377\004\021\212\016\257\227V\342c\212\203" + path: 2147483649 + path: 1 + path: 9 +} + +type: DETERMINISTIC_KEY +public_key: "\002|h\274DS-\330\035\366o\203x\3343\020q\225\024\005c\326B\200q2\213f\346N?V\330" +deterministic_key { + chain_code: "\233\211\254E\347\036\352\262\027\363WF\324\345\232\247j\277\f\344\031\\\177\263\223j\333?\003\025W\020" + path: 2147483649 + path: 1 + path: 10 +} + +type: DETERMINISTIC_KEY +public_key: "\002\256[\025\037L\371\365\300\357\315\224v\006\r\236\335BIO\210vvT\n\266\243{G\321`\366\377" +deterministic_key { + chain_code: ":\240\372\237\034\033DR1\220\t\022\200\033\307/\200wBy\365\223\244\0170\327\277>Z9\365\277" + path: 2147483649 + path: 1 + path: 11 +} + +type: DETERMINISTIC_KEY +public_key: "\002\375\023ByLoCf\260\230\001\334\373\031\327\325\347\353Ad\253\244\246A=\217vf\034D5\250" +deterministic_key { + chain_code: "\277\265\332jOb\260G6,u\342\263A]\214KF\360\337\217\231:\342x\225\330\250Fa*n" + path: 2147483649 + path: 1 + path: 12 +} \ No newline at end of file diff --git a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization-account-one.txt b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization-account-one.txt index 314856cff..5d784fd51 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization-account-one.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization-account-one.txt @@ -5,6 +5,7 @@ deterministic_key { chain_code: "SE\031\000\277\023 \256W\237\036\034c\340\aB-I\246\304\025\325\262\314\235\240]\020\374T\315\027" path: 1 } +output_script_type: P2PKH type: DETERMINISTIC_KEY public_key: "\003QO{n\351\326\357\343T\203u\345\025{]\231\234\273A\376U`\307\212\336h\255\215\335(\364\001" diff --git a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt index dd539d6c6..25318b8ee 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt @@ -5,6 +5,7 @@ deterministic_key { chain_code: "\370\017\223\021O?.@gZ|\233j\3437\317q-\241!\177J \323\'\264s\203\314\321\v\346" path: 2147483648 } +output_script_type: P2PKH type: DETERMINISTIC_KEY public_key: "\002\361V\216\001\371p\270\212\272\236%\216\356o\025g\r\035>a\305j\001P\217Q\242\261.\353\367\315" diff --git a/examples/src/main/java/org/bitcoinj/examples/GenerateLowSTests.java b/examples/src/main/java/org/bitcoinj/examples/GenerateLowSTests.java index fe76ec0e0..df6585bfa 100644 --- a/examples/src/main/java/org/bitcoinj/examples/GenerateLowSTests.java +++ b/examples/src/main/java/org/bitcoinj/examples/GenerateLowSTests.java @@ -1,5 +1,6 @@ /* * Copyright 2015 Ross Nicoll. + * Copyright 2019 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +65,7 @@ public class GenerateLowSTests { final ECKey key = new ECKey(secureRandom); final KeyBag bag = new KeyBag() { @Override - public ECKey findKeyFromPubKeyHash(byte[] pubKeyHash) { + public ECKey findKeyFromPubKeyHash(byte[] pubkeyHash, Script.ScriptType scriptType) { return key; } diff --git a/examples/src/main/java/org/bitcoinj/examples/RestoreFromSeed.java b/examples/src/main/java/org/bitcoinj/examples/RestoreFromSeed.java index 106cf6ccc..12b2ef844 100644 --- a/examples/src/main/java/org/bitcoinj/examples/RestoreFromSeed.java +++ b/examples/src/main/java/org/bitcoinj/examples/RestoreFromSeed.java @@ -20,8 +20,10 @@ import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.core.*; import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script; import org.bitcoinj.store.SPVBlockStore; import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.KeyChainGroupStructure; import org.bitcoinj.wallet.Wallet; import java.io.File; @@ -46,7 +48,7 @@ public class RestoreFromSeed { DeterministicSeed seed = new DeterministicSeed(seedCode, null, passphrase, creationtime); // The wallet class provides a easy fromSeed() function that loads a new wallet from a given seed. - Wallet wallet = Wallet.fromSeed(params, seed); + Wallet wallet = Wallet.fromSeed(params, seed, Script.ScriptType.P2PKH); // Because we are importing an existing wallet which might already have transactions we must re-download the blockchain to make the wallet picks up these transactions // You can find some information about this in the guides: https://bitcoinj.github.io/working-with-the-wallet#setup diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 9e2c9d2ff..de5d09277 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -24,6 +24,8 @@ import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.protocols.payments.PaymentProtocol; import org.bitcoinj.protocols.payments.PaymentProtocolException; import org.bitcoinj.protocols.payments.PaymentSession; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptException; import org.bitcoinj.script.ScriptPattern; @@ -65,6 +67,7 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Peer; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; @@ -121,6 +124,7 @@ public class WalletTool { private static OptionSpec dateFlag; private static OptionSpec unixtimeFlag; private static OptionSpec seedFlag, watchFlag; + private static OptionSpec outputScriptTypeFlag; private static OptionSpec xpubkeysFlag; private static NetworkParameters params; @@ -233,6 +237,7 @@ public class WalletTool { OptionSpec walletFileName = parser.accepts("wallet").withRequiredArg().defaultsTo("wallet"); seedFlag = parser.accepts("seed").withRequiredArg(); watchFlag = parser.accepts("watchkey").withRequiredArg(); + outputScriptTypeFlag = parser.accepts("output-script-type").withRequiredArg().ofType(Script.ScriptType.class); OptionSpec netFlag = parser.accepts("net").withRequiredArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.MAIN); dateFlag = parser.accepts("date").withRequiredArg().ofType(Date.class) .withValuesConvertedBy(DateConverter.datePattern("yyyy/MM/dd")); @@ -1313,6 +1318,7 @@ public class WalletTool { return; } long creationTimeSecs = getCreationTimeSeconds(); + ScriptType outputScriptType = options.valueOf(outputScriptTypeFlag); if (creationTimeSecs == 0) creationTimeSecs = MnemonicCode.BIP39_STANDARDISATION_TIME_SECS; if (options.has(seedFlag)) { @@ -1337,11 +1343,11 @@ public class WalletTool { // not reached - all subclasses handled above throw new RuntimeException(e); } - wallet = Wallet.fromSeed(params, seed); + wallet = Wallet.fromSeed(params, seed, outputScriptType); } else if (options.has(watchFlag)) { wallet = Wallet.fromWatchingKeyB58(params, options.valueOf(watchFlag), creationTimeSecs); } else { - wallet = new Wallet(params); + wallet = Wallet.createDeterministic(params, outputScriptType); } if (password != null) wallet.encrypt(password); @@ -1448,7 +1454,10 @@ public class WalletTool { if (!key.isCompressed()) System.out.println("WARNING: Importing an uncompressed key"); wallet.importKey(key); - System.out.println(LegacyAddress.fromKey(params, key) + " " + key); + System.out.print("Addresses: " + LegacyAddress.fromKey(params, key)); + if (key.isCompressed()) + System.out.print("," + SegwitAddress.fromKey(params, key)); + System.out.println(); } /** @@ -1489,7 +1498,7 @@ public class WalletTool { key = wallet.findKeyFromPubKey(HEX.decode(pubKey)); } else { try { - Address address = LegacyAddress.fromBase58(wallet.getParams(), addr); + Address address = Address.fromString(wallet.getParams(), addr); key = wallet.findKeyFromAddress(address); } catch (AddressFormatException e) { System.err.println(addr + " does not parse as a Bitcoin address of the right network parameters."); @@ -1508,8 +1517,8 @@ public class WalletTool { } private static void currentReceiveAddr() { - ECKey key = wallet.currentReceiveKey(); - System.out.println(LegacyAddress.fromKey(params, key) + " " + key); + Address address = wallet.currentReceiveAddress(); + System.out.println(address); } private static void dumpWallet() throws BlockStoreException { diff --git a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt index a9a618a4e..ed7a5acab 100644 --- a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt +++ b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt @@ -12,7 +12,8 @@ Usage: wallet-tool --flags action-name Will complain and require --force if the wallet already exists. If --seed is present, it should specify either a mnemonic code or hex/base58 raw seed bytes. If --watchkey is present, it creates a watching wallet using the specified base58 xpub. - If --seed or --watchkey is combined with either --date or --unixtime, use that as a birthdate for + If --seed or --watchkey is combined with either --date or --unixtime, use that as a birthdate for. + If --output-script-type, use that for deriving addresses. the wallet. See the set-creation-time action for the meaning of these flags. marry Makes the wallet married with other parties, requiring multisig to spend funds. External public keys for other signing parties must be specified with --xpubkeys (comma separated). diff --git a/wallettemplate/src/main/java/wallettemplate/Main.java b/wallettemplate/src/main/java/wallettemplate/Main.java index e2a7a5301..6cb3c4661 100644 --- a/wallettemplate/src/main/java/wallettemplate/Main.java +++ b/wallettemplate/src/main/java/wallettemplate/Main.java @@ -22,6 +22,7 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Utils; import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.params.*; +import org.bitcoinj.script.Script; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.DeterministicSeed; @@ -46,6 +47,7 @@ import static wallettemplate.utils.GuiUtils.*; public class Main extends Application { public static NetworkParameters params = MainNetParams.get(); + public static final Script.ScriptType PREFERRED_OUTPUT_SCRIPT_TYPE = Script.ScriptType.P2WPKH; public static final String APP_NAME = "WalletTemplate"; private static final String WALLET_FILE_NAME = APP_NAME.replaceAll("[^a-zA-Z0-9.-]", "_") + "-" + params.getPaymentProtocolId(); @@ -132,7 +134,7 @@ public class Main extends Application { public void setupWalletKit(@Nullable DeterministicSeed seed) { // If seed is non-null it means we are restoring from backup. - bitcoin = new WalletAppKit(params, new File("."), WALLET_FILE_NAME) { + bitcoin = new WalletAppKit(params, PREFERRED_OUTPUT_SCRIPT_TYPE, null, new File("."), WALLET_FILE_NAME) { @Override protected void onSetupCompleted() { // Don't make the user wait for confirmations for now, as the intention is they're sending it diff --git a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java index f88a746e1..40fbb2cd3 100644 --- a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java +++ b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java @@ -58,6 +58,7 @@ public class SendMoneyController { new TextFieldValidator(amountEdit, text -> !WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0))); amountEdit.setText(balance.toPlainString()); + address.setPromptText(Address.fromKey(Main.params, new ECKey(), Main.PREFERRED_OUTPUT_SCRIPT_TYPE).toString()); } public void cancel(ActionEvent event) { diff --git a/wallettemplate/src/main/resources/wallettemplate/send_money.fxml b/wallettemplate/src/main/resources/wallettemplate/send_money.fxml index 105437b1e..ffe629075 100644 --- a/wallettemplate/src/main/resources/wallettemplate/send_money.fxml +++ b/wallettemplate/src/main/resources/wallettemplate/send_money.fxml @@ -33,7 +33,7 @@ - +