various sp wallet related fixes and improvements

This commit is contained in:
Craig Raw 2026-04-22 14:51:52 +02:00
parent 15e5aaa4b9
commit bc526c90e1
7 changed files with 95 additions and 16 deletions

View File

@ -671,9 +671,14 @@ public class OutputDescriptor {
private static OutputDescriptor parseTwoArgSp(String scanArg, String spendArg, Map<String, Integer> annotations) {
KeyDerivationAndKey originResult = parseKeyOrigin(scanArg);
KeyDerivation keyDerivation = originResult.keyDerivation();
scanArg = originResult.key();
if(keyDerivation.getDerivation().size() > 2) {
List<ChildNumber> accountDerivation = keyDerivation.getDerivation().subList(0, keyDerivation.getDerivation().size() - 2);
if(KeyDerivation.getBip352ScanDerivation(accountDerivation).equals(keyDerivation.getDerivation())) {
keyDerivation = new KeyDerivation(keyDerivation.getMasterFingerprint(), accountDerivation);
}
}
ECKey scanPrivateKey = parseSilentPaymentScanKey(scanArg);
ECKey scanPrivateKey = parseSilentPaymentScanKey(originResult.key());
ECKey spendKey = parseSilentPaymentSpendKey(spendArg);
ECKey spendPubKey = spendKey.isPubKeyOnly() ? spendKey : ECKey.fromPublicOnly(spendKey.getPubKey());
@ -965,6 +970,25 @@ public class OutputDescriptor {
return keyBuilder.toString();
}
public static String writeKey(ECKey ecKey, KeyDerivation keyDerivation, boolean addKeyOrigin) {
return writeKey(ecKey, keyDerivation, addKeyOrigin, false);
}
public static String writeKey(ECKey ecKey, KeyDerivation keyDerivation, boolean addKeyOrigin, boolean useApostrophes) {
StringBuilder keyBuilder = new StringBuilder();
if(addKeyOrigin && keyDerivation != null && keyDerivation.getMasterFingerprint() != null && keyDerivation.getMasterFingerprint().length() == 8 && Utils.isHex(keyDerivation.getMasterFingerprint())) {
keyBuilder.append("[");
keyBuilder.append(keyDerivation.getMasterFingerprint());
if(!keyDerivation.getDerivation().isEmpty()) {
keyBuilder.append(KeyDerivation.writePath(keyDerivation.getDerivation(), useApostrophes).substring(1));
}
keyBuilder.append("]");
}
keyBuilder.append(ecKey.hasPrivKey() ? ecKey.getPrivateKeyEncoded().toString() : Utils.bytesToHex(ecKey.getPubKey()));
return keyBuilder.toString();
}
public static String writeKey(ExtendedKey pubKey, KeyDerivation keyDerivation, String childDerivation, boolean addKeyOrigin, boolean addKey) {
return writeKey(pubKey, keyDerivation, childDerivation, addKeyOrigin, addKey, false);
}

View File

@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.*;
import com.sparrowwallet.drongo.wallet.*;
@ -161,7 +162,7 @@ public class PSBT {
byte[] silentPaymentsTweak = walletNode.getSilentPaymentTweak();
Map<ECKey, KeyDerivation> spSpendDerivations = new LinkedHashMap<>();
for(Keystore keystore : signingWallet.getKeystores()) {
if(silentPaymentsTweak != null && keystore.getSilentPaymentScanAddress() != null) {
if(silentPaymentsTweak != null && keystore.getSilentPaymentScanAddress() != null && signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
KeyDerivation spendKeyDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
spSpendDerivations.put(spendPubKey, spendKeyDerivation);
@ -209,9 +210,9 @@ public class PSBT {
SilentPaymentAddress outputSpAddress = null;
Long outputSpLabel = null;
for(Keystore keystore : recipientWallet.getKeystores()) {
if(outputNode.getSilentPaymentTweak() != null && keystore.getSilentPaymentScanAddress() != null) {
if(outputNode.getSilentPaymentTweak() != null && keystore.getSilentPaymentScanAddress() != null && recipientWallet.getPolicyType() == PolicyType.SINGLE_SP) {
SilentPaymentScanAddress changeAddress = keystore.getSilentPaymentScanAddress().getChangeAddress();
outputSpAddress = new SilentPaymentAddress(ECKey.fromPublicOnly(changeAddress.getScanKey()), changeAddress.getSpendKey());
outputSpAddress = changeAddress.getSilentPaymentAddress();
outputSpLabel = 0L;
} else {
derivedPublicKeys.put(recipientWallet.getScriptType().getOutputKey(recipientWallet.getPolicyType(), keystore.getPubKey(outputNode)), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
@ -630,7 +631,7 @@ public class PSBT {
WalletNode walletNode = signingNodes.get(psbtInput);
if(walletNode != null && walletNode.getWallet() != null) {
for(Keystore keystore : signingWallet.getKeystores()) {
if(psbtInput.getSilentPaymentsTweak() != null && keystore.getSilentPaymentScanAddress() != null) {
if(psbtInput.getSilentPaymentsTweak() != null && keystore.getSilentPaymentScanAddress() != null && signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
KeyDerivation spendKeyDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
psbtInput.getSilentPaymentsSpendDerivations().put(spendPubKey, spendKeyDerivation);

View File

@ -56,6 +56,10 @@ public class SilentPaymentScanAddress extends SilentPaymentAddress {
return new SilentPaymentScanAddress(scanPrivateKey, spendPublicKey);
}
public SilentPaymentAddress getSilentPaymentAddress() {
return new SilentPaymentAddress(ECKey.fromPublicOnly(getScanKey()), getSpendKey());
}
public SilentPaymentScanAddress copy() {
return new SilentPaymentScanAddress(getScanKey(), getSpendKey());
}
@ -68,6 +72,16 @@ public class SilentPaymentScanAddress extends SilentPaymentAddress {
return Utils.concat(getScanKey().getPrivKeyBytes(), getSpendKey().getPubKey(true));
}
public static boolean isValid(String encoded) {
try {
fromKeyString(encoded);
} catch(Exception e) {
return false;
}
return true;
}
public static SilentPaymentScanAddress fromKeyString(String encoded) {
Bech32.Bech32Data data = Bech32.decode(encoded, 1023);
if(data.encoding != Bech32.Encoding.BECH32M) {

View File

@ -204,10 +204,14 @@ public class SilentPaymentUtils {
* @throws InvalidSilentPaymentException if the computed shared secrets or addresses are invalid
*/
public static Map<ECKey, EcdhShareAndProof> computeOutputAddresses(List<SilentPayment> silentPayments, Map<HashIndex, WalletNode> utxos) throws InvalidSilentPaymentException {
ECKey summedPrivateKey = getSummedPrivateKey(utxos.values());
return computeOutputAddresses(silentPayments, summedPrivateKey, utxos.keySet());
}
public static Map<ECKey, EcdhShareAndProof> computeOutputAddresses(List<SilentPayment> silentPayments, ECKey summedPrivateKey, Set<HashIndex> outpoints) throws InvalidSilentPaymentException {
Map<ECKey, EcdhShareAndProof> scanKeyProofs = new LinkedHashMap<>();
SecureRandom random = new SecureRandom();
ECKey summedPrivateKey = getSummedPrivateKey(utxos.values());
BigInteger inputHash = getInputHash(utxos.keySet(), summedPrivateKey);
BigInteger inputHash = getInputHash(outpoints, summedPrivateKey);
Map<ECKey, List<SilentPayment>> scanKeyGroups = getScanKeyGroups(silentPayments);
for(Map.Entry<ECKey, List<SilentPayment>> scanKeyGroup : scanKeyGroups.entrySet()) {
ECKey scanKey = scanKeyGroup.getKey();

View File

@ -37,6 +37,7 @@ public class Keystore extends Persistable {
//Avoid performing repeated expensive seed derivation checks
private transient boolean extendedPublicKeyChecked;
private transient boolean silentPaymentScanAddressChecked;
public Keystore() {
this(DEFAULT_LABEL);
@ -112,6 +113,7 @@ public class Keystore extends Persistable {
public void setSilentPaymentScanAddress(SilentPaymentScanAddress silentPaymentScanAddress) {
this.silentPaymentScanAddress = silentPaymentScanAddress;
this.silentPaymentScanAddressChecked = false;
}
public byte[] getDeviceRegistration() {
@ -227,10 +229,16 @@ public class Keystore extends Persistable {
}
public ECKey getKey(WalletNode walletNode) throws MnemonicException {
if(silentPaymentScanAddress != null && walletNode.getSilentPaymentTweak() != null) {
if(silentPaymentScanAddress != null && walletNode.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPrivKey = getSpendPrivateKey(Collections.emptyMap());
ECKey tweakKey = ECKey.fromPrivate(walletNode.getSilentPaymentTweak());
return spendPrivKey.addPrivate(tweakKey);
byte[] tweak = walletNode.getSilentPaymentTweak();
if(tweak == null) {
if(walletNode.isPurposeNode()) {
return spendPrivKey;
}
throw new IllegalStateException("Silent payment tweak is required for address node " + walletNode.getDerivationPath());
}
return spendPrivKey.addPrivate(ECKey.fromPrivate(tweak));
}
if(source == KeystoreSource.SW_PAYMENT_CODE) {
@ -256,9 +264,16 @@ public class Keystore extends Persistable {
}
public ECKey getPubKey(WalletNode walletNode) {
if(silentPaymentScanAddress != null && walletNode.getSilentPaymentTweak() != null) {
if(silentPaymentScanAddress != null && walletNode.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendKey = silentPaymentScanAddress.getSpendKey();
ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(walletNode.getSilentPaymentTweak()));
byte[] tweak = walletNode.getSilentPaymentTweak();
if(tweak == null) {
if(walletNode.isPurposeNode()) {
return spendKey;
}
throw new IllegalStateException("Silent payment tweak is required for address node " + walletNode.getDerivationPath());
}
ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak));
return spendKey.add(tweakPoint, true);
}
@ -382,7 +397,7 @@ public class Keystore extends Persistable {
throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
}
if(!extendedPublicKeyChecked && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
if(!extendedPublicKeyChecked && extendedPublicKey != null && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
try {
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
@ -396,6 +411,21 @@ public class Keystore extends Persistable {
throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e);
}
}
if(!silentPaymentScanAddressChecked && silentPaymentScanAddress != null && ((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted()))) {
try {
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
DeterministicKey derivedScanKey = getExtendedMasterPrivateKey().getKey(KeyDerivation.getBip352ScanDerivation(derivation));
DeterministicKey derivedSpendKey = getExtendedMasterPrivateKey().getKey(KeyDerivation.getBip352SpendDerivation(derivation));
SilentPaymentScanAddress derivedScanAddress = new SilentPaymentScanAddress(derivedScanKey, derivedSpendKey);
if(!derivedScanAddress.equals(getSilentPaymentScanAddress())) {
throw new InvalidKeystoreException("Specified silent payments scan address does not match scan and spend keys derived from seed");
}
silentPaymentScanAddressChecked = true;
} catch(MnemonicException e) {
throw new InvalidKeystoreException("Invalid mnemonic specified for seed", e);
}
}
}
if(source == KeystoreSource.SW_PAYMENT_CODE) {

View File

@ -150,12 +150,14 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Keystore derivedKeystore = keystore.hasSeed() ? Keystore.fromSeed(keystore.getSeed(), childDerivation) : Keystore.fromMasterPrivateExtendedKey(keystore.getMasterPrivateExtendedKey(), childDerivation);
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
} catch(Exception e) {
throw new IllegalStateException("Cannot derive keystore for " + standardAccount + " account", e);
}
} else {
keystore.setKeyDerivation(new KeyDerivation(null, KeyDerivation.writePath(childDerivation)));
keystore.setExtendedPublicKey(null);
keystore.setSilentPaymentScanAddress(null);
}
}
@ -552,7 +554,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
}
public int getGapLimit() {
return gapLimit == null ? DEFAULT_LOOKAHEAD : gapLimit;
return policyType == PolicyType.SINGLE_SP ? 0 : (gapLimit == null ? DEFAULT_LOOKAHEAD : gapLimit);
}
public void gapLimit(Integer gapLimit) {
@ -1732,7 +1734,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
PSBTInput psbtInput = signingEntry.getKey();
if(!psbtInput.isSigned()) {
if(psbtInput.getSilentPaymentsTweak() != null && keystore.getSilentPaymentScanAddress() != null) {
if(psbtInput.getSilentPaymentsTweak() != null && keystore.getSilentPaymentScanAddress() != null && signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPrivKey = keystore.getSpendPrivateKey(psbtInput.getSilentPaymentsSpendDerivations());
psbtInput.signSilentPayments(spendPrivKey);
} else {

View File

@ -99,6 +99,10 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
return derivation;
}
public boolean isPurposeNode() {
return getDerivation().size() == 1;
}
public String getLabel() {
return label;
}