diff --git a/src/main/java/com/sparrowwallet/drongo/address/Address.java b/src/main/java/com/sparrowwallet/drongo/address/Address.java index 90bf9ca..e557ada 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/Address.java +++ b/src/main/java/com/sparrowwallet/drongo/address/Address.java @@ -108,15 +108,29 @@ public abstract class Address { Bech32.Bech32Data data = Bech32.decode(address); if(data.hrp.equals(network.getBech32AddressHRP())) { int witnessVersion = data.data[0]; - if (witnessVersion == 0) { + if(witnessVersion == 0) { + if(data.encoding != Bech32.Encoding.BECH32) { + throw new InvalidAddressException("Invalid address - witness version is 0 but encoding is " + data.encoding); + } + byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length); byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false); - if (witnessProgram.length == 20) { + if(witnessProgram.length == 20) { return new P2WPKHAddress(witnessProgram); } - if (witnessProgram.length == 32) { + if(witnessProgram.length == 32) { return new P2WSHAddress(witnessProgram); } + } else if(witnessVersion == 1) { + if(data.encoding != Bech32.Encoding.BECH32M) { + throw new InvalidAddressException("Invalid address - witness version is 1 but encoding is " + data.encoding); + } + + byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length); + byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false); + if(witnessProgram.length == 32) { + return new P2TRAddress(witnessProgram); + } } } } catch (Exception e) { diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java index 35df236..aaa956b 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java @@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; public class P2PKAddress extends Address { - private byte[] pubKey; + private final byte[] pubKey; public P2PKAddress(byte[] pubKey) { super(Utils.sha256hash160(pubKey)); diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2TRAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2TRAddress.java new file mode 100644 index 0000000..a263762 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/address/P2TRAddress.java @@ -0,0 +1,46 @@ +package com.sparrowwallet.drongo.address; + +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.Bech32; +import com.sparrowwallet.drongo.protocol.Script; +import com.sparrowwallet.drongo.protocol.ScriptType; + +public class P2TRAddress extends Address { + private final byte[] pubKey; + + public P2TRAddress(byte[] pubKey) { + super(Utils.sha256hash160(pubKey)); + this.pubKey = pubKey; + } + + @Override + public int getVersion(Network network) { + return 1; + } + + @Override + public String getAddress(Network network) { + return Bech32.encode(network.getBech32AddressHRP(), getVersion(), pubKey); + } + + @Override + public ScriptType getScriptType() { + return ScriptType.P2TR; + } + + @Override + public Script getOutputScript() { + return getScriptType().getOutputScript(pubKey); + } + + @Override + public byte[] getOutputScriptData() { + return pubKey; + } + + @Override + public String getOutputScriptDataType() { + return "Taproot"; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java index 1075ef3..d62f852 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java @@ -340,6 +340,13 @@ public class ECKey implements EncryptableItem { return pub.getEncoded(); } + /** + * Gets the x coordinate of the raw public key value. This appears in transaction scriptPubKeys for Taproot outputs. + */ + public byte[] getPubKeyXCoord() { + return pub.getEncodedXCoord(); + } + /** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */ public ECPoint getPubKeyPoint() { return pub.get(); @@ -625,8 +632,10 @@ public class ECKey implements EncryptableItem { * Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression. */ public static boolean isPubKeyCanonical(byte[] pubkey) { - if (pubkey.length < 33) + if (pubkey.length < 32) return false; + if (pubkey.length == 32) + return true; if (pubkey[0] == 0x04) { // Uncompressed pubkey if (pubkey.length != 65) @@ -644,7 +653,7 @@ public class ECKey implements EncryptableItem { * Returns true if the given pubkey is in its compressed form. */ public static boolean isPubKeyCompressed(byte[] encoded) { - if (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03)) + if (encoded.length == 32 || (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03))) return true; else if (encoded.length == 65 && encoded[0] == 0x04) return false; diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/LazyECPoint.java b/src/main/java/com/sparrowwallet/drongo/crypto/LazyECPoint.java index 981f935..1eee6bc 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/LazyECPoint.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/LazyECPoint.java @@ -20,7 +20,7 @@ public class LazyECPoint { public LazyECPoint(ECCurve curve, byte[] bits) { this.curve = curve; - this.bits = bits; + this.bits = (bits != null && bits.length == 32 ? addYCoord(bits) : bits); this.compressed = ECKey.isPubKeyCompressed(bits); } @@ -61,6 +61,13 @@ public class LazyECPoint { return get().getEncoded(compressed); } + public byte[] getEncodedXCoord() { + byte[] compressed = getEncoded(true); + byte[] xcoord = new byte[32]; + System.arraycopy(compressed, 1, xcoord, 0, 32); + return xcoord; + } + public String toString() { return Hex.toHexString(getEncoded()); } @@ -80,4 +87,11 @@ public class LazyECPoint { private byte[] getCanonicalEncoding() { return getEncoded(true); } + + private static byte[] addYCoord(byte[] xcoord) { + byte[] compressed = new byte[33]; + compressed[0] = 0x02; + System.arraycopy(xcoord, 0, compressed, 1, 32); + return compressed; + } } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java b/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java index da5ad0a..7ed7f16 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Bech32.java @@ -39,10 +39,18 @@ public class Bech32 { public static class Bech32Data { public final String hrp; public final byte[] data; + public final Encoding encoding; private Bech32Data(final String hrp, final byte[] data) { this.hrp = hrp; this.data = data; + this.encoding = (data[0] == 0x00 ? Encoding.BECH32 : Encoding.BECH32M); + } + + public Bech32Data(String hrp, byte[] data, Encoding encoding) { + this.hrp = hrp; + this.data = data; + this.encoding = encoding; } } @@ -64,7 +72,7 @@ public class Bech32 { /** Expand a HRP for use in checksum computation. */ private static byte[] expandHrp(final String hrp) { int hrpLength = hrp.length(); - byte ret[] = new byte[hrpLength * 2 + 1]; + byte[] ret = new byte[hrpLength * 2 + 1]; for (int i = 0; i < hrpLength; ++i) { int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII ret[i] = (byte) ((c >>> 5) & 0x07); @@ -75,21 +83,29 @@ public class Bech32 { } /** Verify a checksum. */ - private static boolean verifyChecksum(final String hrp, final byte[] values) { + private static Encoding verifyChecksum(final String hrp, final byte[] values) { byte[] hrpExpanded = expandHrp(hrp); byte[] combined = new byte[hrpExpanded.length + values.length]; System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length); System.arraycopy(values, 0, combined, hrpExpanded.length, values.length); - return polymod(combined) == 1; + + int check = polymod(combined); + for(Encoding encoding : Encoding.values()) { + if(check == encoding.checksumConstant) { + return encoding; + } + } + + return null; } /** Create a checksum. */ - private static byte[] createChecksum(final String hrp, final byte[] values) { + private static byte[] createChecksum(final String hrp, Encoding encoding, final byte[] values) { byte[] hrpExpanded = expandHrp(hrp); byte[] enc = new byte[hrpExpanded.length + values.length + 6]; System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length); System.arraycopy(values, 0, enc, hrpExpanded.length, values.length); - int mod = polymod(enc) ^ 1; + int mod = polymod(enc) ^ encoding.checksumConstant; byte[] ret = new byte[6]; for (int i = 0; i < 6; ++i) { ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); @@ -99,16 +115,17 @@ public class Bech32 { /** Encode a Bech32 string. */ public static String encode(final Bech32Data bech32) { - return encode(bech32.hrp, bech32.data); + return encode(bech32.hrp, bech32.encoding, bech32.data); } /** Encode a Bech32 string. */ public static String encode(String hrp, int version, final byte[] values) { - return encode(hrp, encode(0, values)); + Encoding encoding = (version == 0 ? Encoding.BECH32 : Encoding.BECH32M); + return encode(hrp, encoding, encode(version, values)); } /** Encode a Bech32 string. */ - public static String encode(String hrp, final byte[] values) { + public static String encode(String hrp, Encoding encoding, final byte[] values) { if(hrp.length() < 1) { throw new ProtocolException("Human-readable part is too short"); } @@ -118,7 +135,7 @@ public class Bech32 { } hrp = hrp.toLowerCase(Locale.ROOT); - byte[] checksum = createChecksum(hrp, values); + byte[] checksum = createChecksum(hrp, encoding, values); byte[] combined = new byte[values.length + checksum.length]; System.arraycopy(values, 0, combined, 0, values.length); System.arraycopy(checksum, 0, combined, values.length, checksum.length); @@ -163,14 +180,18 @@ public class Bech32 { values[i] = CHARSET_REV[c]; } String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); - if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum"); - return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); + Encoding encoding = verifyChecksum(hrp, values); + if(encoding == null) { + throw new ProtocolException("Invalid checksum"); + } + + return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6), encoding); } private static byte[] encode(int witnessVersion, byte[] witnessProgram) { byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true); byte[] bytes = new byte[1 + convertedProgram.length]; - bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff); + bytes[0] = (byte)(witnessVersion & 0xff); System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length); return bytes; } @@ -206,4 +227,14 @@ public class Bech32 { } return out.toByteArray(); } + + public enum Encoding { + BECH32(1), BECH32M(0x2bc830a3); + + private final int checksumConstant; + + Encoding(int checksumConstant) { + this.checksumConstant = checksumConstant; + } + } } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index 05435f4..36f5d41 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -135,6 +135,21 @@ public class Script { return false; } + /** + *
If the program somehow pays to a pubkey, returns the pubkey.
+ * + *Otherwise this method throws a ScriptException.
+ */ + public ECKey getPubKey() throws ProtocolException { + for(ScriptType scriptType : SINGLE_KEY_TYPES) { + if(scriptType.isScriptType(this)) { + return scriptType.getPublicKeyFromScript(this); + } + } + + throw new ProtocolException("Script not a standard form that contains a single key"); + } + /** *If the program somehow pays to a hash, returns the hash.
* @@ -160,8 +175,10 @@ public class Script { } } - if(P2PK.isScriptType(this)) { - return new Address[] { P2PK.getAddress(P2PK.getPublicKeyFromScript(this).getPubKey()) }; + for(ScriptType scriptType : SINGLE_KEY_TYPES) { + if(scriptType.isScriptType(this)) { + return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) }; + } } if(MULTISIG.isScriptType(this)) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index 2a0a7c0..8e07b8c 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -974,6 +974,106 @@ public enum ScriptType { public List