add output descriptor checksum support

This commit is contained in:
Craig Raw 2020-09-08 12:03:18 +02:00
parent 794cde7250
commit 08c159ebad
2 changed files with 108 additions and 7 deletions

View file

@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.math.BigInteger;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -20,9 +21,13 @@ import java.util.regex.Pattern;
import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
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 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 int multisigThreshold;
@ -65,11 +70,15 @@ public class OutputDescriptor {
}
public boolean describesMultipleAddresses(ExtendedKey extendedPublicKey) {
return getChildDerivationPath(extendedPublicKey).endsWith("/*");
return getChildDerivationPath(extendedPublicKey) == null || getChildDerivationPath(extendedPublicKey).endsWith("/*");
}
public List<ChildNumber> getReceivingDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
if(childDerivationPath == null) {
childDerivationPath = "/0/*";
}
if(describesMultipleAddresses(extendedPublicKey)) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
@ -85,6 +94,10 @@ public class OutputDescriptor {
public List<ChildNumber> getChangeDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
if(childDerivationPath == null) {
childDerivationPath = "/1/*";
}
if(describesMultipleAddresses(extendedPublicKey)) {
if(childDerivationPath.endsWith("0/*")) {
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) {
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, String> keyChildDerivationMap = new LinkedHashMap<>();
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
@ -280,7 +302,7 @@ public class OutputDescriptor {
String masterFingerprint = null;
String keyDerivationPath = null;
String extPubKey;
String childDerivationPath = "/0/*";
String childDerivationPath = null;
if(matcher.group(1) != null) {
String keyOrigin = matcher.group(1);
@ -309,7 +331,74 @@ public class OutputDescriptor {
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() {
return toString(false);
}
public String toString(boolean addChecksum) {
StringBuilder builder = new StringBuilder();
builder.append(scriptType.getDescriptor());
@ -329,6 +418,12 @@ public class OutputDescriptor {
}
builder.append(scriptType.getCloseDescriptor());
if(addChecksum) {
String descriptor = builder.toString();
builder.append("#");
builder.append(getChecksum(descriptor));
}
return builder.toString();
}

View file

@ -11,7 +11,7 @@ public class OutputDescriptorTest {
@Test
public void electrumP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z");
Assert.assertEquals("pkh(xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z/0/*)", descriptor.toString());
Assert.assertEquals("pkh(xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z)", descriptor.toString());
}
@Test
@ -23,7 +23,7 @@ public class OutputDescriptorTest {
@Test
public void electrumP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR");
Assert.assertEquals("wpkh(xpub69551L7SwJE6mYiKTucL3J9dF53Nd7ujGpw6zKJyukUPgi2apP3KdQMiBEkp5WBFeaQuEqDUmc5LzsfmUFASKDHfkLtQjxuvaEPFXNDF4Kg/0/*)", descriptor.toString());
Assert.assertEquals("wpkh(xpub69551L7SwJE6mYiKTucL3J9dF53Nd7ujGpw6zKJyukUPgi2apP3KdQMiBEkp5WBFeaQuEqDUmc5LzsfmUFASKDHfkLtQjxuvaEPFXNDF4Kg)", descriptor.toString());
}
@Test
@ -35,13 +35,13 @@ public class OutputDescriptorTest {
@Test
public void bip84P2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)", descriptor.toString());
Assert.assertEquals("wpkh(xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V)", descriptor.toString());
}
@Test
public void redditP2SHP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString());
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP))", descriptor.toString());
}
@Test
@ -91,4 +91,10 @@ public class OutputDescriptorTest {
Assert.assertEquals("04ba1ef0", derivation2.getMasterFingerprint());
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));
}
}