From 8e4924783224fc894b45855cdbd866981413db3b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 13 Nov 2021 15:04:37 +0200 Subject: [PATCH] introduce multipath derivations to output descriptors and use h for hardened indexes --- .../sparrowwallet/drongo/KeyDerivation.java | 14 +++--- .../drongo/OutputDescriptor.java | 43 ++++++++++++++++--- .../drongo/crypto/ChildNumber.java | 6 ++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java b/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java index b1f4e10..2f811ec 100644 --- a/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java +++ b/src/main/java/com/sparrowwallet/drongo/KeyDerivation.java @@ -68,13 +68,17 @@ public class KeyDerivation { } public static String writePath(List pathList) { - String path = "m"; - for (ChildNumber child: pathList) { - path += "/"; - path += child.toString(); + return writePath(pathList, true); + } + + public static String writePath(List pathList, boolean useApostrophes) { + StringBuilder path = new StringBuilder("m"); + for(ChildNumber child: pathList) { + path.append("/"); + path.append(child.toString(useApostrophes)); } - return path; + return path.toString(); } public static boolean isValid(String derivationPath) { diff --git a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java index 0e8bfd9..e5ac381 100644 --- a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java +++ b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java @@ -22,10 +22,11 @@ public class OutputDescriptor { 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 XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?"); private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})"); 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 MULTIPATH_PATTERN = Pattern.compile("<([\\d*'hH;]+)>"); private static final Pattern CHECKSUM_PATTERN = Pattern.compile("#([" + CHECKSUM_CHARSET + "]{8})$"); private final ScriptType scriptType; @@ -258,12 +259,25 @@ public class OutputDescriptor { } public static OutputDescriptor getOutputDescriptor(Wallet wallet, KeyPurpose keyPurpose, Integer index) { + return getOutputDescriptor(wallet, keyPurpose == null ? null : List.of(keyPurpose), index); + } + + public static OutputDescriptor getOutputDescriptor(Wallet wallet, List keyPurposes, Integer index) { Map extendedKeyDerivationMap = new LinkedHashMap<>(); Map extendedKeyChildDerivationMap = new LinkedHashMap<>(); for(Keystore keystore : wallet.getKeystores()) { extendedKeyDerivationMap.put(keystore.getExtendedPublicKey(), keystore.getKeyDerivation()); - if(keyPurpose != null) { - extendedKeyChildDerivationMap.put(keystore.getExtendedPublicKey(), keyPurpose.getPathIndex().num() + "/" + (index == null ? "*" : index)); + if(keyPurposes != null) { + String chain; + if(keyPurposes.size() == 1) { + chain = Integer.toString(keyPurposes.get(0).getPathIndex().num()); + } else { + StringJoiner joiner = new StringJoiner(";"); + keyPurposes.forEach(keyPurpose -> joiner.add(Integer.toString(keyPurpose.getPathIndex().num()))); + chain = "<" + joiner + ">"; + } + + extendedKeyChildDerivationMap.put(keystore.getExtendedPublicKey(), chain + "/" + (index == null ? "*" : index)); } } @@ -395,7 +409,7 @@ public class OutputDescriptor { 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)); + c = c.and(new BigInteger("7ffffffff", 16)).shiftLeft(5).xor(BigInteger.valueOf(val)); if((c0 & 1) > 0) { c = c.xor(new BigInteger("f5dee51989", 16)); @@ -461,10 +475,10 @@ public class OutputDescriptor { Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator(); sortedKeys.sort((o1, o2) -> { - List derivation1 = KeyDerivation.parsePath(mapChildrenDerivations.get(o1)); + List derivation1 = getDerivations(mapChildrenDerivations.get(o1)).get(0); derivation1.add(0, o1.getKeyChildNumber()); ECKey key1 = o1.getKey(derivation1); - List derivation2 = KeyDerivation.parsePath(mapChildrenDerivations.get(o2)); + List derivation2 = getDerivations(mapChildrenDerivations.get(o2)).get(0); derivation2.add(0, o2.getKeyChildNumber()); ECKey key2 = o2.getKey(derivation2); return lexicographicByteArrayComparator.compare(key1.getPubKey(), key2.getPubKey()); @@ -473,6 +487,21 @@ public class OutputDescriptor { return sortedKeys; } + private List> getDerivations(String childDerivation) { + Matcher matcher = MULTIPATH_PATTERN.matcher(childDerivation); + if(matcher.find()) { + String multipath = matcher.group(1); + String[] paths = multipath.split(";"); + List> derivations = new ArrayList<>(); + for(String path : paths) { + derivations.add(KeyDerivation.parsePath(childDerivation.replace(matcher.group(), path))); + } + return derivations; + } else { + return List.of(KeyDerivation.parsePath(childDerivation)); + } + } + private String toString(ExtendedKey pubKey, boolean addKeyOrigin) { StringBuilder keyBuilder = new StringBuilder(); KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey); @@ -482,7 +511,7 @@ public class OutputDescriptor { keyBuilder.append(keyDerivation.getMasterFingerprint()); keyBuilder.append("/"); } - keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "")); + keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", "").replace('\'', 'h')); keyBuilder.append("]"); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ChildNumber.java b/src/main/java/com/sparrowwallet/drongo/crypto/ChildNumber.java index 67ec957..0f83fbb 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ChildNumber.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ChildNumber.java @@ -46,7 +46,11 @@ public class ChildNumber { public int i() { return i; } public String toString() { - return String.format(Locale.US, "%d%s", num(), isHardened() ? "'" : ""); + return toString(true); + } + + public String toString(boolean useApostrophes) { + return String.format(Locale.US, "%d%s", num(), isHardened() ? (useApostrophes ? "'" : "h") : ""); } public boolean equals(Object o) {