persist silent payment address mappings for safe rbf of sp-sending transactions

This commit is contained in:
Craig Raw
2026-05-18 12:41:56 +02:00
parent 273d2aacf3
commit e985a03a58
10 changed files with 161 additions and 3 deletions
+1 -1
Submodule drongo updated: 7df781c77c...698f8b08a1
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletSilentPaymentAddressesChangedEvent extends WalletChangedEvent {
public WalletSilentPaymentAddressesChangedEvent(Wallet wallet) {
super(wallet.resolveMasterWallet());
}
}
@@ -337,6 +337,11 @@ public class DbPersistence implements Persistence {
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
}
if(dirtyPersistables.silentPaymentAddresses) {
SilentPaymentAddressDao silentPaymentAddressDao = handle.attach(SilentPaymentAddressDao.class);
silentPaymentAddressDao.clearAndAddAll(wallet);
}
if(dirtyPersistables.walletTable != null) {
WalletTableDao walletTableDao = handle.attach(WalletTableDao.class);
walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable);
@@ -888,6 +893,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).silentPaymentAddresses = true);
}
}
private static class DirtyPersistables {
public boolean deleteAccount;
public boolean clearHistory;
@@ -906,6 +918,7 @@ public class DbPersistence implements Persistence {
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
public final List<Keystore> registrationKeystores = new ArrayList<>();
public boolean silentPaymentAddresses;
public String toString() {
return "Dirty Persistables" +
@@ -927,7 +940,8 @@ public class DbPersistence implements Persistence {
"\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 registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList());
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()) +
"\nSilent payment addresses:" + silentPaymentAddresses;
}
}
}
@@ -0,0 +1,40 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public interface SilentPaymentAddressDao {
@SqlQuery("select address, silentPaymentAddress from silentPaymentAddress")
@RegisterRowMapper(SilentPaymentAddressMapper.class)
Map<Address, SilentPaymentAddress> getAll();
@SqlBatch("insert into silentPaymentAddress (address, silentPaymentAddress) values (?, ?)")
void insertSilentPaymentAddresses(List<byte[]> addresses, List<byte[]> silentPaymentAddresses);
@SqlUpdate("delete from silentPaymentAddress")
void clear();
default void clearAndAddAll(Wallet wallet) {
clear();
List<byte[]> addresses = new ArrayList<>();
List<byte[]> silentPaymentAddresses = new ArrayList<>();
for(Map.Entry<Address, SilentPaymentAddress> entry : wallet.getSilentPaymentAddresses().entrySet()) {
addresses.add(entry.getKey().getData());
silentPaymentAddresses.add(entry.getValue().serialize());
}
if(!addresses.isEmpty()) {
insertSilentPaymentAddresses(addresses, silentPaymentAddresses);
}
}
}
@@ -0,0 +1,36 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2TRAddress;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class SilentPaymentAddressMapper implements RowMapper<Map.Entry<Address, SilentPaymentAddress>> {
@Override
public Map.Entry<Address, SilentPaymentAddress> map(ResultSet rs, StatementContext ctx) throws SQLException {
Address address = new P2TRAddress(rs.getBytes("address"));
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.fromBytes(rs.getBytes("silentPaymentAddress"));
return new Map.Entry<>() {
@Override
public Address getKey() {
return address;
}
@Override
public SilentPaymentAddress getValue() {
return silentPaymentAddress;
}
@Override
public SilentPaymentAddress setValue(SilentPaymentAddress value) {
return null;
}
};
}
}
@@ -30,6 +30,9 @@ public interface WalletDao {
@CreateSqlObject
DetachedLabelDao createDetachedLabelDao();
@CreateSqlObject
SilentPaymentAddressDao createSilentPaymentAddressDao();
@CreateSqlObject
WalletConfigDao createWalletConfigDao();
@@ -121,6 +124,8 @@ public interface WalletDao {
Map<String, String> detachedLabels = createDetachedLabelDao().getAll();
wallet.getDetachedLabels().putAll(detachedLabels);
wallet.getSilentPaymentAddresses().putAll(createSilentPaymentAddressDao().getAll());
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
Map<TableType, WalletTable> walletTables = createWalletTableDao().getForWalletId(wallet.getId());
@@ -144,6 +149,7 @@ public interface WalletDao {
createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet);
createDetachedLabelDao().clearAndAddAll(wallet);
createSilentPaymentAddressDao().clearAndAddAll(wallet);
createWalletConfigDao().addWalletConfig(wallet);
createWalletTableDao().addWalletTables(wallet);
createMixConfigDao().addMixConfig(wallet);
@@ -578,6 +578,8 @@ public class HeadersController extends TransactionFormController implements Init
int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired();
signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold);
learnSilentPaymentAddresses(signingWallet, headersForm.getPsbt());
});
blockchainForm.setDynamicUpdate(this);
@@ -1156,6 +1158,11 @@ public class HeadersController extends TransactionFormController implements Init
Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
if(!silentPayments.isEmpty()) {
Wallet signingWallet = headersForm.getSigningWallet();
for(SilentPayment silentPayment : silentPayments) {
signingWallet.addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress());
}
EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(signingWallet));
EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
}
unencryptedWallet.sign(signingNodes);
@@ -1443,6 +1450,37 @@ public class HeadersController extends TransactionFormController implements Init
requestPayjoinPSBTService.start();
}
private void learnSilentPaymentAddresses(Wallet wallet, PSBT psbt) {
if(wallet == null || psbt == null) {
return;
}
Map<Address, SilentPaymentAddress> pending = new LinkedHashMap<>();
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
SilentPaymentAddress spAddress = psbtOutput.getSilentPaymentAddress();
if(spAddress != null) {
Script script = psbtOutput.getScript();
Address address = script == null ? null : script.getToAddress();
if(address == null) {
return;
}
pending.put(address, spAddress);
}
}
boolean changed = false;
for(Map.Entry<Address, SilentPaymentAddress> entry : pending.entrySet()) {
if(!entry.getValue().equals(wallet.getSilentPaymentAddress(entry.getKey()))) {
wallet.addSilentPaymentAddress(entry.getKey(), entry.getValue());
changed = true;
}
}
if(changed) {
EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(wallet));
}
}
@Override
public void update() {
BlockTransaction blockTransaction = headersForm.getBlockTransaction();
@@ -1664,6 +1702,7 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe
public void psbtCombined(PSBTCombinedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt());
if(headersForm.getSigningWallet() != null) {
updateSignedKeystores(headersForm.getSigningWallet());
} else if(headersForm.getPsbt().isSigned()) {
@@ -1678,6 +1717,7 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe
public void psbtFinalized(PSBTFinalizedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt());
if(headersForm.getSigningWallet() != null) {
updateSignedKeystores(headersForm.getSigningWallet());
}
@@ -1559,6 +1559,11 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void excludeUtxo(ExcludeUtxoEvent event) {
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
BlockTransaction replacedTransaction = replacedTransactionProperty.get();
if(replacedTransaction != null && !getWalletForm().getWallet().isSafeToAddInputsOrOutputs(replacedTransaction)) {
AppServices.showErrorDialog("Cannot Exclude Input", "Removing an input from this replacement transaction could break silent payment outputs as the original output script depends on the input set.");
return;
}
UtxoSelector utxoSelector = utxoSelectorProperty.get();
if(utxoSelector instanceof MaxUtxoSelector) {
Collection<BlockTransactionHashIndex> utxos = event.getWalletTransaction().getSelectedUtxos().keySet();
@@ -749,6 +749,13 @@ public class WalletForm {
}
}
@Subscribe
public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
@@ -1,3 +1,4 @@
alter table wallet add column birthHeight integer after birthDate;
alter table walletNode add column silentPaymentTweak varbinary(32) after addressData;
alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode;
alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode;
create table silentPaymentAddress (address varbinary(32) primary key not null, silentPaymentAddress varbinary(67) not null);