retrieve, store and use device registrations to avoid unncessary reregistration on ledger multisig wallets

This commit is contained in:
Craig Raw 2025-01-22 16:26:42 +02:00
parent 95200c7143
commit ee2f387cd5
10 changed files with 130 additions and 35 deletions

2
drongo

@ -1 +1 @@
Subproject commit 0df1f79e5c7e9fc1daa212c875c9da5dbcc0ee56
Subproject commit f67a2caf5379a6931040f3a9b0c9203e9bfc3f44

View file

@ -16,11 +16,8 @@ import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
@ -778,10 +775,12 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt, OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
@ -821,10 +820,11 @@ public class DevicePane extends TitledDescriptionPane {
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
@ -834,6 +834,26 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressService.start();
}
private byte[] getDeviceRegistration() {
Optional<Keystore> optKeystore = wallet.getKeystores().stream()
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
}
private void updateDeviceRegistrations(Set<byte[]> newDeviceRegistrations) {
if(!newDeviceRegistrations.isEmpty()) {
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores();
if(!registrationKeystores.isEmpty()) {
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(wallet, registrationKeystores));
}
}
}
private List<Keystore> getDeviceRegistrationKeystores() {
return wallet.getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
}
private void signMessage() {
if(device.isCard()) {
try {

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List;
public class KeystoreDeviceRegistrationsChangedEvent extends WalletChangedEvent {
private final List<Keystore> changedKeystores;
public KeystoreDeviceRegistrationsChangedEvent(Wallet wallet, List<Keystore> changedKeystores) {
super(wallet);
this.changedKeystores = changedKeystores;
}
public List<Keystore> getChangedKeystores() {
return changedKeystores;
}
}

View file

@ -36,6 +36,8 @@ public class Hwi {
private static boolean isPromptActive = false;
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
static {
//deleteHwiDir();
}
@ -161,7 +163,10 @@ public class Hwi {
isPromptActive = true;
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
return lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
String address = lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
newDeviceRegistrations.remove(walletRegistration);
return address;
} catch(DeviceException e) {
throw new DisplayAddressException(e.getMessage(), e);
} catch(RuntimeException e) {
@ -192,7 +197,10 @@ public class Hwi {
try {
isPromptActive = true;
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
return lark.signTransaction(device.getType(), device.getPath(), psbt);
PSBT signed = lark.signTransaction(device.getType(), device.getPath(), psbt);
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
newDeviceRegistrations.remove(walletRegistration);
return signed;
} catch(DeviceException e) {
throw new SignTransactionException(e.getMessage(), e);
} catch(RuntimeException e) {
@ -346,16 +354,7 @@ public class Hwi {
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName) {
this.device = device;
this.passphrase = passphrase;
this.scriptType = scriptType;
this.addressDescriptor = addressDescriptor;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = null;
}
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
@ -367,12 +366,18 @@ public class Hwi {
this.walletRegistration = walletRegistration;
}
public Set<byte[]> getNewDeviceRegistrations() {
return newDeviceRegistrations;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
protected String call() throws DisplayAddressException {
Hwi hwi = new Hwi();
return hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
String address = hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
return address;
}
};
}
@ -453,15 +458,7 @@ public class Hwi {
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName) {
this.device = device;
this.passphrase = passphrase;
this.psbt = psbt;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = null;
}
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
@ -472,12 +469,18 @@ public class Hwi {
this.walletRegistration = walletRegistration;
}
public Set<byte[]> getNewDeviceRegistrations() {
return newDeviceRegistrations;
}
@Override
protected Task<PSBT> createTask() {
return new Task<>() {
protected PSBT call() throws SignTransactionException {
Hwi hwi = new Hwi();
return hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
PSBT signed = hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
return signed;
}
};
}

View file

@ -355,6 +355,13 @@ public class DbPersistence implements Persistence {
}
}
if(!dirtyPersistables.registrationKeystores.isEmpty()) {
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
for(Keystore keystore : dirtyPersistables.registrationKeystores) {
keystoreDao.updateDeviceRegistration(keystore.getDeviceRegistration(), keystore.getId());
}
}
dirtyPersistablesMap.remove(wallet);
} finally {
walletDao.setSchema(DEFAULT_SCHEMA);
@ -792,6 +799,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void keystoreDeviceRegistrationsChanged(KeystoreDeviceRegistrationsChangedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).registrationKeystores.addAll(event.getChangedKeystores()));
}
}
@Subscribe
public void walletWatchLastChanged(WalletWatchLastChangedEvent event) {
if(persistsFor(event.getWallet())) {
@ -815,6 +829,7 @@ public class DbPersistence implements Persistence {
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
public final List<Keystore> registrationKeystores = new ArrayList<>();
public String toString() {
return "Dirty Persistables" +
@ -834,7 +849,8 @@ public class DbPersistence implements Persistence {
"\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList());
}
}
}

View file

@ -13,16 +13,16 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.List;
public interface KeystoreDao {
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, " +
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, keystore.deviceRegistration, " +
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
@RegisterRowMapper(KeystoreMapper.class)
List<Keystore> getForWalletId(Long id);
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, deviceRegistration, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, byte[] deviceRegistration, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
@ -41,6 +41,9 @@ public interface KeystoreDao {
@SqlUpdate("update keystore set label = ? where id = ?")
void updateLabel(String label, long id);
@SqlUpdate("update keystore set deviceRegistration = ? where id = ?")
void updateDeviceRegistration(byte[] deviceRegistration, long id);
default void addKeystores(Wallet wallet) {
for(int i = 0; i < wallet.getKeystores().size(); i++) {
Keystore keystore = wallet.getKeystores().get(i);
@ -73,6 +76,7 @@ public interface KeystoreDao {
keystore.getKeyDerivation().getDerivationPath(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
keystore.getDeviceRegistration(),
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
keystore.setId(id);

View file

@ -25,6 +25,7 @@ public class KeystoreMapper implements RowMapper<Keystore> {
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
keystore.setExternalPaymentCode(rs.getString("keystore.externalPaymentCode") == null ? null : PaymentCode.fromString(rs.getString("keystore.externalPaymentCode")));
keystore.setDeviceRegistration(rs.getBytes("keystore.deviceRegistration"));
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode"));

View file

@ -10,6 +10,7 @@ import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
@ -234,7 +235,10 @@ public class ReceiveController extends WalletFormController implements Initializ
} else {
Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor,
OutputDescriptor.getOutputDescriptor(walletForm.getWallet()), walletForm.getWallet().getFullName());
OutputDescriptor.getOutputDescriptor(walletForm.getWallet()), walletForm.getWallet().getFullName(), getDeviceRegistration(actualDevice));
displayAddressService.setOnSucceeded(successEvent -> {
updateDeviceRegistrations(actualDevice, displayAddressService.getNewDeviceRegistrations());
});
displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> {
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
@ -252,6 +256,26 @@ public class ReceiveController extends WalletFormController implements Initializ
}
}
private byte[] getDeviceRegistration(Device device) {
Optional<Keystore> optKeystore = getWalletForm().getWallet().getKeystores().stream()
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
}
private void updateDeviceRegistrations(Device device, Set<byte[]> newDeviceRegistrations) {
if(!newDeviceRegistrations.isEmpty()) {
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores(device);
if(!registrationKeystores.isEmpty()) {
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(getWalletForm().getWallet(), registrationKeystores));
}
}
}
private List<Keystore> getDeviceRegistrationKeystores(Device device) {
return getWalletForm().getWallet().getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
}
public void clear() {
if(currentEntry != null) {
label.textProperty().unbindBidirectional(currentEntry.labelProperty());

View file

@ -640,6 +640,13 @@ public class WalletForm {
}
}
@Subscribe
public void keystoreDeviceRegistrationsChanged(KeystoreDeviceRegistrationsChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {

View file

@ -0,0 +1 @@
alter table keystore add column deviceRegistration varbinary(32) after externalPaymentCode;