improve validation of bip129, descriptor and unchained wallet imports
What changed, and why it matters
This commit hardens how Sparrow Wallet imports wallet files from other tools. It adds checks that reject malformed or inconsistent imports—such as a multisig setup that says it needs 3 signers but only provides 2 keys, or a coordinator file that would silently make the wallet derive different Bitcoin addresses than the rest of the signing group. The change is defensive: it makes the wallet refuse suspicious imports rather than accepting them.
Review the underlying drongo library changes referenced by the 'drongo' file change to confirm wallet.checkWallet() covers the same edge cases (threshold zero, address derivation consistency, key count). Ensure the new validation does not break legitimate imports with unusual but valid path restrictions, and consider adding release-note guidance for users importing BSMS/Caravan files.
Security signals we found
Added wallet.checkWallet() validation after descriptor, BSMS, Caravan and terminal wallet creation
BIP129 first-address verification prevents coordinator from supplying different keys to each signer
BIP129 path restriction enforcement limits derivation to standard receive/change chains
Caravan import now rejects quorum.totalSigners != number of provided extended public keys
Descriptor import now validates wallet consistency before returning
New test vectors cover threshold-zero, address mismatch, malformed address, non-standard chains and truncated records
Evidence from the diff
The patch improves validation across BIP129 (BSMS) wallet imports, descriptor imports, and Caravan/Unchained multisig imports. It calls wallet.checkWallet() after import in AppController, Descriptor, CaravanMultisig, Bip129 and NewWalletDialog. Bip129 now enforces first-address verification against the descriptor, validates path restrictions, distinguishes sortedmulti vs multi semantics, and rejects unsigned BSMS records more explicitly. CaravanMultisig now rejects mismatches between declared quorum.totalSigners and the number of supplied xpubs. A large set of unit tests and test fixtures were added to cover these cases.
Changed components
src/main/java/com/sparrowwallet/sparrow/AppController.javasrc/main/java/com/sparrowwallet/sparrow/io/Bip129.javasrc/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.javasrc/main/java/com/sparrowwallet/sparrow/io/Descriptor.javasrc/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.javaInspect captured patch +469 / −4
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index 2ebcedf..c016b29 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -1334,6 +1334,13 @@ public class AppController implements Initializable {
return;
}
+ try {
+ wallet.checkWallet();
+ } catch(InvalidWalletException e) {
+ showErrorDialog("Error Importing Wallet", "The imported wallet is not valid: " + e.getMessage());
+ return;
+ }
+
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
nameDlg.initOwner(rootStack.getScene().getWindow());
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java
index 5083a1a..33da237 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Bip129.java
@@ -1,14 +1,20 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.CharStreams;
+import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
+import com.sparrowwallet.drongo.address.Address;
+import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.policy.PolicyType;
+import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
@@ -17,10 +23,16 @@ import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExport, WalletImport {
+ private static final Logger log = LoggerFactory.getLogger(Bip129.class);
+
+ private static final String NO_PATH_RESTRICTIONS = "No path restrictions";
+
@Override
public String getName() {
return "BSMS";
@@ -174,6 +186,8 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
} catch(SignatureException e) {
throw new ImportException("Signature did not match provided public key", e);
}
+ } else {
+ log.info("BSMS record for keystore " + label + " is not signed, the provided public key cannot be verified as originating from the signer");
}
return keystore;
@@ -245,12 +259,102 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
String address = reader.readLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
- return outputDescriptor.toWallet();
+ Wallet wallet = outputDescriptor.toWallet();
+
+ try {
+ wallet.checkWallet();
+ } catch(InvalidWalletException e) {
+ throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
+ }
+
+ List<KeyPurpose> keyPurposes = getPathKeyPurposes(paths);
+ checkFirstAddress(wallet, outputDescriptor, descriptor, keyPurposes, address);
+
+ return wallet;
} catch(Exception e) {
throw new ImportException("Error importing BSMS format", e);
}
}
+ //Returns the key purposes of the provided path restrictions in the order given, or an empty list if the record does not restrict derivation paths
+ //Sparrow derives the standard receive and change chains only, so a record restricted to any other derivation cannot be honoured whether or not it supplies a first address
+ private List<KeyPurpose> getPathKeyPurposes(String paths) {
+ if(paths == null || paths.isBlank() || paths.trim().equalsIgnoreCase(NO_PATH_RESTRICTIONS)) {
+ return Collections.emptyList();
+ }
+
+ List<KeyPurpose> keyPurposes = new ArrayList<>();
+ for(String path : paths.split(",")) {
+ KeyPurpose keyPurpose = getPathKeyPurpose(path.trim());
+ if(keyPurpose == null) {
+ throw new IllegalStateException("This file restricts derivation to " + paths.trim() + ", which is not the standard receive and change derivation. " +
+ "Addresses derived from it would not match those of the other signers in the quorum.");
+ }
+
+ keyPurposes.add(keyPurpose);
+ }
+
+ return keyPurposes;
+ }
+
+ private KeyPurpose getPathKeyPurpose(String path) {
+ for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
+ if(path.equals("/" + keyPurpose.getPathIndex().num() + "/*")) {
+ return keyPurpose;
+ }
+ }
+
+ return null;
+ }
+
+ private void checkFirstAddress(Wallet wallet, OutputDescriptor outputDescriptor, String descriptor, List<KeyPurpose> keyPurposes, String address) {
+ //BIP129 requires the first address, and it is the only means of detecting a coordinator serving a different set of keys to each signer
+ if(address == null || address.isBlank()) {
+ throw new IllegalStateException("This file does not provide a first address, so the descriptor cannot be verified against the other signers in the quorum.");
+ }
+
+ Address recordAddress;
+ try {
+ recordAddress = Address.fromString(address.trim());
+ } catch(InvalidAddressException e) {
+ throw new IllegalStateException("The first address in this file (" + address.trim() + ") is not a valid address: " + e.getMessage());
+ }
+
+ Address firstAddress = getFirstAddress(wallet, outputDescriptor, keyPurposes);
+ if(firstAddress.equals(recordAddress)) {
+ return;
+ }
+
+ if(descriptor.contains("multi(") && !descriptor.contains("sortedmulti(")) {
+ throw new IllegalStateException("The first address in this BSMS record (" + recordAddress + ") does not match the first address of " + firstAddress + " derived by sorting the provided keys");
+ } else {
+ throw new IllegalStateException("The first address in this file (" + recordAddress + ") does not match the first address of the provided descriptor (" + firstAddress + "). " +
+ "The coordinator may be providing a different set of keys to each signer in the quorum.");
+ }
+ }
+
+ //BIP129 defines the first address as the first address of the first path restriction, or where derivation is not restricted, the descriptor's only address
+ private Address getFirstAddress(Wallet wallet, OutputDescriptor outputDescriptor, List<KeyPurpose> keyPurposes) {
+ if(!keyPurposes.isEmpty()) {
+ return wallet.getNode(keyPurposes.getFirst()).getChildren().iterator().next().getAddress();
+ }
+
+ //Only the receive and change chains are derived here whatever chain the descriptor names, so a descriptor of multiple addresses starts at the first receive address
+ if(outputDescriptor.describesMultipleAddresses()) {
+ return wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
+ }
+
+ //A record without path restrictions provides a descriptor fixed at one address, which is only present in this wallet if it is at an index on the receive or change chain
+ List<ChildNumber> childDerivation = outputDescriptor.getChildDerivation();
+ List<ChildNumber> fixedDerivation = childDerivation.subList(1, childDerivation.size());
+ if(fixedDerivation.size() != 2 || KeyPurpose.fromChildNumber(fixedDerivation.getFirst()) == null) {
+ throw new IllegalStateException("This file restricts derivation to " + KeyDerivation.writePath(fixedDerivation) + ", which is not the standard receive and change derivation. " +
+ "Addresses derived from it would not match those of the other signers in the quorum.");
+ }
+
+ return outputDescriptor.getAddress(childDerivation);
+ }
+
@Override
public boolean isWalletImportScannable() {
return true;
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java
index e4ceca6..e2ecf3f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/CaravanMultisig.java
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -79,9 +80,19 @@ public class CaravanMultisig implements WalletImport, WalletExport {
wallet.getKeystores().add(keystore);
}
+ if(cf.quorum.totalSigners != wallet.getKeystores().size()) {
+ throw new IllegalStateException("This file declares a quorum of " + cf.quorum.totalSigners + " signers but provides " + wallet.getKeystores().size() + " extended public keys.");
+ }
+
wallet.setScriptType(scriptType);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
+ try {
+ wallet.checkWallet();
+ } catch(InvalidWalletException e) {
+ throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
+ }
+
return wallet;
} catch(Exception e) {
throw new ImportException("Error importing " + getName() + " wallet", e);
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
index 551e60c..751ae79 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
+import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
@@ -112,7 +113,7 @@ public class Descriptor implements WalletImport, WalletExport {
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
try {
- return ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet());
+ return checkWallet(ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet()));
} catch(Exception e) {
//ignore
}
@@ -120,7 +121,7 @@ public class Descriptor implements WalletImport, WalletExport {
List<String> paragraphs = getParagraphs(secondClone);
for(String paragraph : paragraphs) {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
- return ensureKeyDerivations(descriptor.toWallet());
+ return checkWallet(ensureKeyDerivations(descriptor.toWallet()));
}
throw new ImportException("Could not find an output descriptor in the file");
@@ -152,6 +153,16 @@ public class Descriptor implements WalletImport, WalletExport {
return wallet;
}
+ private static Wallet checkWallet(Wallet wallet) {
+ try {
+ wallet.checkWallet();
+ } catch(InvalidWalletException e) {
+ throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
+ }
+
+ return wallet;
+ }
+
@Override
public boolean isWalletImportScannable() {
return true;
diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
index b02c21c..a21b89b 100644
--- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
@@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.crypto.Key;
+import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
@@ -74,6 +75,15 @@ public abstract class NewWalletDialog extends DialogWindow {
return;
}
+ for(Wallet wallet : wallets) {
+ try {
+ wallet.checkWallet();
+ } catch(InvalidWalletException e) {
+ showErrorDialog("Error Creating Wallet", "The wallet is not valid: " + e.getMessage());
+ return;
+ }
+ }
+
if(AppServices.onlineProperty().get()) {
discoverAccounts(wallets);
} else {
diff --git a/src/test/java/com/sparrowwallet/sparrow/io/Bip129Test.java b/src/test/java/com/sparrowwallet/sparrow/io/Bip129Test.java
new file mode 100644
index 0000000..ec8640e
--- /dev/null
+++ b/src/test/java/com/sparrowwallet/sparrow/io/Bip129Test.java
@@ -0,0 +1,170 @@
+package com.sparrowwallet.sparrow.io;
+
+import com.sparrowwallet.drongo.KeyPurpose;
+import com.sparrowwallet.drongo.policy.PolicyType;
+import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.wallet.Wallet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class Bip129Test extends IoTest {
+ @Test
+ public void importWallet1() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-1.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals(PolicyType.MULTI_HD, wallet.getPolicyType());
+ Assertions.assertEquals(ScriptType.P2WSH, wallet.getScriptType());
+ Assertions.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
+ Assertions.assertEquals(2, wallet.getKeystores().size());
+ Assertions.assertEquals("bc1qfagqa83vrv9phj0886rlx6d68zzd7gejtr0ns8xxfeckve27397q6vq47w",
+ wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress().toString());
+ }
+
+ //Test vector from BIP129 (NO_ENCRYPTION with XPUBs, round 2)
+ @Test
+ public void importWallet2() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-2.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals(ScriptType.P2WSH, wallet.getScriptType());
+ Assertions.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
+ Assertions.assertEquals("bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej",
+ wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress().toString());
+ }
+
+ //Test vector from BIP129 (EXTENDED encryption, round 2) - multi() is interpreted as sortedmulti(), but these keys are not in BIP67 order so sorting derives a different first address
+ @Test
+ public void importWallet3() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-3.bsms"), null));
+ }
+
+ //As above, but with the first address uppercase as bech32 addresses are conventionally QR encoded
+ @Test
+ public void importWallet4() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-4.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals("bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej",
+ wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress().toString());
+ }
+
+ //Sparrow derives the standard receive and change chains only, so a record restricted to any other derivation cannot be honoured
+ @Test
+ public void importWalletUnsupportedPathRestrictions() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-5.bsms"), null));
+ }
+
+ //Path support is a property of the record, so it is rejected for the derivation rather than for the first address it also omits
+ @Test
+ public void importWalletUnsupportedPathRestrictionsNoAddress() {
+ Bip129 bip129 = new Bip129();
+ ImportException e = Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-7.bsms"), null));
+ Assertions.assertTrue(e.getCause().getMessage().contains("standard receive and change derivation"));
+ }
+
+ //BIP129 requires the first address, and without it the descriptor cannot be verified against the other signers in the quorum
+ @Test
+ public void importWalletNoFirstAddress() {
+ Bip129 bip129 = new Bip129();
+ ImportException e = Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-14.bsms"), null));
+ Assertions.assertTrue(e.getCause().getMessage().contains("does not provide a first address"));
+ }
+
+ //Without path restrictions the descriptor describes a single address, which is still checked against the record
+ @Test
+ public void importWalletNoPathRestrictionsSingleAddress() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-8.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
+ }
+
+ //As above for P2SH-P2WSH, where the keys sit at a child number that is otherwise mistaken for a chain index
+ @Test
+ public void importWalletNoPathRestrictionsNestedSegwit() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-10.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals(ScriptType.P2SH_P2WSH, wallet.getScriptType());
+ Assertions.assertEquals(3, wallet.getKeystores().size());
+ }
+
+ //A descriptor fixed to a non-standard chain is rejected even though its first address matches, as that address is not in the wallet derived here
+ @Test
+ public void importWalletNoPathRestrictionsNonStandardChain() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-11.bsms"), null));
+ }
+
+ //A descriptor fixed to the change chain is on a chain derived here, so it is checked and imported
+ @Test
+ public void importWalletNoPathRestrictionsChangeChain() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-12.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals("bc1qr47z3tyaep62u4v3tjwj3gre5aqje242g0aarp0gurnltwpswa0svqrv9y",
+ wallet.getNode(KeyPurpose.CHANGE).getChildren().stream().filter(node -> node.getIndex() == 3).findFirst().orElseThrow().getAddress().toString());
+ }
+
+ //A descriptor fixed at the chain itself rather than an index on it derives an address that is not in the wallet either
+ @Test
+ public void importWalletNoPathRestrictionsChainDepthOnly() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-13.bsms"), null));
+ }
+
+ @Test
+ public void importWalletNoPathRestrictionsAddressMismatch() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-9.bsms"), null));
+ }
+
+ //Only the receive and change chains are derived here, so an unrestricted wildcard descriptor is still checked against the first receive address
+ @Test
+ public void importWalletNoPathRestrictions() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-6.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ Assertions.assertEquals("bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej",
+ wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress().toString());
+ }
+
+ //Declaring no path restrictions must not opt the record out of the first address check
+ @Test
+ public void importWalletNoPathRestrictionsSubstitutedAddress() {
+ Bip129 bip129 = new Bip129();
+ ImportException e = Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-15.bsms"), null));
+ Assertions.assertTrue(e.getCause().getMessage().contains("different set of keys"));
+ }
+
+ //Nor must a blank path restrictions line, which is indistinguishable from a truncated file
+ @Test
+ public void importWalletBlankPathRestrictions() throws ImportException {
+ Bip129 bip129 = new Bip129();
+ Wallet wallet = bip129.importWallet(getInputStream("bsms/multisig-16.bsms"), null);
+ Assertions.assertTrue(wallet.isValid());
+ }
+
+ //The first address is parsed before it is compared, so a malformed address is rejected on every path
+ @Test
+ public void importWalletNoPathRestrictionsMalformedAddress() {
+ Bip129 bip129 = new Bip129();
+ ImportException e = Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-17.bsms"), null));
+ Assertions.assertTrue(e.getCause().getMessage().contains("is not a valid address"));
+ }
+
+ @Test
+ public void importWalletThresholdZero() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-threshold-zero.bsms"), null));
+ }
+
+ @Test
+ public void importWalletAddressMismatch() {
+ Bip129 bip129 = new Bip129();
+ Assertions.assertThrows(ImportException.class, () -> bip129.importWallet(getInputStream("bsms/multisig-address-mismatch.bsms"), null));
+ }
+}
diff --git a/src/test/java/com/sparrowwallet/sparrow/io/CaravanMultisigTest.java b/src/test/java/com/sparrowwallet/sparrow/io/CaravanMultisigTest.java
index 294c2c5..d9e5eec 100644
--- a/src/test/java/com/sparrowwallet/sparrow/io/CaravanMultisigTest.java
+++ b/src/test/java/com/sparrowwallet/sparrow/io/CaravanMultisigTest.java
@@ -30,6 +30,19 @@ public class CaravanMultisigTest extends IoTest {
Assertions.assertEquals("xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
}
+ @Test
+ public void importWallet2() {
+ CaravanMultisig ccMultisig = new CaravanMultisig();
+ Assertions.assertThrows(ImportException.class, () -> ccMultisig.importWallet(getInputStream("caravan-multisig-export-2.json"), null));
+ }
+
+ //A declared quorum larger than the keys provided would import as a smaller wallet deriving different addresses
+ @Test
+ public void importWallet3() {
+ CaravanMultisig ccMultisig = new CaravanMultisig();
+ Assertions.assertThrows(ImportException.class, () -> ccMultisig.importWallet(getInputStream("caravan-multisig-export-3.json"), null));
+ }
+
@Test
public void exportWallet1() throws ImportException, ExportException, IOException {
CaravanMultisig ccMultisig = new CaravanMultisig();
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-1.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-1.bsms
new file mode 100644
index 0000000..9c34bc9
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-1.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[8188029f/48'/0'/0'/2']xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp/**,[3bd5c9b0/48'/0'/0'/2']xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco/**))
+/0/*,/1/*
+bc1qfagqa83vrv9phj0886rlx6d68zzd7gejtr0ns8xxfeckve27397q6vq47w
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-10.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-10.bsms
new file mode 100644
index 0000000..37d9cf4
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-10.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+sh(wsh(sortedmulti(2,[793cc70b/48'/0'/0'/1']xpub6ErVmcYYHmavsMgxEcTZyzN5sqth1ZyRpFNJC26ij1wYGC2SBKYrgt9yariSbn7HLRoZUvhUhmPfsRTPrdhhGFscpPZzmch6UTdmRP1aZUj/0/0,[b3118e52/48'/0'/0'/1']xpub6Du5Jn6eYZE96ccmAc1ZTFPzdnzrvqfG4mpamDun2qZYKywoiQJMCbS3kWWMr6U3XW6s125RLsaPABWgv2yA749ieaMe67FxkTjMsbcxCch/0/0,[842bd2ed/48'/0'/0'/1']xpub6Ex81KopPkEt9hJiWHabYy8LNsSR4A7sUQoFBk9dR8XxHrr4p9HrYWN3NCf5uwfopHnQkCG7FYnZMztKbtRtbh6tzZC4xtHPbmVVxRSN7ic/0/0)))
+No path restrictions
+3MmNkJ3e67jDGNwGL7yQ886T192Bbb81zP
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-11.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-11.bsms
new file mode 100644
index 0000000..c6d618f
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-11.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/5/7,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/5/7))
+No path restrictions
+bc1q8n3lmaupwkl4yag8vqvf083kqjp5gqpscsk3dpgnjjw4lryvxxkqmz3e7y
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-12.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-12.bsms
new file mode 100644
index 0000000..0d6cb6f
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-12.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/1/3,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/1/3))
+No path restrictions
+bc1qr47z3tyaep62u4v3tjwj3gre5aqje242g0aarp0gurnltwpswa0svqrv9y
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-13.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-13.bsms
new file mode 100644
index 0000000..6498b50
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-13.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/0,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/0))
+No path restrictions
+bc1qn0u4ucv5x0gc5qxtm6kja6gdh28vlf44yet953ryd8lffyyrd5sq98z2qu
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-14.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-14.bsms
new file mode 100644
index 0000000..9bb85cc
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-14.bsms
@@ -0,0 +1,3 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+/0/*,/1/*
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-15.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-15.bsms
new file mode 100644
index 0000000..55a677b
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-15.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+No path restrictions
+bc1qng45c3rez2265epnmvhajjj6hlpdv83zn8q8xy6h3mh2pnn8fglq0qgdzk
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-16.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-16.bsms
new file mode 100644
index 0000000..7716fed
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-16.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+
+bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-17.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-17.bsms
new file mode 100644
index 0000000..00c6f04
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-17.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+No path restrictions
+not-an-address
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-2.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-2.bsms
new file mode 100644
index 0000000..ac1795b
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-2.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+/0/*,/1/*
+bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-3.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-3.bsms
new file mode 100644
index 0000000..6606d5d
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-3.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+sh(wsh(multi(2,[793cc70b/48'/0'/0'/1']xpub6ErVmcYYHmavsMgxEcTZyzN5sqth1ZyRpFNJC26ij1wYGC2SBKYrgt9yariSbn7HLRoZUvhUhmPfsRTPrdhhGFscpPZzmch6UTdmRP1aZUj/**,[b3118e52/48'/0'/0'/1']xpub6Du5Jn6eYZE96ccmAc1ZTFPzdnzrvqfG4mpamDun2qZYKywoiQJMCbS3kWWMr6U3XW6s125RLsaPABWgv2yA749ieaMe67FxkTjMsbcxCch/**,[842bd2ed/48'/0'/0'/1']xpub6Ex81KopPkEt9hJiWHabYy8LNsSR4A7sUQoFBk9dR8XxHrr4p9HrYWN3NCf5uwfopHnQkCG7FYnZMztKbtRtbh6tzZC4xtHPbmVVxRSN7ic/**)))
+/0/*,/1/*
+3GzMtFXahiu4TpGNGFc4bHMvAcvz5vVQrT
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-4.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-4.bsms
new file mode 100644
index 0000000..06fa606
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-4.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+/0/*,/1/*
+BC1QRGC6P3KYLFZTU06YSL752GWWUEKHVTFH9VR7ZG43JVU60MUTAMCSV948EJ
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-5.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-5.bsms
new file mode 100644
index 0000000..160a831
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-5.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+/2/*,/3/*
+bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-6.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-6.bsms
new file mode 100644
index 0000000..2d53ce7
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-6.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+No path restrictions
+bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-7.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-7.bsms
new file mode 100644
index 0000000..c89ca15
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-7.bsms
@@ -0,0 +1,3 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/**,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/**))
+/2/*,/3/*
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-8.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-8.bsms
new file mode 100644
index 0000000..b7f4a99
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-8.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/0/0,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/0/0))
+No path restrictions
+bc1qrgc6p3kylfztu06ysl752gwwuekhvtfh9vr7zg43jvu60mutamcsv948ej
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-9.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-9.bsms
new file mode 100644
index 0000000..94d9de0
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-9.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[1cf0bf7e/48'/0'/0'/2']xpub6FL8FhxNNUVnG64YurPd16AfGyvFLhh7S2uSsDqR3Qfcm6o9jtcMYwh6DvmcBF9qozxNQmTCVvWtxLpKTnhVLN3Pgnu2D3pAoXYFgVyd8Yz/0/0,[4fc1dd4a/48'/0'/0'/2']xpub6EebMbEps7ZcV3FYEnddRsvrFWDrt2tiPmCeM7pPXQEmphvq9ZfJ1LWFUDjf3vxCeBuPrfyGrMazWUsYsetrnHatQZVLJH7LsgCjtMqdzgj/0/0))
+No path restrictions
+bc1qng45c3rez2265epnmvhajjj6hlpdv83zn8q8xy6h3mh2pnn8fglq0qgdzk
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-address-mismatch.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-address-mismatch.bsms
new file mode 100644
index 0000000..67275a9
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-address-mismatch.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(2,[8188029f/48'/0'/0'/2']xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp/**,[3bd5c9b0/48'/0'/0'/2']xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco/**))
+/0/*,/1/*
+bc1qng45c3rez2265epnmvhajjj6hlpdv83zn8q8xy6h3mh2pnn8fglq0qgdzk
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-threshold-zero.bsms b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-threshold-zero.bsms
new file mode 100644
index 0000000..187d0f7
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/bsms/multisig-threshold-zero.bsms
@@ -0,0 +1,4 @@
+BSMS 1.0
+wsh(sortedmulti(0,[8188029f/48'/0'/0'/2']xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp/**,[3bd5c9b0/48'/0'/0'/2']xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco/**))
+/0/*,/1/*
+bc1qfagqa83vrv9phj0886rlx6d68zzd7gejtr0ns8xxfeckve27397q6vq47w
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-2.json b/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-2.json
new file mode 100644
index 0000000..d9c99dc
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-2.json
@@ -0,0 +1,36 @@
+{
+ "name": "Test Wallet",
+ "addressType": "P2WSH",
+ "network": "mainnet",
+ "client": {
+ "type": "public"
+ },
+ "quorum": {
+ "requiredSigners": 0,
+ "totalSigners": 3
+ },
+ "extendedPublicKeys": [
+ {
+ "name": "Mercury",
+ "bip32Path": "m/48'/0'/0'/2'",
+ "xpub": "xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp",
+ "xfp": "8188029f",
+ "method": "trezor"
+ },
+ {
+ "name": "Venus",
+ "bip32Path": "m/48'/0'/0'/2'",
+ "xpub": "xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco",
+ "xfp": "a15e8133",
+ "method": "ledger"
+ },
+ {
+ "name": "Earth",
+ "bip32Path": "m/48'/0'/0'/2'",
+ "xpub": "xpub6Ea6qWGr6BWpCSyLo8ggypvXg1MwadiAGZxAHvrBUwvT2ki6pNVG61bg3YEdxkj7FRJ1cLFK7Vvqd9rcDvwCLX7VfEMfJJfdbA1NGsFazvz",
+ "xfp": "d31cf7f9",
+ "method": "coldcard"
+ }
+ ],
+ "startingAddressIndex": 0
+}
\ No newline at end of file
diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-3.json b/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-3.json
new file mode 100644
index 0000000..a2754a9
--- /dev/null
+++ b/src/test/resources/com/sparrowwallet/sparrow/io/caravan-multisig-export-3.json
@@ -0,0 +1,29 @@
+{
+ "name": "Test Wallet",
+ "addressType": "P2WSH",
+ "network": "mainnet",
+ "client": {
+ "type": "public"
+ },
+ "quorum": {
+ "requiredSigners": 2,
+ "totalSigners": 3
+ },
+ "extendedPublicKeys": [
+ {
+ "name": "Mercury",
+ "bip32Path": "m/48'/0'/0'/2'",
+ "xpub": "xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp",
+ "xfp": "8188029f",
+ "method": "trezor"
+ },
+ {
+ "name": "Venus",
+ "bip32Path": "m/48'/0'/0'/2'",
+ "xpub": "xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco",
+ "xfp": "a15e8133",
+ "method": "ledger"
+ }
+ ],
+ "startingAddressIndex": 0
+}
Why this scored 60/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.