mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
add output descriptor checksum support
This commit is contained in:
parent
794cde7250
commit
08c159ebad
2 changed files with 108 additions and 7 deletions
|
@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -20,9 +21,13 @@ import java.util.regex.Pattern;
|
||||||
import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
|
import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
|
||||||
|
|
||||||
public class OutputDescriptor {
|
public class OutputDescriptor {
|
||||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]{100,112})(/[/\\d*'hH]+)?");
|
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||||
|
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH]+)?");
|
||||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
||||||
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
|
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
|
||||||
|
private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$");
|
||||||
|
|
||||||
private final ScriptType scriptType;
|
private final ScriptType scriptType;
|
||||||
private final int multisigThreshold;
|
private final int multisigThreshold;
|
||||||
|
@ -65,11 +70,15 @@ public class OutputDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean describesMultipleAddresses(ExtendedKey extendedPublicKey) {
|
public boolean describesMultipleAddresses(ExtendedKey extendedPublicKey) {
|
||||||
return getChildDerivationPath(extendedPublicKey).endsWith("/*");
|
return getChildDerivationPath(extendedPublicKey) == null || getChildDerivationPath(extendedPublicKey).endsWith("/*");
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChildNumber> getReceivingDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
public List<ChildNumber> getReceivingDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
||||||
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
||||||
|
if(childDerivationPath == null) {
|
||||||
|
childDerivationPath = "/0/*";
|
||||||
|
}
|
||||||
|
|
||||||
if(describesMultipleAddresses(extendedPublicKey)) {
|
if(describesMultipleAddresses(extendedPublicKey)) {
|
||||||
if(childDerivationPath.endsWith("0/*")) {
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||||
|
@ -85,6 +94,10 @@ public class OutputDescriptor {
|
||||||
|
|
||||||
public List<ChildNumber> getChangeDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
public List<ChildNumber> getChangeDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
||||||
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
||||||
|
if(childDerivationPath == null) {
|
||||||
|
childDerivationPath = "/1/*";
|
||||||
|
}
|
||||||
|
|
||||||
if(describesMultipleAddresses(extendedPublicKey)) {
|
if(describesMultipleAddresses(extendedPublicKey)) {
|
||||||
if(childDerivationPath.endsWith("0/*")) {
|
if(childDerivationPath.endsWith("0/*")) {
|
||||||
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
||||||
|
@ -273,6 +286,15 @@ public class OutputDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor) {
|
private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor) {
|
||||||
|
Matcher checksumMatcher = CHECKSUM_PATTERN.matcher(descriptor);
|
||||||
|
if(checksumMatcher.find()) {
|
||||||
|
String checksum = checksumMatcher.group(1);
|
||||||
|
String calculatedChecksum = getChecksum(descriptor.substring(0, checksumMatcher.start()));
|
||||||
|
if(!checksum.equals(calculatedChecksum)) {
|
||||||
|
throw new IllegalArgumentException("Descriptor checksum invalid - checksum of " + checksum + " did not match calculated checksum of " + calculatedChecksum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
|
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
|
||||||
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>();
|
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>();
|
||||||
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
|
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
|
||||||
|
@ -280,7 +302,7 @@ public class OutputDescriptor {
|
||||||
String masterFingerprint = null;
|
String masterFingerprint = null;
|
||||||
String keyDerivationPath = null;
|
String keyDerivationPath = null;
|
||||||
String extPubKey;
|
String extPubKey;
|
||||||
String childDerivationPath = "/0/*";
|
String childDerivationPath = null;
|
||||||
|
|
||||||
if(matcher.group(1) != null) {
|
if(matcher.group(1) != null) {
|
||||||
String keyOrigin = matcher.group(1);
|
String keyOrigin = matcher.group(1);
|
||||||
|
@ -309,7 +331,74 @@ public class OutputDescriptor {
|
||||||
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap);
|
return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getChecksum(String descriptor) {
|
||||||
|
BigInteger c = BigInteger.valueOf(1);
|
||||||
|
int cls = 0;
|
||||||
|
int clscount = 0;
|
||||||
|
for(int i = 0; i < descriptor.length(); i++) {
|
||||||
|
char ch = descriptor.charAt(i);
|
||||||
|
int pos = INPUT_CHARSET.indexOf(ch);
|
||||||
|
|
||||||
|
if(pos < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
c = polyMod(c, pos & 31); // Emit a symbol for the position inside the group, for every character.
|
||||||
|
cls = cls * 3 + (pos >> 5); // Accumulate the group numbers
|
||||||
|
if(++clscount == 3) {
|
||||||
|
// Emit an extra symbol representing the group numbers, for every 3 characters.
|
||||||
|
c = polyMod(c, cls);
|
||||||
|
cls = 0;
|
||||||
|
clscount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(clscount > 0) {
|
||||||
|
c = polyMod(c, cls);
|
||||||
|
}
|
||||||
|
for(int j = 0; j < 8; ++j) {
|
||||||
|
c = polyMod(c, 0); // Shift further to determine the checksum.
|
||||||
|
}
|
||||||
|
c = c.xor(BigInteger.valueOf(1)); // Prevent appending zeroes from not affecting the checksum.
|
||||||
|
|
||||||
|
StringBuilder ret = new StringBuilder();
|
||||||
|
for(int j = 0; j < 8; ++j) {
|
||||||
|
BigInteger index = c.shiftRight(5 * (7 - j)).and(BigInteger.valueOf(31));
|
||||||
|
ret.append(CHECKSUM_CHARSET.charAt(index.intValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigInteger polyMod(BigInteger c, int val)
|
||||||
|
{
|
||||||
|
byte c0 = c.shiftRight(35).byteValue();
|
||||||
|
c = c.and(new BigInteger("7ffffffff", 16)).shiftLeft(5).or(BigInteger.valueOf(val));
|
||||||
|
|
||||||
|
if((c0 & 1) > 0) {
|
||||||
|
c = c.xor(new BigInteger("f5dee51989", 16));
|
||||||
|
}
|
||||||
|
if((c0 & 2) > 0) {
|
||||||
|
c = c.xor(new BigInteger("a9fdca3312", 16));
|
||||||
|
}
|
||||||
|
if((c0 & 4) > 0) {
|
||||||
|
c = c.xor(new BigInteger("1bab10e32d", 16));
|
||||||
|
}
|
||||||
|
if((c0 & 8) > 0) {
|
||||||
|
c = c.xor(new BigInteger("3706b1677a", 16));
|
||||||
|
}
|
||||||
|
if((c0 & 16) > 0) {
|
||||||
|
c = c.xor(new BigInteger("644d626ffd", 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
return toString(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString(boolean addChecksum) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(scriptType.getDescriptor());
|
builder.append(scriptType.getDescriptor());
|
||||||
|
|
||||||
|
@ -329,6 +418,12 @@ public class OutputDescriptor {
|
||||||
}
|
}
|
||||||
builder.append(scriptType.getCloseDescriptor());
|
builder.append(scriptType.getCloseDescriptor());
|
||||||
|
|
||||||
|
if(addChecksum) {
|
||||||
|
String descriptor = builder.toString();
|
||||||
|
builder.append("#");
|
||||||
|
builder.append(getChecksum(descriptor));
|
||||||
|
}
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ public class OutputDescriptorTest {
|
||||||
@Test
|
@Test
|
||||||
public void electrumP2PKH() {
|
public void electrumP2PKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
|
||||||
Assert.assertEquals("pkh(xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z/0/*)", descriptor.toString());
|
Assert.assertEquals("pkh(xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z)", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -23,7 +23,7 @@ public class OutputDescriptorTest {
|
||||||
@Test
|
@Test
|
||||||
public void electrumP2WPKH() {
|
public void electrumP2WPKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
|
||||||
Assert.assertEquals("wpkh(xpub69551L7SwJE6mYiKTucL3J9dF53Nd7ujGpw6zKJyukUPgi2apP3KdQMiBEkp5WBFeaQuEqDUmc5LzsfmUFASKDHfkLtQjxuvaEPFXNDF4Kg/0/*)", descriptor.toString());
|
Assert.assertEquals("wpkh(xpub69551L7SwJE6mYiKTucL3J9dF53Nd7ujGpw6zKJyukUPgi2apP3KdQMiBEkp5WBFeaQuEqDUmc5LzsfmUFASKDHfkLtQjxuvaEPFXNDF4Kg)", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -35,13 +35,13 @@ public class OutputDescriptorTest {
|
||||||
@Test
|
@Test
|
||||||
public void bip84P2WPKH() {
|
public void bip84P2WPKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
|
||||||
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)", descriptor.toString());
|
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V)", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void redditP2SHP2WPKH() {
|
public void redditP2SHP2WPKH() {
|
||||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
|
||||||
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString());
|
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP))", descriptor.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -91,4 +91,10 @@ public class OutputDescriptorTest {
|
||||||
Assert.assertEquals("04ba1ef0", derivation2.getMasterFingerprint());
|
Assert.assertEquals("04ba1ef0", derivation2.getMasterFingerprint());
|
||||||
Assert.assertEquals("m/48'/0'/0'/2'", derivation2.getDerivationPath());
|
Assert.assertEquals("m/48'/0'/0'/2'", derivation2.getDerivationPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testChecksum() {
|
||||||
|
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t");
|
||||||
|
Assert.assertEquals("sh(sortedmulti(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#s66h0xn6", descriptor.toString(true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue