From 99d779c657d73fb7dddff3a95924af9908284626 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 29 May 2023 14:35:46 +0200 Subject: [PATCH] support range and pair crypto-hdkey path components --- .../hummingbird/registry/CryptoKeypath.java | 26 +----- .../hummingbird/registry/PathComponent.java | 37 --------- .../pathcomponent/IndexPathComponent.java | 27 ++++++ .../pathcomponent/PairPathComponent.java | 23 ++++++ .../registry/pathcomponent/PathComponent.java | 82 +++++++++++++++++++ .../pathcomponent/RangePathComponent.java | 37 +++++++++ .../pathcomponent/WildcardPathComponent.java | 17 ++++ .../registry/CryptoAccountTest.java | 18 +++- 8 files changed, 205 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/IndexPathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PairPathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/RangePathComponent.java create mode 100644 src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/WildcardPathComponent.java diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java index 64bf237..198d884 100644 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/CryptoKeypath.java @@ -1,6 +1,7 @@ package com.sparrowwallet.hummingbird.registry; import co.nstant.in.cbor.model.*; +import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent; import java.math.BigInteger; import java.util.ArrayList; @@ -38,7 +39,7 @@ public class CryptoKeypath extends RegistryItem { StringJoiner joiner = new StringJoiner("/"); for(PathComponent component : components) { - joiner.add((component.isWildcard() ? "*" : component.getIndex()) + (component.isHardened() ? "'" : "")); + joiner.add(component.toString()); } return joiner.toString(); } @@ -53,16 +54,7 @@ public class CryptoKeypath extends RegistryItem { public DataItem toCbor() { Map map = new Map(); - Array componentArray = new Array(); - for(PathComponent pathComponent : components) { - if(pathComponent.isWildcard()) { - componentArray.add(new Array()); - } else { - componentArray.add(new UnsignedInteger(pathComponent.getIndex())); - } - componentArray.add(pathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); - } - map.put(new UnsignedInteger(COMPONENTS_KEY), componentArray); + map.put(new UnsignedInteger(COMPONENTS_KEY), PathComponent.toCbor(components)); if(sourceFingerprint != null) { map.put(new UnsignedInteger(SOURCE_FINGERPRINT_KEY), new UnsignedInteger(new BigInteger(1, sourceFingerprint))); } @@ -87,17 +79,7 @@ public class CryptoKeypath extends RegistryItem { UnsignedInteger uintKey = (UnsignedInteger)key; int intKey = uintKey.getValue().intValue(); if(intKey == COMPONENTS_KEY) { - Array componentArray = (Array)map.get(key); - for(int i = 0; i < componentArray.getDataItems().size(); i+=2) { - boolean hardened = (componentArray.getDataItems().get(i+1) == SimpleValue.TRUE); - DataItem pathSeg = componentArray.getDataItems().get(i); - if(pathSeg instanceof UnsignedInteger) { - UnsignedInteger uintIndex = (UnsignedInteger)pathSeg; - components.add(new PathComponent(uintIndex.getValue().intValue(), hardened)); - } else if(pathSeg instanceof Array) { - components.add(new PathComponent(hardened)); - } - } + components = PathComponent.fromCbor(map.get(key)); } else if(intKey == SOURCE_FINGERPRINT_KEY) { sourceFingerprint = bigIntegerToBytes(((UnsignedInteger)map.get(key)).getValue(), 4); } else if(intKey == DEPTH_KEY) { diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java deleted file mode 100644 index faf395e..0000000 --- a/src/main/java/com/sparrowwallet/hummingbird/registry/PathComponent.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sparrowwallet.hummingbird.registry; - -public class PathComponent { - public static final int HARDENED_BIT = 0x80000000; - - private final int index; - private final boolean wildcard; - private final boolean hardened; - - public PathComponent(int index, boolean hardened) { - this.index = index; - this.wildcard = false; - this.hardened = hardened; - - if((index & HARDENED_BIT) != 0) { - throw new IllegalArgumentException("Invalid index " + index + " - most significant bit cannot be set"); - } - } - - public PathComponent(boolean hardened) { - this.index = 0; - this.wildcard = true; - this.hardened = hardened; - } - - public int getIndex() { - return index; - } - - public boolean isWildcard() { - return wildcard; - } - - public boolean isHardened() { - return hardened; - } -} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/IndexPathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/IndexPathComponent.java new file mode 100644 index 0000000..973f4f2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/IndexPathComponent.java @@ -0,0 +1,27 @@ +package com.sparrowwallet.hummingbird.registry.pathcomponent; + +public class IndexPathComponent extends PathComponent { + private final int index; + private final boolean hardened; + + public IndexPathComponent(int index, boolean hardened) { + this.index = index; + this.hardened = hardened; + + if((index & HARDENED_BIT) != 0) { + throw new IllegalArgumentException("Invalid index " + index + " - most significant bit cannot be set"); + } + } + + public int getIndex() { + return index; + } + + public boolean isHardened() { + return hardened; + } + + public String toString() { + return index + (hardened ? "'" : ""); + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PairPathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PairPathComponent.java new file mode 100644 index 0000000..0546393 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PairPathComponent.java @@ -0,0 +1,23 @@ +package com.sparrowwallet.hummingbird.registry.pathcomponent; + +public class PairPathComponent extends PathComponent { + private final IndexPathComponent external; + private final IndexPathComponent internal; + + public PairPathComponent(IndexPathComponent external, IndexPathComponent internal) { + this.external = external; + this.internal = internal; + } + + public IndexPathComponent getExternal() { + return external; + } + + public IndexPathComponent getInternal() { + return internal; + } + + public String toString() { + return "<" + external.toString() + ";" + internal.toString() + ">"; + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PathComponent.java new file mode 100644 index 0000000..a7686d2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/PathComponent.java @@ -0,0 +1,82 @@ +package com.sparrowwallet.hummingbird.registry.pathcomponent; + +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.SimpleValue; +import co.nstant.in.cbor.model.UnsignedInteger; + +import java.util.ArrayList; +import java.util.List; + +public abstract class PathComponent { + public static final int HARDENED_BIT = 0x80000000; + + public static DataItem toCbor(List components) { + Array componentArray = new Array(); + for(PathComponent pathComponent : components) { + if(pathComponent instanceof WildcardPathComponent) { + WildcardPathComponent wildcardPathComponent = (WildcardPathComponent)pathComponent; + componentArray.add(new Array()); + componentArray.add(wildcardPathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); + } else if(pathComponent instanceof RangePathComponent) { + RangePathComponent rangePathComponent = (RangePathComponent)pathComponent; + Array array = new Array(); + array.add(new UnsignedInteger(rangePathComponent.getStart())); + array.add(new UnsignedInteger(rangePathComponent.getEnd())); + componentArray.add(array); + componentArray.add(rangePathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); + } else if(pathComponent instanceof PairPathComponent) { + PairPathComponent pairPathComponent = (PairPathComponent)pathComponent; + Array array = new Array(); + array.add(new UnsignedInteger(pairPathComponent.getExternal().getIndex())); + array.add(pairPathComponent.getExternal().isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); + array.add(new UnsignedInteger(pairPathComponent.getInternal().getIndex())); + array.add(pairPathComponent.getInternal().isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); + componentArray.add(array); + } else if(pathComponent instanceof IndexPathComponent) { + IndexPathComponent indexPathComponent = (IndexPathComponent)pathComponent; + componentArray.add(new UnsignedInteger(indexPathComponent.getIndex())); + componentArray.add(indexPathComponent.isHardened() ? SimpleValue.TRUE : SimpleValue.FALSE); + } else { + throw new IllegalArgumentException("Unknown path component of " + pathComponent.getClass()); + } + } + + return componentArray; + } + + public static List fromCbor(DataItem item) { + List components = new ArrayList<>(); + + Array componentArray = (Array)item; + for(int i = 0; i < componentArray.getDataItems().size(); i++) { + DataItem component = componentArray.getDataItems().get(i); + if(component instanceof Array) { + Array subcomponentArray = (Array)component; + if(subcomponentArray.getDataItems().isEmpty()) { + boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE); + components.add(new WildcardPathComponent(hardened)); + } else if(subcomponentArray.getDataItems().size() == 2) { + boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE); + UnsignedInteger startIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(0); + UnsignedInteger endIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(1); + components.add(new RangePathComponent(startIndex.getValue().intValue(), endIndex.getValue().intValue(), hardened)); + } else if(subcomponentArray.getDataItems().size() == 4) { + UnsignedInteger externalIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(0); + boolean externalHardened = (subcomponentArray.getDataItems().get(1) == SimpleValue.TRUE); + IndexPathComponent externalPathComponent = new IndexPathComponent(externalIndex.getValue().intValue(), externalHardened); + UnsignedInteger internalIndex = (UnsignedInteger)subcomponentArray.getDataItems().get(2); + boolean internalHardened = (subcomponentArray.getDataItems().get(3) == SimpleValue.TRUE); + IndexPathComponent internalPathComponent = new IndexPathComponent(internalIndex.getValue().intValue(), internalHardened); + components.add(new PairPathComponent(externalPathComponent, internalPathComponent)); + } + } else if(component instanceof UnsignedInteger) { + UnsignedInteger index = (UnsignedInteger)component; + boolean hardened = (componentArray.getDataItems().get(++i) == SimpleValue.TRUE); + components.add(new IndexPathComponent(index.getValue().intValue(), hardened)); + } + } + + return components; + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/RangePathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/RangePathComponent.java new file mode 100644 index 0000000..862b5d5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/RangePathComponent.java @@ -0,0 +1,37 @@ +package com.sparrowwallet.hummingbird.registry.pathcomponent; + +public class RangePathComponent extends PathComponent { + private final int start; + private final int end; + private final boolean hardened; + + public RangePathComponent(int start, int end, boolean hardened) { + this.start = start; + this.end = end; + this.hardened = hardened; + + if((start & HARDENED_BIT) != 0 || (end & HARDENED_BIT) != 0) { + throw new IllegalArgumentException("Invalid range [" + start + ", " + end + "] - most significant bit cannot be set"); + } + + if(start >= end) { + throw new IllegalArgumentException("Invalid range [" + start + ", " + end + "] - start must be lower than end"); + } + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public boolean isHardened() { + return hardened; + } + + public String toString() { + return "[" + start + (hardened ? "'" : "") + "-" + end + (hardened ? "'" : "") + "]"; + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/WildcardPathComponent.java b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/WildcardPathComponent.java new file mode 100644 index 0000000..8e4baa2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/registry/pathcomponent/WildcardPathComponent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.hummingbird.registry.pathcomponent; + +public class WildcardPathComponent extends PathComponent { + private final boolean hardened; + + public WildcardPathComponent(boolean hardened) { + this.hardened = hardened; + } + + public boolean isHardened() { + return hardened; + } + + public String toString() { + return "*"; + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java index 96b3560..5b66e16 100644 --- a/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java +++ b/src/test/java/com/sparrowwallet/hummingbird/registry/CryptoAccountTest.java @@ -3,9 +3,7 @@ package com.sparrowwallet.hummingbird.registry; import co.nstant.in.cbor.CborDecoder; import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.DataItem; -import com.sparrowwallet.hummingbird.TestUtils; -import com.sparrowwallet.hummingbird.UR; -import com.sparrowwallet.hummingbird.UREncoder; +import com.sparrowwallet.hummingbird.*; import org.junit.Assert; import org.junit.Test; @@ -159,4 +157,18 @@ public class CryptoAccountTest { String encoded = UREncoder.encode(ur); Assert.assertEquals("ur:crypto-account/oeadcyemrewytyaolntaadmutaaddloxaxhdclaxwmfmdeiamecsdsemgtvsjzcncygrkowtrontzschgezokstswkkscfmklrtauteyaahdcxiehfonurdppfyntapejpproypegrdawkgmaewejlsfdtsrfybdehcaflmtrlbdhpamtaaddyoyadlncsdwykaeykaeykaycynlytsnyltaadmhtaadmwtaaddloxaxhdclaostvelfemdyynwydwyaievosrgmambklovabdgypdglldvespsthysadamhpmjeinaahdcxntdllnaaeykoytdacygegwhgjsiyonpywmcmrpwphsvodsrerozsbyaxluzcoxdpamtaaddyoyadlncsehykaeykaeykaycypdbskeuytaadmwtaaddloxaxhdclaxzcfxeegdrpmogrgwkbzctlttweadkiengrwlhtprremouoluutqdpfbncedkynfhaahdcxjpwevdeogthttkmeswzcolcpsaahcfnshkhtehytclmnteatmoteadtlwynnftloamtaaddyoyadlncsghykaeykaeykaycybthlvytstaadmhtaaddloxaxhdclaxhhsnhdrpftdwuocntilydibehnecmovdfekpjkclcslasbhkpawsaddmcmmnahnyaahdcxlotedtndfymyltclhlmtpfsadscnhtztaolbnnkistaedegwfmmedreetnwmcycnamtaaddyoyadlfcsdpykaycyemrewytytaadmhtaadmetaaddloxaxhdclaxdwkswmztpytnswtsecnblfbayajkdldeclqzzolrsnhljedsgminetytbnahatbyaahdcxkkguwsvyimjkvwteytwztyswvendtpmncpasfrrylprnhtkblndrgrmkoyjtbkrpamtaaddyoyadlocsdyykaeykaeykadykaycyhkrpnddrtaadmetaaddloxaxhdclaohnhffmvsbndslrfgclpfjejyatbdpebacnzokotofxntaoemvskpaowmryfnotfgaahdcxdlnbvecentssfsssgylnhkrstoytecrdlyadrekirfaybglahltalsrfcaeerobwamtaaddyoyadlocsdyykaeykaeykaoykaycyhkrpnddrgdaogykb", encoded); } + + @Test + public void testPairPathComponent() throws Exception { + String ur = "ur:crypto-account/oeadcylpvefyjeaolttaadeetaadmutaaddlonaxhdclaxhkfzdphtkplevtqzprkgnnsacagesgctzmspytctdstocsbgmkamurrdpffmtsseaahdcxpdnbdysgfeaycfsegwmhwfjewzfwdmesrlqdhglagahkytcflbrtsedraefwbweyamtaaddyoeadlncsdwykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycypsjpsbfntaadeetaadmhtaadmwtaaddlonaxhdclaouypakomsrlhlqzvwlymesadevybkueqdoxhdcximrdhgfhtybwvyhdkeyklddpmwaahdcxwnvsaxgsdldtmurknyfpcedyiaiopautyafrpejoaxjncfhhhtpfdetteotnknsramtaaddyoeadlncsehykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyoeyagmmstaadeetaadmwtaaddlonaxhdclaorernoyonguhlfsdecnmohnuydwnnchjpuyroftdnaadkatcfkirdtkispsiajycxaahdcxlyutkbnymklbdskesbihbelysedwlupklfmhnntlfwsegsvwwspkghkgvwheiejoamtaaddyoeadlncsghykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycywyqzwzkptaadeetaadmhtaadnytaaddlonaxhdclaoiorpcxecynfloxtamhlsaarkkotpclcydahtamluhnimkertlgftknoerymobaneaahdcxrsatfdqdenvtspvdieindaztrpcazsknzsoywkghfhtyswdkkpjstbneayfncxoxamtaaddyoeadlfcsdpykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycylpvefyjetaadeetaadmhtaadmetaadnytaaddlonaxhdclaoldpfglrkwntlvwlarsdmnbzefmghkeeeoteerdpfframuoidstpacswmnnutcwkkaahdcxhkrozmjneceoldplnluetscyjnbdhsldenontncafgdkmdvlsnidamlnvwfzsstaamtaaddyoeadlocsdyykaeykaeykadykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyrhimryuotaadeetaadmetaadnytaaddlonaxhdclaowysfahwsqdclsnfmwmjlgoeerhzoteherosnmyhkinolrlspdadsasswlarpvtpfaahdcxwybnryoewshycwghlfhhwtbscxpyylenpkolmoftgymksbdpvsgtlbdefhrswnyaamtaaddyoeadlocsdyykaeykaeykaoykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycyrhimryuotaadeetaadnltaaddlonaxhdclaxpmctzmjzemjspsplgrlgtyvssffrzcrlgogojnmncwbdtytelblyhlflmtdnkifdaahdcxgytkgujnjtaszejprdpmoskseehkamvlcersoxdlghsglagmjkjewmrlpfonwldwamtaaddyoeadlncshfykaeykaeykaocylpvefyjeattaaddyoyadlslraewkadwklawkaycynsmwdtfzryrorncp"; + URDecoder urDecoder = new URDecoder(); + urDecoder.receivePart(ur); + URDecoder.Result urResult = urDecoder.getResult(); + if(urResult.type == ResultType.SUCCESS) { + if(urResult.ur.getRegistryType().equals(RegistryType.CRYPTO_ACCOUNT)) { + CryptoAccount cryptoAccount = (CryptoAccount)urResult.ur.decodeFromRegistry(); + Assert.assertEquals("<0;1>/*", cryptoAccount.getOutputDescriptors().get(0).getHdKey().getChildren().getPath()); + } + } + } }