diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java index d62f852..5922d11 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java @@ -17,10 +17,7 @@ import org.bouncycastle.crypto.params.ECKeyGenerationParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; -import org.bouncycastle.math.ec.ECAlgorithms; -import org.bouncycastle.math.ec.ECPoint; -import org.bouncycastle.math.ec.FixedPointCombMultiplier; -import org.bouncycastle.math.ec.FixedPointUtil; +import org.bouncycastle.math.ec.*; import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; import org.bouncycastle.util.Properties; import org.bouncycastle.util.encoders.Hex; @@ -30,6 +27,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.security.SignatureException; @@ -628,6 +626,40 @@ public class ECKey implements EncryptableItem { } } + public ECKey getTweakedOutputKey() { + ECPoint internalKey = liftX(getPubKeyXCoord()); + byte[] taggedHash = taggedHash("TapTweak", internalKey.getXCoord().getEncoded()); + ECPoint outputKey = internalKey.add(ECKey.fromPrivate(taggedHash).getPubKeyPoint()); + return ECKey.fromPublicOnly(outputKey, true); + } + + private static ECPoint liftX(byte[] bytes) { + SecP256K1Curve secP256K1Curve = (SecP256K1Curve)CURVE_PARAMS.getCurve(); + BigInteger x = new BigInteger(1, bytes); + BigInteger p = secP256K1Curve.getQ(); + if(x.compareTo(p) > -1) { + throw new IllegalArgumentException("Provided bytes must be less than secp256k1 field size"); + } + + BigInteger y_sq = x.modPow(BigInteger.valueOf(3), p).add(BigInteger.valueOf(7)).mod(p); + BigInteger y = y_sq.modPow(p.add(BigInteger.valueOf(1)).divide(BigInteger.valueOf(4)), p); + if(!y.modPow(BigInteger.valueOf(2), p).equals(y_sq)) { + throw new IllegalStateException("Calculated invalid y_sq when solving for y co-ordinate"); + } + + return secP256K1Curve.createPoint(x, y.and(BigInteger.ONE).equals(BigInteger.ZERO) ? y : p.subtract(y)); + } + + private static byte[] taggedHash(String tag, byte[] msg) { + byte[] hash = Sha256Hash.hash(tag.getBytes(StandardCharsets.UTF_8)); + ByteBuffer buffer = ByteBuffer.allocate(hash.length + hash.length + msg.length); + buffer.put(hash); + buffer.put(hash); + buffer.put(msg); + + return Sha256Hash.hash(buffer.array()); + } + /** * Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression. */ diff --git a/src/main/java/com/sparrowwallet/drongo/policy/Miniscript.java b/src/main/java/com/sparrowwallet/drongo/policy/Miniscript.java index c372540..98294b5 100644 --- a/src/main/java/com/sparrowwallet/drongo/policy/Miniscript.java +++ b/src/main/java/com/sparrowwallet/drongo/policy/Miniscript.java @@ -5,6 +5,7 @@ import java.util.regex.Pattern; public class Miniscript { private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\("); + private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\("); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private String script; @@ -27,6 +28,11 @@ public class Miniscript { return 1; } + Matcher taprootMatcher = TAPROOT_PATTERN.matcher(script); + if(taprootMatcher.find()) { + return 1; + } + Matcher multiMatcher = MULTI_PATTERN.matcher(script); if(multiMatcher.find()) { String threshold = multiMatcher.group(1); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index 36f5d41..3dd051f 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -175,6 +175,11 @@ public class Script { } } + //Special handling for taproot tweaked keys - we don't want to tweak them again + if(P2TR.isScriptType(this)) { + return new Address[] { new P2TRAddress(P2TR.getPublicKeyFromScript(this).getPubKeyXCoord()) }; + } + for(ScriptType scriptType : SINGLE_KEY_TYPES) { if(scriptType.isScriptType(this)) { return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) }; diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index 8e07b8c..81a799f 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -975,15 +975,15 @@ public enum ScriptType { return List.of(MULTI, CUSTOM); } }, - P2TR("P2TR", "Taproot (P2TR)", "m/6789'/0'/0'") { + P2TR("P2TR", "Taproot (P2TR)", "m/86'/0'/0'") { @Override public Address getAddress(byte[] pubKey) { return new P2TRAddress(pubKey); } @Override - public Address getAddress(ECKey key) { - return getAddress(key.getPubKeyXCoord()); + public Address getAddress(ECKey derivedKey) { + return getAddress(derivedKey.getTweakedOutputKey().getPubKeyXCoord()); } @Override @@ -1001,8 +1001,8 @@ public enum ScriptType { } @Override - public Script getOutputScript(ECKey key) { - return getOutputScript(key.getPubKeyXCoord()); + public Script getOutputScript(ECKey derivedKey) { + return getOutputScript(derivedKey.getTweakedOutputKey().getPubKeyXCoord()); } @Override @@ -1011,8 +1011,8 @@ public enum ScriptType { } @Override - public String getOutputDescriptor(ECKey key) { - return getDescriptor() + Utils.bytesToHex(key.getPubKeyXCoord()) + getCloseDescriptor(); + public String getOutputDescriptor(ECKey derivedKey) { + return getDescriptor() + Utils.bytesToHex(derivedKey.getPubKeyXCoord()) + getCloseDescriptor(); } @Override @@ -1052,7 +1052,15 @@ public enum ScriptType { @Override public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { - throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported"); + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + if(!scriptPubKey.equals(getOutputScript(pubKey))) { + throw new ProtocolException("Provided P2TR scriptPubKey does not match constructed pubkey script"); + } + + return new Script(new byte[0]); } @Override @@ -1072,7 +1080,7 @@ public enum ScriptType { @Override public List getAllowedPolicyTypes() { - return Collections.emptyList(); + return Network.get() == Network.REGTEST || Network.get() == Network.SIGNET ? List.of(SINGLE) : Collections.emptyList(); } };