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
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, + ImmutableListaccountPath) { + 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 @@ - +