Script, SegwitAddress: detect taproot scripts and decode them into addresses

This commit is contained in:
Andreas Schildbach 2021-10-06 20:55:27 +02:00
parent 559f6a2711
commit 4b243a615a
5 changed files with 69 additions and 10 deletions

View file

@ -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.

View file

@ -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;
}

View file

@ -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 <pubkey>}. This is a P2TR scriptPubKey. This
* script type was introduced with taproot.
*/
public static boolean isP2TR(Script script) {
List<ScriptChunk> 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}

View file

@ -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());

View file

@ -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)