Skip to content

Commit

Permalink
Full support for native segwit:
Browse files Browse the repository at this point in the history
- Hierarchical-deterministic derivation of native segwit addresses.
- Receive payments to native segwit addresses.
- Spend and sign payments from native segwit addresses.
- Watch-only wallets with native segwit addresses (zpub/vpub).
- Wallet-tool is taught to deal with segwit-enabled wallets.

Be aware this adds a new field in the wallet protobuf: output_script_type in Key, which keeps track
of the script type of DeterministicKeyChains. Protobufs will be migrated; old DeterministicKeyChains
are assumed to be of type P2PKH.

Includes some code by Fabrice Drouin.
  • Loading branch information
Andreas Schildbach committed Feb 6, 2019
1 parent be4245d commit a3974a2
Show file tree
Hide file tree
Showing 54 changed files with 1,667 additions and 354 deletions.
13 changes: 10 additions & 3 deletions core/src/main/java/org/bitcoinj/core/ECKey.java
Expand Up @@ -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;
Expand Down Expand Up @@ -1288,10 +1290,15 @@ private String toString(boolean includePrivate, @Nullable KeyParameter aesKey, N
}

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:");
Expand Down
27 changes: 19 additions & 8 deletions core/src/main/java/org/bitcoinj/core/NetworkParameters.java
Expand Up @@ -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;
Expand Down Expand Up @@ -370,16 +372,25 @@ public byte[] getAlertSigningKey() {
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
Expand Down
30 changes: 21 additions & 9 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Expand Up @@ -921,17 +921,29 @@ public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scrip
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;
}

Expand Down
12 changes: 10 additions & 2 deletions 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.
Expand All @@ -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;
Expand All @@ -26,8 +29,13 @@
* 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);
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/org/bitcoinj/core/TransactionInput.java
Expand Up @@ -474,8 +474,8 @@ public void verify(TransactionOutput output) throws VerificationException {
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);
}

/**
Expand Down
14 changes: 10 additions & 4 deletions core/src/main/java/org/bitcoinj/core/TransactionOutPoint.java
Expand Up @@ -129,7 +129,7 @@ public byte[] getConnectedPubKeyScript() {
}

/**
* 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.
Expand All @@ -143,7 +143,10 @@ public ECKey getConnectedKey(KeyBag keyBag) throws ScriptException {
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);
Expand All @@ -153,7 +156,7 @@ public ECKey getConnectedKey(KeyBag keyBag) throws ScriptException {
}

/**
* 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.
*
Expand All @@ -166,7 +169,10 @@ public RedeemData getConnectedRedeemData(KeyBag keyBag) throws ScriptException {
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);
Expand Down
9 changes: 7 additions & 2 deletions core/src/main/java/org/bitcoinj/core/TransactionOutput.java
Expand Up @@ -306,7 +306,11 @@ public boolean isMine(TransactionBag transactionBag) {
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) {
Expand All @@ -325,7 +329,8 @@ public String toString() {
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)));
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/org/bitcoinj/core/TransactionWitness.java
Expand Up @@ -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<byte[]> pushes;

public TransactionWitness(int pushCount) {
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/org/bitcoinj/crypto/ChildNumber.java
Expand Up @@ -37,8 +37,9 @@ public class ChildNumber implements Comparable<ChildNumber> {
public static final int HARDENED_BIT = 0x80000000;

public static final ChildNumber ZERO = new ChildNumber(0);
public static final ChildNumber ONE = new ChildNumber(1);
public static final ChildNumber ZERO_HARDENED = new ChildNumber(0, true);
public static final ChildNumber ONE = new ChildNumber(1);
public static final ChildNumber ONE_HARDENED = new ChildNumber(1, true);

/** Integer i as per BIP 32 spec, including the MSB denoting derivation type (0 = public, 1 = private) **/
private final int i;
Expand Down
38 changes: 27 additions & 11 deletions core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java
Expand Up @@ -18,6 +18,7 @@
package org.bitcoinj.crypto;

import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
Expand Down Expand Up @@ -460,17 +461,24 @@ public BigInteger getPrivKey() {
return key;
}

@Deprecated
public byte[] serializePublic(NetworkParameters params) {
return serialize(params, true);
return serialize(params, true, Script.ScriptType.P2PKH);
}

@Deprecated
public byte[] serializePrivate(NetworkParameters params) {
return serialize(params, false);
return serialize(params, false, Script.ScriptType.P2PKH);
}

private byte[] serialize(NetworkParameters params, boolean pub) {
private byte[] serialize(NetworkParameters params, boolean pub, Script.ScriptType outputScriptType) {
ByteBuffer ser = ByteBuffer.allocate(78);
ser.putInt(pub ? params.getBip32HeaderPub() : params.getBip32HeaderPriv());
if (outputScriptType == Script.ScriptType.P2PKH)
ser.putInt(pub ? params.getBip32HeaderP2PKHpub() : params.getBip32HeaderP2PKHpriv());
else if (outputScriptType == Script.ScriptType.P2WPKH)
ser.putInt(pub ? params.getBip32HeaderP2WPKHpub() : params.getBip32HeaderP2WPKHpriv());
else
throw new IllegalStateException(outputScriptType.toString());
ser.put((byte) getDepth());
ser.putInt(getParentFingerprint());
ser.putInt(getChildNumber().i());
Expand All @@ -480,12 +488,20 @@ private byte[] serialize(NetworkParameters params, boolean pub) {
return ser.array();
}

public String serializePubB58(NetworkParameters params, Script.ScriptType outputScriptType) {
return toBase58(serialize(params, true, outputScriptType));
}

public String serializePrivB58(NetworkParameters params, Script.ScriptType outputScriptType) {
return toBase58(serialize(params, false, outputScriptType));
}

public String serializePubB58(NetworkParameters params) {
return toBase58(serialize(params, true));
return serializePubB58(params, Script.ScriptType.P2PKH);
}

public String serializePrivB58(NetworkParameters params) {
return toBase58(serialize(params, false));
return serializePrivB58(params, Script.ScriptType.P2PKH);
}

static String toBase58(byte[] ser) {
Expand Down Expand Up @@ -520,9 +536,10 @@ public static DeterministicKey deserialize(NetworkParameters params, byte[] seri
public static DeterministicKey deserialize(NetworkParameters params, byte[] serializedKey, @Nullable DeterministicKey parent) {
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
int header = buffer.getInt();
if (header != params.getBip32HeaderPriv() && header != params.getBip32HeaderPub())
final boolean pub = header == params.getBip32HeaderP2PKHpub() || header == params.getBip32HeaderP2WPKHpub();
final boolean priv = header == params.getBip32HeaderP2PKHpriv() || header == params.getBip32HeaderP2WPKHpriv();
if (!(pub || priv))
throw new IllegalArgumentException("Unknown header bytes: " + toBase58(serializedKey).substring(0, 4));
boolean pub = header == params.getBip32HeaderPub();
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
final int parentFingerprint = buffer.getInt();
final int i = buffer.getInt();
Expand Down Expand Up @@ -615,9 +632,8 @@ public String toString() {

@Override
public void formatKeyWithAddress(boolean includePrivateKeys, @Nullable KeyParameter aesKey, StringBuilder builder,
NetworkParameters params) {
final Address address = LegacyAddress.fromKey(params, this);
builder.append(" addr:").append(address);
NetworkParameters params, Script.ScriptType outputScriptType) {
builder.append(" addr:").append(Address.fromKey(params, this, outputScriptType).toString());
builder.append(" hash160:").append(Utils.HEX.encode(getPubKeyHash()));
builder.append(" (").append(getPathAsString()).append(")\n");
if (includePrivateKeys) {
Expand Down

0 comments on commit a3974a2

Please sign in to comment.