From 4b8a4594b058cefaf014b9252d5b02f933e437bc Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 16 Jul 2026 11:55:07 +0200 Subject: [PATCH] verify scanned or loaded transactions match the originating psbt --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 58 ++++++++++++------- .../event/RequestTransactionOpenEvent.java | 14 ++++- .../transaction/HeadersController.java | 18 ++++-- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/drongo b/drongo index 077d2142..26fa3b09 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc +Subproject commit 26fa3b0977e001d3b7244c7538ca0d74ced2080f diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 534e4bf6..ab15c02b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -614,6 +614,10 @@ public class AppController implements Initializable { } public void openTransactionFromFile(ActionEvent event) { + openTransactionFromFile(event, null); + } + + private void openTransactionFromFile(ActionEvent event, PSBT contextPsbt) { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); @@ -628,19 +632,21 @@ public class AppController implements Initializable { List files = fileChooser.showOpenMultipleDialog(window); if(files != null) { for(File file : files) { - openTransactionFile(file); + openTransactionFile(file, contextPsbt); } } } - private void openTransactionFile(File file) { - for(Tab tab : tabs.getTabs()) { - TabData tabData = (TabData)tab.getUserData(); - if(tabData instanceof TransactionTabData) { - TransactionTabData transactionTabData = (TransactionTabData)tabData; - if(file.equals(transactionTabData.getFile())) { - tabs.getSelectionModel().select(tab); - return; + private void openTransactionFile(File file, PSBT contextPsbt) { + if(contextPsbt == null) { + for(Tab tab : tabs.getTabs()) { + TabData tabData = (TabData)tab.getUserData(); + if(tabData instanceof TransactionTabData) { + TransactionTabData transactionTabData = (TransactionTabData)tabData; + if(file.equals(transactionTabData.getFile())) { + tabs.getSelectionModel().select(tab); + return; + } } } } @@ -651,9 +657,9 @@ public class AppController implements Initializable { String name = file.getName(); if(Utils.isHex(bytes) || Utils.isBase64(bytes)) { - addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim()); + addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim(), contextPsbt); } else { - addTransactionTab(name, file, bytes); + addTransactionTab(name, file, bytes, contextPsbt); } } catch(IOException e) { showErrorDialog("Error opening file", e.getMessage()); @@ -675,7 +681,7 @@ public class AppController implements Initializable { Optional text = dialog.showAndWait(); if(text.isPresent() && !text.get().isEmpty()) { try { - addTransactionTab(null, null, text.get().trim()); + addTransactionTab(null, null, text.get().trim(), null); } catch(PSBTParseException e) { showErrorDialog("Invalid PSBT", e.getMessage()); } catch(TransactionParseException e) { @@ -1091,7 +1097,7 @@ public class AppController implements Initializable { verifyOpened = true; } } else { - openTransactionFile(file); + openTransactionFile(file, null); } } } @@ -1931,25 +1937,35 @@ public class AppController implements Initializable { return Collections.emptyList(); } - private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException { + private void addTransactionTab(String name, File file, String string, PSBT contextPsbt) throws ParseException, PSBTParseException, TransactionParseException { if(Utils.isBase64(string) && !Utils.isHex(string)) { - addTransactionTab(name, file, Base64.getDecoder().decode(string)); + addTransactionTab(name, file, Base64.getDecoder().decode(string), contextPsbt); } else if(Utils.isHex(string)) { - addTransactionTab(name, file, Utils.hexToBytes(string)); + addTransactionTab(name, file, Utils.hexToBytes(string), contextPsbt); } else { throw new ParseException("Input is not base64 or hex", 0); } } - private void addTransactionTab(String name, File file, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException { + private void addTransactionTab(String name, File file, byte[] bytes, PSBT contextPsbt) throws PSBTParseException, ParseException, TransactionParseException { if(PSBT.isPSBT(bytes)) { //Don't verify signatures here - provided PSBT may omit UTXO data that can be found when combining with an existing PSBT PSBT psbt = new PSBT(bytes, false); - addTransactionTab(name, file, psbt); + if(contextPsbt == null || contextPsbt.matches(psbt)) { + addTransactionTab(name, file, psbt); + } else { + AppServices.showErrorDialog("Mismatched Transaction", "The loaded transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device."); + } } else if(Transaction.isTransaction(bytes)) { try { Transaction transaction = new Transaction(bytes); - addTransactionTab(name, file, transaction); + if(contextPsbt == null || contextPsbt.matches(transaction)) { + addTransactionTab(name, file, transaction); + } else if(contextPsbt.possibleUnverifiableSilentPaymentsTransaction(transaction)) { + AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified."); + } else { + AppServices.showErrorDialog("Mismatched Transaction", "The loaded transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device."); + } } catch(Exception e) { throw new TransactionParseException(e.getMessage()); } @@ -3290,9 +3306,9 @@ public class AppController implements Initializable { public void requestTransactionOpen(RequestTransactionOpenEvent event) { if(tabs.getScene().getWindow().equals(event.getWindow())) { if(event.getFile() != null) { - openTransactionFile(event.getFile()); + openTransactionFile(event.getFile(), event.getContextPsbt()); } else { - openTransactionFromFile(null); + openTransactionFromFile(null, event.getContextPsbt()); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RequestTransactionOpenEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/RequestTransactionOpenEvent.java index bb16c0db..57b40384 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/RequestTransactionOpenEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/RequestTransactionOpenEvent.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.event; +import com.sparrowwallet.drongo.psbt.PSBT; import javafx.stage.Window; import java.io.File; @@ -10,15 +11,20 @@ import java.io.File; public class RequestTransactionOpenEvent { private final Window window; private final File file; + private final PSBT contextPsbt; public RequestTransactionOpenEvent(Window window) { - this.window = window; - this.file = null; + this(window, null, null); } public RequestTransactionOpenEvent(Window window, File file) { + this(window, file, null); + } + + public RequestTransactionOpenEvent(Window window, File file, PSBT contextPsbt) { this.window = window; this.file = file; + this.contextPsbt = contextPsbt; } public Window getWindow() { @@ -28,4 +34,8 @@ public class RequestTransactionOpenEvent { public File getFile() { return file; } + + public PSBT getContextPsbt() { + return contextPsbt; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 28c8b83a..77a452f3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -1037,9 +1037,19 @@ public class HeadersController extends TransactionFormController implements Init if(optionalResult.isPresent()) { QRScanDialog.Result result = optionalResult.get(); if(result.transaction != null) { - EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction)); + if(headersForm.getPsbt().matches(result.transaction)) { + EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction)); + } else if(headersForm.getPsbt().possibleUnverifiableSilentPaymentsTransaction(result.transaction)) { + AppServices.showErrorDialog("Silent Payments Transaction", "This transaction pays a silent payment address.\n\nThe signing device must return the PSBT rather than the final transaction, so the silent payment outputs can be verified."); + } else { + AppServices.showErrorDialog("Mismatched Transaction", "The scanned transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device."); + } } else if(result.psbt != null) { - EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt)); + if(headersForm.getPsbt().matches(result.psbt)) { + EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt)); + } else { + AppServices.showErrorDialog("Mismatched Transaction", "The scanned transaction does not match the transaction in this tab.\n\nCheck that the correct transaction was signed and exported from the signing device."); + } } else if(result.seed != null) { signFromSeed(result.seed); } else if(result.exception != null) { @@ -1110,7 +1120,7 @@ public class HeadersController extends TransactionFormController implements Init ToggleButton toggleButton = (ToggleButton)event.getSource(); toggleButton.setSelected(false); - EventManager.get().post(new RequestTransactionOpenEvent(toggleButton.getScene().getWindow())); + EventManager.get().post(new RequestTransactionOpenEvent(toggleButton.getScene().getWindow(), null, headersForm.getPsbt())); } public void signPSBT(ActionEvent event) { @@ -1882,4 +1892,4 @@ public class HeadersController extends TransactionFormController implements Init return wallet.getKeystores().stream().map(keystore -> sourceOrder.indexOf(keystore.getSource())).mapToInt(v -> v).max().orElse(0); } } -} \ No newline at end of file +}