only apply dns payment resolutions matching the currently entered recipient
What changed, and why it matters
This commit fixes a race condition in Sparrow Wallet's payment screen. When a user types a human-readable payment name (like a DNS-based address), every keystroke that forms a valid name triggers a slow background lookup. If the user keeps typing or changes the recipient, an older lookup could finish later and overwrite the current recipient address with the wrong one. The fix checks that the lookup result still matches what the user has actually typed before applying it.
Review the drongo submodule bump (4336bbd469e9bdd91531f55ac0f2bdb3ca07efb4) for related DNS payment changes. Consider adding cancellation of in-flight DnsPaymentService tasks when the HRN changes, and add regression tests covering rapid recipient changes and out-of-order DNS responses. Users should upgrade to a release containing this commit.
Security signals we found
Race condition between asynchronous DNS resolution and user input
Possible recipient-address overwrite by stale lookup result
UI state desynchronization (displayed recipient vs. resolved recipient)
Potential for user to send funds to an unintended address
Missing validation that async result corresponds to current input
Evidence from the diff
PaymentController spawns a DnsPaymentService for each valid HRN (human-readable name) produced by keystrokes in the address field. Because DNS payment resolution is asynchronous and slow, multiple requests can be in flight concurrently. The previous code applied the first successful result unconditionally via setOnSucceeded, and showed validation errors unconditionally via setOnFailed. This created a TOCTOU-style race: a stale resolution for an earlier HRN could overwrite the recipient field or display a misleading error after the user had already typed a different recipient. The patch adds isCurrentHrn(dnsPaymentHrn) guards in both success and failure handlers, ensuring only results matching the currently entered recipient are applied or surfaced. A drongo submodule bump is included but its contents are not supplied.
Changed components
src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.javaDnsPaymentServicedrongo submoduleInspect captured patch +15 / −4
### drongo
@@ -1 +1 @@
-Subproject commit 23e092f556c4ae88576674965d806d49dfc11966
+Subproject commit 4336bbd469e9bdd91531f55ac0f2bdb3ca07efb4
### src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -222,10 +222,15 @@ public void changed(ObservableValue<? extends String> observable, String oldValu
}
DnsPaymentService dnsPaymentService = new DnsPaymentService(dnsPaymentHrn);
- dnsPaymentService.setOnSucceeded(_ -> dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment)));
+ dnsPaymentService.setOnSucceeded(_ -> {
+ if(isCurrentHrn(dnsPaymentHrn)) {
+ dnsPaymentService.getValue().ifPresent(dnsPayment -> setDnsPayment(dnsPayment));
+ }
+ });
dnsPaymentService.setOnFailed(failEvent -> {
- if(failEvent.getSource().getException() != null && !(failEvent.getSource().getException().getCause() instanceof TimeoutException)) {
- AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(failEvent.getSource().getException()).getMessage());
+ Throwable exception = failEvent.getSource().getException();
+ if(isCurrentHrn(dnsPaymentHrn) && exception != null && !(exception.getCause() instanceof TimeoutException)) {
+ AppServices.showErrorDialog("Validation failed for " + dnsPaymentHrn, Throwables.getRootCause(exception).getMessage());
}
});
dnsPaymentService.start();
@@ -449,6 +454,12 @@ public void setPayNym(PayNym payNym) {
}
}
+ //Resolution is slow enough that several may be in flight at once, since every keystroke forming a valid hrn starts one.
+ //Only the hrn the address field currently holds may be applied - an earlier one landing later must not replace the recipient.
+ private boolean isCurrentHrn(String hrn) {
+ return DnsPayment.getHrn(address.getText()).filter(hrn::equals).isPresent();
+ }
+
public void setDnsPayment(DnsPayment dnsPayment) {
if(dnsPayment.hasAddress()) {
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);Why this scored 59/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.