From 061f5c9841231446e57f543c1e19a10d0e83845e Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Sun, 25 Feb 2018 22:05:54 +0100 Subject: [PATCH] Determine native segwit destination addresses of a transaction by parsing the segwit scriptPubKey. --- .../org/bitcoinj/core/TransactionOutput.java | 4 +-- .../main/java/org/bitcoinj/script/Script.java | 19 +++++------- .../org/bitcoinj/script/ScriptPattern.java | 29 +++++++++++++++++++ .../store/LevelDBFullPrunedBlockStore.java | 5 ++-- .../org/bitcoinj/wallet/KeyChainGroup.java | 4 +-- .../main/java/org/bitcoinj/wallet/Wallet.java | 2 +- .../org/bitcoinj/core/SegwitAddressTest.java | 7 +++++ .../bitcoinj/script/ScriptPatternTest.java | 16 +++++++--- .../java/org/bitcoinj/wallet/WalletTest.java | 2 +- 9 files changed, 65 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java index c363b8e31ba..bcbfba4e467 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionOutput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionOutput.java @@ -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; } @@ -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; } diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index a722c61d119..b2f7e745eba 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -239,22 +239,17 @@ public boolean isSentToAddress() { } /** - *

If a program matches the standard template DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG - * then this function retrieves the third element. - * In this case, this is useful for fetching the destination address of a transaction.

+ *

If the program somehow pays to a hash, returns the hash.

* - *

If a program matches the standard template HASH160 <script hash> 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.

- * - *

Otherwise it throws a ScriptException.

- * + *

Otherwise this method throws a ScriptException.

*/ 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"); } @@ -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); } @@ -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"); } diff --git a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java index 7da1f9bc0ee..165f1578d69 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java @@ -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; @@ -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 . This can either be a P2WPKH or P2WSH scriptPubKey. These + * two script types were introduced with segwit. + */ + public static boolean isPayToWitnessHash(Script script) { + List 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 */ diff --git a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java index 7a54f9fc60f..1e3fd549e71 100644 --- a/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java +++ b/core/src/main/java/org/bitcoinj/store/LevelDBFullPrunedBlockStore.java @@ -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; @@ -461,7 +462,7 @@ public List getOpenTransactionOutputs(List 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); @@ -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()); diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 62c38f8c39e..d08716c0520 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -124,8 +124,8 @@ private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKey if (isMarried()) { for (Map.Entry 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); } } } diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index c1664750940..e9377995336 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -1005,7 +1005,7 @@ public List getWatchedAddresses() { List 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(); diff --git a/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java b/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java index fbcd1234aff..c3ada243909 100644 --- a/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java +++ b/core/src/test/java/org/bitcoinj/core/SegwitAddressTest.java @@ -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; @@ -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()); } } diff --git a/core/src/test/java/org/bitcoinj/script/ScriptPatternTest.java b/core/src/test/java/org/bitcoinj/script/ScriptPatternTest.java index 7e3b0243dad..8c334da4ab9 100644 --- a/core/src/test/java/org/bitcoinj/script/ScriptPatternTest.java +++ b/core/src/test/java/org/bitcoinj/script/ScriptPatternTest.java @@ -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; @@ -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)) )); diff --git a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java index dc811a8c103..a2747655bb0 100644 --- a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java @@ -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));