diff --git a/core/src/main/java/org/bitcoinj/core/SegwitAddress.java b/core/src/main/java/org/bitcoinj/core/SegwitAddress.java index 80ada8702..bd8af26a9 100644 --- a/core/src/main/java/org/bitcoinj/core/SegwitAddress.java +++ b/core/src/main/java/org/bitcoinj/core/SegwitAddress.java @@ -47,6 +47,7 @@ import org.bitcoinj.script.Script; public class SegwitAddress extends Address { public static final int WITNESS_PROGRAM_LENGTH_PKH = 20; public static final int WITNESS_PROGRAM_LENGTH_SH = 32; + public static final int WITNESS_PROGRAM_LENGTH_TR = 32; public static final int WITNESS_PROGRAM_MIN_LENGTH = 2; public static final int WITNESS_PROGRAM_MAX_LENGTH = 40; @@ -59,7 +60,7 @@ public class SegwitAddress extends Address { * @param witnessVersion * version number between 0 and 16 * @param witnessProgram - * hash of pubkey or script (for version 0) + * hash of pubkey, pubkey or script (depending on version) */ private SegwitAddress(NetworkParameters params, int witnessVersion, byte[] witnessProgram) throws AddressFormatException { @@ -138,13 +139,20 @@ public class SegwitAddress extends Address { @Override public Script.ScriptType getOutputScriptType() { int version = getWitnessVersion(); - checkState(version == 0); - int programLength = getWitnessProgram().length; - if (programLength == WITNESS_PROGRAM_LENGTH_PKH) - return Script.ScriptType.P2WPKH; - if (programLength == WITNESS_PROGRAM_LENGTH_SH) - return Script.ScriptType.P2WSH; - throw new IllegalStateException("Cannot happen."); + if (version == 0) { + int programLength = getWitnessProgram().length; + if (programLength == WITNESS_PROGRAM_LENGTH_PKH) + return Script.ScriptType.P2WPKH; + if (programLength == WITNESS_PROGRAM_LENGTH_SH) + return Script.ScriptType.P2WSH; + throw new IllegalStateException(); // cannot happen + } else if (version == 1) { + int programLength = getWitnessProgram().length; + if (programLength == WITNESS_PROGRAM_LENGTH_TR) + return Script.ScriptType.P2TR; + throw new IllegalStateException(); // cannot happen + } + throw new IllegalStateException("cannot handle: " + version); } @Override @@ -202,6 +210,23 @@ public class SegwitAddress extends Address { return new SegwitAddress(params, 0, hash); } + /** + * Construct a {@link SegwitAddress} that represents the given program, which is either a pubkey, a pubkey hash + * or a script hash – depending on the script version. The resulting address will be either a P2WPKH, a P2WSH or + * a P2TR type of address. + * + * @param params + * network this address is valid for + * @param witnessVersion + * version number between 0 and 16 + * @param witnessProgram + * version dependent witness program + * @return constructed address + */ + public static SegwitAddress fromProgram(NetworkParameters params, int witnessVersion, byte[] witnessProgram) { + return new SegwitAddress(params, witnessVersion, witnessProgram); + } + /** * Construct a {@link SegwitAddress} that represents the public part of the given {@link ECKey}. Note that an * address is derived from a hash of the public key and is not the public key itself. diff --git a/core/src/main/java/org/bitcoinj/script/Script.java b/core/src/main/java/org/bitcoinj/script/Script.java index 65561f993..08e48d2dd 100644 --- a/core/src/main/java/org/bitcoinj/script/Script.java +++ b/core/src/main/java/org/bitcoinj/script/Script.java @@ -59,7 +59,8 @@ public class Script { P2PK(2), // pay to pubkey P2SH(3), // pay to script hash P2WPKH(4), // pay to witness pubkey hash - P2WSH(5); // pay to witness script hash + P2WSH(5), // pay to witness script hash + P2TR(6); // pay to taproot public final int id; @@ -283,6 +284,8 @@ public class Script { return LegacyAddress.fromKey(params, ECKey.fromPublicOnly(ScriptPattern.extractKeyFromP2PK(this))); else if (ScriptPattern.isP2WH(this)) return SegwitAddress.fromHash(params, ScriptPattern.extractHashFromP2WH(this)); + else if (ScriptPattern.isP2TR(this)) + return SegwitAddress.fromProgram(params, 1, ScriptPattern.extractOutputKeyFromP2TR(this)); else throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Cannot cast this script to an address"); } @@ -1676,6 +1679,8 @@ public class Script { return ScriptType.P2WPKH; if (ScriptPattern.isP2WSH(this)) return ScriptType.P2WSH; + if (ScriptPattern.isP2TR(this)) + return ScriptType.P2TR; return null; } diff --git a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java index 7c360c09e..a2d06be84 100644 --- a/core/src/main/java/org/bitcoinj/script/ScriptPattern.java +++ b/core/src/main/java/org/bitcoinj/script/ScriptPattern.java @@ -196,6 +196,32 @@ public class ScriptPattern { return script.chunks.get(1).data; } + /** + * Returns true if this script is of the form {@code OP_1 }. This is a P2TR scriptPubKey. This + * script type was introduced with taproot. + */ + public static boolean isP2TR(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(OP_1)) + return false; + byte[] chunk1data = chunks.get(1).data; + if (chunk1data == null) + return false; + if (chunk1data.length != SegwitAddress.WITNESS_PROGRAM_LENGTH_TR) + return false; + return true; + } + + /** + * Extract the taproot output key from a P2TR scriptPubKey. It's important that the script is in the correct + * form, so you will want to guard calls to this method with {@link #isP2TR(Script)}. + */ + public static byte[] extractOutputKeyFromP2TR(Script script) { + return script.chunks.get(1).data; + } + /** * Returns whether this script matches the format used for m-of-n multisig outputs: * {@code [m] [keys...] [n] CHECKMULTISIG} diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index a5ba776bb..6285a125c 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -1162,7 +1162,7 @@ public class Wallet extends BaseTaggableObject return isPubKeyHashMine(address.getHash(), scriptType); else if (scriptType == ScriptType.P2SH) return isPayToScriptHashMine(address.getHash()); - else if (scriptType == ScriptType.P2WSH) + else if (scriptType == ScriptType.P2WSH || scriptType == ScriptType.P2TR) return false; else throw new IllegalArgumentException(address.toString()); diff --git a/core/src/test/java/org/bitcoinj/script/ScriptTest.java b/core/src/test/java/org/bitcoinj/script/ScriptTest.java index 268a6d537..fc88897c3 100644 --- a/core/src/test/java/org/bitcoinj/script/ScriptTest.java +++ b/core/src/test/java/org/bitcoinj/script/ScriptTest.java @@ -462,6 +462,9 @@ public class ScriptTest { Script p2wshScript = ScriptBuilder.createP2WSHOutputScript(new byte[32]); scriptAddress = SegwitAddress.fromHash(TESTNET, ScriptPattern.extractHashFromP2WH(p2wshScript)); assertEquals(scriptAddress, p2wshScript.getToAddress(TESTNET)); + // P2TR + toAddress = SegwitAddress.fromProgram(TESTNET, 1, new byte[32]); + assertEquals(toAddress, ScriptBuilder.createOutputScript(toAddress).getToAddress(TESTNET)); } @Test(expected = ScriptException.class)