add tests for descriptor import and export, and handle multiline descriptor
What changed, and why it matters
This commit changes how Sparrow Wallet reads multi-line Bitcoin wallet descriptors from files. Previously, the importer would join non-empty lines together into one paragraph, which could accidentally merge separate descriptors into a single broken string. Now each non-empty, non-comment line is treated as its own descriptor. The change is mostly a bug fix and test addition; it does not appear to be a security patch, but the old behavior could cause import failures or confusion when a file contained multiple descriptors.
No immediate security action required. Reviewers may want to verify that treating each line as an independent descriptor does not break legitimate multi-line single descriptors or introduce parsing edge cases, and confirm the behavior is covered by the new tests.
Security signals we found
Parsing logic change for wallet descriptor import
Potential for malformed descriptor parsing when multiple descriptors are present in one file
No explicit security claims in commit message or diff
Evidence from the diff
The patch modifies Descriptor.getParagraphs() in Sparrow Wallet. The old implementation accumulated trimmed, non-comment lines into a StringBuilder, splitting only on blank lines, and stripped any prefix ending in ‘:’ from each line. The new implementation treats every non-empty, non-comment line as an independent paragraph, still stripping label prefixes. This fixes handling of files where multiple descriptors appear on consecutive lines without a blank separator, and where a single logical descriptor is split across multiple lines (the latter would now be parsed as separate paragraphs, which the commit title suggests is the intended ‘multiline descriptor’ support). The commit also adds unit tests and test fixtures for import/export round-trips, multipath descriptors, labelled descriptors, and separated receive/change descriptors.
Changed components
src/main/java/com/sparrowwallet/sparrow/io/Descriptor.javaWallet descriptor import/export functionalityInspect captured patch +121 / −14
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
index d3caae6..ea8acb1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
@@ -114,26 +114,14 @@ public class Descriptor implements WalletImport, WalletExport {
private static List<String> getParagraphs(InputStream inputStream) {
List<String> paragraphs = new ArrayList<>();
- StringBuilder paragraph = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
- if(line.isEmpty()) {
- if(!paragraph.isEmpty()) {
- paragraphs.add(paragraph.toString());
- paragraph.setLength(0);
- }
- } else if(line.startsWith("#")) {
- continue;
- } else {
- paragraph.append(line.replaceFirst("^.+:", "").trim());
+ if(!line.isEmpty() && !line.startsWith("#")) {
+ paragraphs.add(line.replaceFirst("^.+:", "").trim());
}
}
- if(!paragraph.isEmpty()) {
- paragraphs.add(paragraph.toString());
- }
-
return paragraphs;
}
diff --git a/src/test/java/com/sparrowwallet/sparrow/io/DescriptorTest.java b/src/test/java/com/sparrowwallet/sparrow/io/DescriptorTest.java
new file mode 100644
index 0000000..9265a07
--- /dev/null
+++ b/src/test/java/com/sparrowwallet/sparrow/io/DescriptorTest.java
@@ -0,0 +1,111 @@
+package com.sparrowwallet.sparrow.io;
+
+import com.sparrowwallet.drongo.ExtendedKey;
+import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.wallet.Keystore;
+import com.sparrowwallet.drongo.wallet.Wallet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+
+public class DescriptorTest extends IoTest {
+ @Test
+ public void testImport() throws ImportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive.txt"), null);
+
+ Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+
+ @Test
+ public void testImportMultipath() throws ImportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
+
+ Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+
+ @Test
+ public void testImportSeparateDescriptors() throws ImportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive-change1.txt"), null);
+
+ Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+
+ @Test
+ public void testExport() throws ImportException, ExportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ descriptor.exportWallet(wallet, baos, null);
+ String export = baos.toString();
+
+ Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/<0;1>/*)#cpx4ean7"));
+ Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda"));
+ Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9"));
+ }
+
+ @Test
+ public void testImportExport() throws ImportException, ExportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ descriptor.exportWallet(wallet, baos, null);
+
+ Wallet reimported = descriptor.importWallet(new ByteArrayInputStream(baos.toByteArray()), null);
+
+ Assertions.assertEquals(wallet.getScriptType(), reimported.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Keystore reimportedKeystore = reimported.getKeystores().getFirst();
+ Assertions.assertEquals(keystore.getKeyDerivation().getDerivationPath(), reimportedKeystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals(keystore.getKeyDerivation().getMasterFingerprint(), reimportedKeystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(keystore.getExtendedPublicKey(), reimportedKeystore.getExtendedPublicKey());
+ Assertions.assertTrue(reimportedKeystore.isValid());
+ }
+
+ @Test
+ public void testImportLabelled() throws ImportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-labelled.txt"), null);
+
+ Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+
+ @Test
+ public void testImportSeparateDescriptorsNoBlankLine() throws ImportException {
+ Descriptor descriptor = new Descriptor();
+ Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive-change2.txt"), null);
+
+ Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
+ Keystore keystore = wallet.getKeystores().getFirst();
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+}
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-labelled.txt b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-labelled.txt
new file mode 100644
index 0000000..2bdffd9
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-labelled.txt
@@ -0,0 +1 @@
+Receive: wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-multipath.txt b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-multipath.txt
new file mode 100644
index 0000000..6f5405a
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-multipath.txt
@@ -0,0 +1 @@
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/<0;1>/*)#cpx4ean7
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change1.txt b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change1.txt
new file mode 100644
index 0000000..f96858a
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change1.txt
@@ -0,0 +1,3 @@
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
+
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change2.txt b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change2.txt
new file mode 100644
index 0000000..ff2a9c8
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive-change2.txt
@@ -0,0 +1,2 @@
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive.txt b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive.txt
new file mode 100644
index 0000000..d98a0c7
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/descriptor-receive.txt
@@ -0,0 +1 @@
+wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.