Skip to content

Commit

Permalink
Determine native segwit destination addresses of a transaction by par…
Browse files Browse the repository at this point in the history
…sing the segwit scriptPubKey.
  • Loading branch information
Andreas Schildbach committed Mar 4, 2018
1 parent 52afdc6 commit 061f5c9
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 23 deletions.
4 changes: 2 additions & 2 deletions core/src/main/java/org/bitcoinj/core/TransactionOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public Script getScriptPubKey() throws ScriptException {
@Nullable
public LegacyAddress getAddressFromP2PKHScript(NetworkParameters networkParameters) throws ScriptException{
if (ScriptPattern.isPayToPubKeyHash(getScriptPubKey()))
return getScriptPubKey().getToAddress(networkParameters);
return (LegacyAddress) getScriptPubKey().getToAddress(networkParameters);

return null;
}
Expand All @@ -151,7 +151,7 @@ public LegacyAddress getAddressFromP2PKHScript(NetworkParameters networkParamete
@Nullable
public LegacyAddress getAddressFromP2SH(NetworkParameters networkParameters) throws ScriptException{
if (ScriptPattern.isPayToScriptHash(getScriptPubKey()))
return getScriptPubKey().getToAddress(networkParameters);
return (LegacyAddress) getScriptPubKey().getToAddress(networkParameters);

return null;
}
Expand Down
19 changes: 8 additions & 11 deletions core/src/main/java/org/bitcoinj/script/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -239,22 +239,17 @@ public boolean isSentToAddress() {
}

/**
* <p>If a program matches the standard template DUP HASH160 &lt;pubkey hash&gt; EQUALVERIFY CHECKSIG
* then this function retrieves the third element.
* In this case, this is useful for fetching the destination address of a transaction.</p>
* <p>If the program somehow pays to a hash, returns the hash.</p>
*
* <p>If a program matches the standard template HASH160 &lt;script hash&gt; EQUAL
* then this function retrieves the second element.
* In this case, this is useful for fetching the hash of the redeem script of a transaction.</p>
*
* <p>Otherwise it throws a ScriptException.</p>
*
* <p>Otherwise this method throws a ScriptException.</p>
*/
public byte[] getPubKeyHash() throws ScriptException {
if (ScriptPattern.isPayToPubKeyHash(this))
return ScriptPattern.extractHashFromPayToPubKeyHash(this);
else if (ScriptPattern.isPayToScriptHash(this))
return ScriptPattern.extractHashFromPayToScriptHash(this);
else if (ScriptPattern.isPayToWitnessHash(this))
return ScriptPattern.extractHashFromPayToWitnessHash(this);
else
throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Script not in the standard scriptPubKey form");
}
Expand Down Expand Up @@ -283,7 +278,7 @@ public BigInteger getCLTVPaymentChannelExpiry() throws ScriptException {
/**
* Gets the destination address from this script, if it's in the required form.
*/
public LegacyAddress getToAddress(NetworkParameters params) throws ScriptException {
public Address getToAddress(NetworkParameters params) throws ScriptException {
return getToAddress(params, false);
}

Expand All @@ -294,13 +289,15 @@ public LegacyAddress getToAddress(NetworkParameters params) throws ScriptExcepti
* If true, allow payToPubKey to be casted to the corresponding address. This is useful if you prefer
* showing addresses rather than pubkeys.
*/
public LegacyAddress getToAddress(NetworkParameters params, boolean forcePayToPubKey) throws ScriptException {
public Address getToAddress(NetworkParameters params, boolean forcePayToPubKey) throws ScriptException {
if (ScriptPattern.isPayToPubKeyHash(this))
return LegacyAddress.fromPubKeyHash(params, ScriptPattern.extractHashFromPayToPubKeyHash(this));
else if (ScriptPattern.isPayToScriptHash(this))
return LegacyAddress.fromP2SHScript(params, this);
else if (forcePayToPubKey && ScriptPattern.isPayToPubKey(this))
return LegacyAddress.fromKey(params, ECKey.fromPublicOnly(ScriptPattern.extractKeyFromPayToPubKey(this)));
else if (ScriptPattern.isPayToWitnessHash(this))
return SegwitAddress.fromHash(params, ScriptPattern.extractHashFromPayToWitnessHash(this));
else
throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Cannot cast this script to a pay-to-address type");
}
Expand Down
29 changes: 29 additions & 0 deletions core/src/main/java/org/bitcoinj/script/ScriptPattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.bitcoinj.script;

import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.SegwitAddress;

import java.math.BigInteger;
import java.util.List;
Expand Down Expand Up @@ -105,6 +106,34 @@ public static byte[] extractKeyFromPayToPubKey(Script script) {
return script.chunks.get(0).data;
}

/**
* Returns true if this script is of the form OP_0 <hash>. This can either be a P2WPKH or P2WSH scriptPubKey. These
* two script types were introduced with segwit.
*/
public static boolean isPayToWitnessHash(Script script) {
List<ScriptChunk> chunks = script.chunks;
if (chunks.size() != 2)
return false;
if (!chunks.get(0).equalsOpCode(OP_0))
return false;
byte[] chunk1data = chunks.get(1).data;
if (chunk1data == null)
return false;
if (chunk1data.length != SegwitAddress.WITNESS_PROGRAM_LENGTH_PKH
&& chunk1data.length != SegwitAddress.WITNESS_PROGRAM_LENGTH_SH)
return false;
return true;
}

/**
* Extract the pubkey hash from a P2WPKH or the script hash from a P2WSH scriptPubKey. It's important that the
* script is in the correct form, so you will want to guard calls to this method with
* {@link #isPayToWitnessHash(Script)}.
*/
public static byte[] extractHashFromPayToWitnessHash(Script script) {
return script.chunks.get(1).data;
}

/**
* Returns whether this script matches the format used for multisig outputs: [n] [keys...] [m] CHECKMULTISIG
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.nio.ByteBuffer;

import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
Expand Down Expand Up @@ -461,7 +462,7 @@ public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProvide
}
if (txout != null) {
Script sc = txout.getScript();
LegacyAddress address = sc.getToAddress(params, true);
Address address = sc.getToAddress(params, true);
UTXO output = new UTXO(txout.getHash(), txout.getIndex(), txout.getValue(), txout.getHeight(),
txout.isCoinbase(), txout.getScript(), address.toString());
results.add(output);
Expand Down Expand Up @@ -887,7 +888,7 @@ public void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException
String address = out.getAddress();
if (address == null || address.equals("")) {
Script sc = out.getScript();
a = sc.getToAddress(params);
a = (LegacyAddress) sc.getToAddress(params);
hashBytes = a.getHash();
} else {
a = LegacyAddress.fromBase58(params, out.getAddress());
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKey

if (isMarried()) {
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : this.currentKeys.entrySet()) {
LegacyAddress address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params);
currentAddresses.put(entry.getKey(), address);
Address address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params);
currentAddresses.put(entry.getKey(), (LegacyAddress) address);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/org/bitcoinj/wallet/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ public List<LegacyAddress> getWatchedAddresses() {
List<LegacyAddress> addresses = new LinkedList<>();
for (Script script : watchedScripts)
if (ScriptPattern.isPayToPubKeyHash(script))
addresses.add(script.getToAddress(params));
addresses.add(((LegacyAddress) script.getToAddress(params)));
return addresses;
} finally {
keyChainGroupLock.unlock();
Expand Down
7 changes: 7 additions & 0 deletions core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@

import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptPattern;
import org.junit.Test;

import com.google.common.base.MoreObjects;
Expand Down Expand Up @@ -103,6 +105,11 @@ public void validAddresses() {
assertEquals(valid.expectedScriptPubKey,
Utils.HEX.encode(ScriptBuilder.createOutputScript(address).getProgram()));
assertEquals(valid.address.toLowerCase(Locale.ROOT), address.toBech32());
if (valid.expectedWitnessVersion == 0) {
Script expectedScriptPubKey = new Script(Utils.HEX.decode(valid.expectedScriptPubKey));
assertEquals(address, SegwitAddress.fromHash(valid.expectedParams,
ScriptPattern.extractHashFromPayToWitnessHash(expectedScriptPubKey)));
}
assertEquals(valid.expectedWitnessVersion, address.getWitnessVersion());
}
}
Expand Down
16 changes: 12 additions & 4 deletions core/src/test/java/org/bitcoinj/script/ScriptPatternTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import com.google.common.collect.Lists;

import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.SegwitAddress;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.params.MainNetParams;
import org.junit.Test;

Expand All @@ -42,12 +44,18 @@ public void testCommonScripts() {
assertTrue(ScriptPattern.isPayToScriptHash(
ScriptBuilder.createP2SHOutputScript(2, keys)
));
assertTrue(ScriptPattern.isSentToMultisig(
ScriptBuilder.createMultiSigOutputScript(2, keys)
));
assertTrue(ScriptPattern.isPayToPubKey(
ScriptBuilder.createOutputScript(keys.get(0))
));
assertTrue(ScriptPattern.isPayToWitnessHash(
ScriptBuilder.createOutputScript(SegwitAddress.fromHash(MAINNET, keys.get(0).getPubKeyHash()))
));
assertTrue(ScriptPattern.isPayToWitnessHash(
ScriptBuilder.createOutputScript(SegwitAddress.fromHash(MAINNET, Sha256Hash.hash(new byte[0])))
));
assertTrue(ScriptPattern.isSentToMultisig(
ScriptBuilder.createMultiSigOutputScript(2, keys)
));
assertTrue(ScriptPattern.isSentToCltvPaymentChannel(
ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.ONE, keys.get(0), keys.get(1))
));
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/java/org/bitcoinj/wallet/WalletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2928,7 +2928,7 @@ public void keyRotationRandom() throws Exception {
assertEquals(THREE_CENTS, tx.getValueSentFromMe(wallet));
assertEquals(THREE_CENTS.subtract(tx.getFee()), tx.getValueSentToMe(wallet));
// TX sends to one of our addresses (for now we ignore married wallets).
final LegacyAddress toAddress = tx.getOutput(0).getScriptPubKey().getToAddress(UNITTEST);
final LegacyAddress toAddress = (LegacyAddress) tx.getOutput(0).getScriptPubKey().getToAddress(UNITTEST);
final ECKey rotatingToKey = wallet.findKeyFromPubHash(toAddress.getHash());
assertNotNull(rotatingToKey);
assertFalse(wallet.isKeyRotating(rotatingToKey));
Expand Down

0 comments on commit 061f5c9

Please sign in to comment.