report a transaction tab height the server will not prove instead of marking it unverified until it does
What changed, and why it matters
This commit changes how Sparrow Wallet shows a transaction's block height when the connected server cannot prove the transaction is actually in that block. Previously, the transaction tab would label such a height as 'Unverified' while waiting for proof. Now it shows the height plainly, and only after the server has had a fair chance to prove it and fails does it add an 'Unverified' warning. The change also makes the transaction-tab verification use the same retry logic and user-facing reporting that wallet history verification already uses, so a momentary server hiccup is not treated as a server lie.
Reviewers should confirm that verifyMerkleProofs(null, ...) safely handles a null wallet (no wallet-specific state is dereferenced), that the new unverifiedWarning is correctly hidden on reorg/disconnect, and that the retry budget is not exhausted by a server that is merely slow before the user is warned. No immediate security patch appears required; this is a defensive hardening change.
Security signals we found
UI no longer marks a transaction as unverified merely because a proof is still pending
Transaction-tab proof requests now share retry logic with wallet-history proof requests
Server refusals/failures outside any wallet are reported to the user with the same event mechanism as wallet transactions
A server that does not implement the proof RPC disables verification for the session rather than marking individual transactions unverified
Reorg and disconnect events clear the unproven-pair state so stale warnings are not shown against a new chain or connection
Evidence from the diff
The patch unifies transaction-merkle-proof verification for the transaction tab with the existing wallet-history path. It routes getProvenHeader() through verifyMerkleProofs(null, …) so it gets the same retry budget and event posting (TransactionProofsRefusedEvent / TransactionProofsFailedEvent). A new verificationUnprovenPair field in HeadersController records only the txid:height the current server actually answered without proving, and a new unverifiedWarning label is shown only in that case, replacing the previous ‘(Unverified)’ suffix on blockStatus. The UI now distinguishes ‘still asking’ from ‘server refused/failed to prove’, and events for transactions outside any wallet carry a null wallet with no ‘Refresh Wallet’ option.
Changed components
ElectrumServer.getProvenHeader()ElectrumServer.verifyMerkleProofs() / postProofEvents()HeadersController transaction tab UITransactionProofsEvent / TransactionProofsFailedEvent / TransactionProofsRefusedEventAppServices proof-dialog handlingheaders.fxml / headers.css UI labelsInspect captured patch +158 / −63
### src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -1547,12 +1547,18 @@ public void transactionProofsRefused(TransactionProofsRefusedEvent event) {
private void showProofsDialog(TransactionProofsEvent event, String title, String content) {
Platform.runLater(() -> {
- ButtonType refreshButton = new ButtonType("Refresh Wallet", ButtonBar.ButtonData.OK_DONE);
- Optional<ButtonType> optType = showErrorDialog(title, content + (event.getReferences().size() == 1 ? " It is" : " They are")
- + " shown as unconfirmed until verified.\n\nConsider switching servers, and refreshing the wallet afterwards.",
- ButtonType.CANCEL, refreshButton);
- if(optType.isPresent() && optType.get() == refreshButton) {
- EventManager.get().post(new RequestWalletRefreshEvent(event.getWallet()));
+ if(event.getWallet() == null) {
+ //Reached outside any wallet, so there is no history holding it and nothing to refresh: it is shown at the height the server reported,
+ //marked as unproven, and switching servers is the only thing that puts the question to anyone else
+ showErrorDialog(title, content + " It is shown at that height marked unverified.\n\nConsider switching servers.");
+ } else {
+ ButtonType refreshButton = new ButtonType("Refresh Wallet", ButtonBar.ButtonData.OK_DONE);
+ Optional<ButtonType> optType = showErrorDialog(title, content + (event.getReferences().size() == 1 ? " It is" : " They are")
+ + " shown as unconfirmed until verified.\n\nConsider switching servers, and refreshing the wallet afterwards.",
+ ButtonType.CANCEL, refreshButton);
+ if(optType.isPresent() && optType.get() == refreshButton) {
+ EventManager.get().post(new RequestWalletRefreshEvent(event.getWallet()));
+ }
}
});
}
### src/main/java/com/sparrowwallet/sparrow/event/TransactionProofsEvent.java
@@ -6,8 +6,8 @@
import java.util.Set;
/**
- * The transactions of one wallet whose confirmed heights the connected server did not prove in a single history pass, aggregated so that a pass
- * surfacing many of them raises one dialog rather than one per transaction.
+ * The transactions whose confirmed heights the connected server did not prove, aggregated per wallet so that a history pass surfacing many of them
+ * raises one dialog rather than one per transaction. A transaction reached outside any wallet is raised on its own, under a null wallet.
*/
public abstract class TransactionProofsEvent {
private final Wallet wallet;
@@ -18,12 +18,16 @@ public TransactionProofsEvent(Wallet wallet, Set<BlockTransactionHash> reference
this.references = references;
}
+ /**
+ * The wallet whose history holds these transactions, or null for one reached outside any wallet, where there is no history to refresh.
+ */
public Wallet getWallet() {
return wallet;
}
/**
- * The transactions with the heights the server reported them at, which are no longer the heights they are held at.
+ * The transactions with the heights the server reported them at. A wallet no longer holds them at those heights, having demoted them; one reached
+ * outside a wallet is still shown at its, marked as unproven.
*/
public Set<BlockTransactionHash> getReferences() {
return references;
### src/main/java/com/sparrowwallet/sparrow/event/TransactionProofsFailedEvent.java
@@ -6,10 +6,11 @@
import java.util.Set;
/**
- * Posted once per wallet history pass where the server supplied a proof that did not reconstruct the merkle root of the verified header at the height
- * it reported - the server proven wrong rather than merely unhelpful. The transactions carry the reported heights, and are already written unconfirmed.
+ * Posted where the server supplied a proof that did not reconstruct the merkle root of the verified header at the height it reported - the server
+ * proven wrong rather than merely unhelpful. Once per wallet history pass, or once for a transaction the transaction tab asked about. The transactions
+ * carry the reported heights, and what a wallet holds is already written unconfirmed.
* <p>
- * Dispatched on the wallet history thread, so a handler must hop to the application thread itself.
+ * Dispatched on the thread that asked for the proof, so a handler must hop to the application thread itself.
*/
public class TransactionProofsFailedEvent extends TransactionProofsEvent {
public TransactionProofsFailedEvent(Wallet wallet, Set<BlockTransactionHash> references) {
### src/main/java/com/sparrowwallet/sparrow/event/TransactionProofsRefusedEvent.java
@@ -6,11 +6,11 @@
import java.util.Set;
/**
- * Posted once per wallet history pass where the server reported a transaction as confirmed and then would not substantiate it at that height, by
- * erroring, by answering for another block, or by leaving the header unverifiable. Nothing has been shown false, so this is the server contradicting
- * itself rather than lying. The transactions are already written unconfirmed.
+ * Posted where the server reported a transaction as confirmed and then would not substantiate it at that height, by erroring, by answering for
+ * another block, or by leaving the header unverifiable. Nothing has been shown false, so this is the server contradicting itself rather than lying.
+ * Once per wallet history pass, or once for a transaction the transaction tab asked about. What a wallet holds is already written unconfirmed.
* <p>
- * Dispatched on the wallet history thread, so a handler must hop to the application thread itself.
+ * Dispatched on the thread that asked for the proof, so a handler must hop to the application thread itself.
*/
public class TransactionProofsRefusedEvent extends TransactionProofsEvent {
public TransactionProofsRefusedEvent(Wallet wallet, Set<BlockTransactionHash> references) {
### src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
@@ -1186,10 +1186,11 @@ private Map<BlockTransactionHash, BlockHeader> verifyMerkleProofs(Wallet wallet,
/**
* The header the given transaction is proven to be included in at the given height, or null where the connected server did not substantiate it,
* whether by declining the proof, answering for another block, or supplying a branch that does not reconstruct. For a transaction reached outside
- * a wallet, where there is no history to demote and nothing to report per wallet: the caller has only the header to show, or its absence.
+ * a wallet, where there is no history to demote: the caller has only the header to show, or its absence.
* <p>
- * A server that does not implement the call at all disables verification for the session here as it does on the wallet paths, since a server
- * lacking it substantiates nothing and marking every transaction against it unverified says something about the server that is not true.
+ * What the server would not substantiate is raised with the user on the terms a wallet's is, a server refusing to stand behind a height it
+ * reported saying the same thing whether or not a wallet happens to hold the transaction. A server that does not implement the call at all has
+ * refused nothing, and disables verification for the session here as it does on the wallet paths.
*/
public BlockHeader getProvenHeader(Sha256Hash txid, int height) throws ServerException {
String pair = txid + ":" + height;
@@ -1200,36 +1201,37 @@ public BlockHeader getProvenHeader(Sha256Hash txid, int height) throws ServerExc
BlockTransactionHash reference = new BlockTransaction(txid, height, null, null, null);
int reorgsBefore = reorgCount;
+ BlockHeader header;
try {
- Map<String, TransactionMerkleProof> proofs = electrumServerRpc.getTransactionMerkleProofs(getTransport(), null, List.of(reference));
- TransactionMerkleProof proof = proofs.get(pair);
- if(proof == null || proof == TransactionMerkleProof.ERROR_PROOF || proof.block_height != height) {
- return null;
- }
-
- BlockHeader header = getVerifiedHeader(height);
- if(header == null || !verifyProof(txid, proof, header)) {
- return null; //only what was proven is cached: a server that will not prove it now is not the server that will be asked next
- }
-
- //Remembered only where no reorg intervened, under the lock reconcile holds while it clears: a proof resolving across one would otherwise
- //be written behind that clear, leaving an entry proven against a header the chain no longer holds. The proof itself still stands or falls
- //on the header it reconstructed, so it is returned either way
- synchronized(headerSyncLock) {
- if(reorgCount == reorgsBefore) {
- provenTransactionHeaders.put(pair, header);
- }
- }
-
- return header;
+ //The wallet paths' own verification, so that the retries telling a server momentarily unable to answer from one that cannot substantiate
+ //the height are spent here too: without them a single refused call would report an overloaded server as contradicting itself
+ header = verifyMerkleProofs(null, Set.of(reference)).get(reference);
} catch(UnsupportedMethodException e) {
//Before the catch below, as elsewhere: a property of the server rather than of this transaction, so it is settled for the session on the
//same terms the wallet paths settle it, and raised rather than recorded where verification is mandatory
disableVerification(e);
return null;
- } catch(ElectrumServerRpcException e) {
- throw new ServerException(e.getMessage(), e.getCause()); //the server said nothing about this transaction, so it is a failed call
+ } catch(ProofsUnavailableException e) {
+ disableVerification(e); //never answered at all, which is the server unable to serve the call rather than unwilling to prove this
+ return null;
+ } finally {
+ postProofEvents(null);
}
+
+ if(header == null) {
+ return null; //only what was proven is cached: a server that will not prove it now is not the server that will be asked next
+ }
+
+ //Remembered only where no reorg intervened, under the lock reconcile holds while it clears: a proof resolving across one would otherwise
+ //be written behind that clear, leaving an entry proven against a header the chain no longer holds. The proof itself still stands or falls
+ //on the header it reconstructed, so it is returned either way
+ synchronized(headerSyncLock) {
+ if(reorgCount == reorgsBefore) {
+ provenTransactionHeaders.put(pair, header);
+ }
+ }
+
+ return header;
}
/**
@@ -1337,8 +1339,9 @@ void prefetchVerifiedHeaders(Collection<Integer> heights) throws ServerException
}
/**
- * Raises one dialog per wallet for what this task could not prove, from a finally so that a later failure in the pass cannot bury a finding whose
- * demotion has already been written. Each pair is reported once per session, since the passes that follow a refusal re-attempt it.
+ * Raises one dialog for what this task could not prove, per wallet or, for a transaction reached outside any wallet, under a null one. Called from
+ * a finally so that a later failure in the pass cannot bury a finding whose demotion has already been written. Each pair is reported once per
+ * session, since the passes that follow a refusal re-attempt it.
*/
void postProofEvents(Wallet wallet) {
postProofEvents(wallet, wallet);
### src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -93,6 +93,10 @@ public class HeadersController extends TransactionFormController implements Init
//The txid:height last asked about, so that a redraw does not ask again, and the request that asked it
private String verificationRequestedPair;
+ //The txid:height the connected server answered without proving. Only an answer marks the form, so the wait for one is not itself an accusation,
+ //and only this connection's: what one server would not prove is not something the next has been asked, still less refused
+ private String verificationUnprovenPair;
+
private ElectrumServer.TransactionVerificationService verificationService;
@FXML
@@ -176,6 +180,9 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private Label blockStatus;
+ @FXML
+ private Label unverifiedWarning;
+
@FXML
private Field blockHeightField;
@@ -354,6 +361,8 @@ private void initializeView() {
futureBlockWarning.setVisible(false);
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
futureDateWarning.setVisible(false);
+ unverifiedWarning.managedProperty().bind(unverifiedWarning.visibleProperty());
+ unverifiedWarning.setVisible(false);
locktimeNone.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
@@ -841,26 +850,28 @@ private void updateBlockchainForm(BlockTransaction reportedTransaction, Integer
//the wallet's own transaction and the one an input fetch reports are both handed over without what a proof has since established
BlockTransaction blockTransaction = ElectrumServer.getProvenTransaction(reportedTransaction);
- //A block hash is recorded only where the transaction was proven to be in that block, so a confirmed height without one is the server's word
- //alone. Asked of what is shown rather than of the wallets: a height a wallet refused and demoted is fetched from the server again, and the
- //server's answer is what reaches this form
- boolean unverified = blockTransaction.getHeight() > 0 && !ElectrumServer.isProven(blockTransaction) && ElectrumServer.isVerifyingTransactions();
- String unverifiedSuffix = unverified ? " (Unverified)" : "";
- blockStatus.setTooltip(unverified ? new Tooltip("The server reported this height but has not proven the transaction was included in that block") : null);
+ //A block hash is recorded only where the transaction was proven to be in that block, and a wallet's own transaction normally arrives carrying
+ //one. What is left is a height this tab took from the server: a transaction no wallet holds, and one a wallet refused and demoted, which is
+ //fetched from the server again and reaches this form as its answer rather than as the wallet's
+ boolean unproven = blockTransaction.getHeight() > 0 && !ElectrumServer.isProven(blockTransaction) && ElectrumServer.isVerifyingTransactions();
+
+ //Marked where the server was asked and did not prove it, and not while it is still being asked: a proof is the ordinary outcome, so marking
+ //the wait for one would put a warning on almost every transaction opened and leave the user nothing to read in the one case it means something
+ unverifiedWarning.setVisible(unproven && (blockTransaction.getHashAsString() + ":" + blockTransaction.getHeight()).equals(verificationUnprovenPair));
if(Sha256Hash.ZERO_HASH.equals(blockTransaction.getBlockHash()) && blockTransaction.getHeight() == 0 && headersForm.getPsbt() == null) {
//A zero block hash indicates that this blocktransaction is incomplete and the height is likely incorrect if we are not sending a tx
blockStatus.setText("Unknown");
} else if(currentHeight == null) {
- blockStatus.setText(blockTransaction.getHeight() > 0 ? "Confirmed" + unverifiedSuffix : "Unconfirmed");
+ blockStatus.setText(blockTransaction.getHeight() > 0 ? "Confirmed" : "Unconfirmed");
} else {
int confirmations = blockTransaction.getHeight() > 0 ? currentHeight - blockTransaction.getHeight() + 1 : 0;
if(confirmations == 0) {
blockStatus.setText("Unconfirmed");
} else if(confirmations == 1) {
- blockStatus.setText(confirmations + " Confirmation" + unverifiedSuffix);
+ blockStatus.setText(confirmations + " Confirmation");
} else {
- blockStatus.setText(confirmations + " Confirmations" + unverifiedSuffix);
+ blockStatus.setText(confirmations + " Confirmations");
}
if(confirmations <= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
@@ -911,20 +922,20 @@ private void updateBlockchainForm(BlockTransaction reportedTransaction, Integer
signedByField.setVisible(false);
}
- if(unverified) {
+ if(unproven) {
verifyBlockTransaction(blockTransaction);
}
}
/**
* Asks the server to prove the height it reported for a transaction that did not arrive proven, whether or not a wallet holds it: what a wallet
* proved is carried on the transaction it holds, and a height fetched from the server again is the server's however familiar the txid. Started
- * from the form rather than from the fetch so that the transaction is shown while this runs, and shown unverified until it returns a header: a
- * server that declines the proof leaves the claim standing as its own.
+ * from the form rather than from the fetch so that the transaction is shown while this runs, the height standing unqualified until the server has
+ * had its chance to substantiate it. ElectrumServer raises the dialog for a server that will not; the form marks what the dialog was raised about.
* <p>
* The pair records what this server has answered, so only an answer records it. Asked while offline, or cut off partway, nothing has been
* answered and the next redraw asks again - the capability that decides whether to ask at all is settled at connect and outlives the connection,
- * so without this an offline redraw would leave the tab reading unverified on a question no server was ever put.
+ * so without this an offline redraw would settle the pair on a question no server was ever put, and no later redraw would ask it.
*/
private void verifyBlockTransaction(BlockTransaction blockTransaction) {
String pair = blockTransaction.getHashAsString() + ":" + blockTransaction.getHeight();
@@ -945,6 +956,10 @@ private void verifyBlockTransaction(BlockTransaction blockTransaction) {
//was verified against rather than looked up, so what is shown does not rest on the proof having been remembered
if(provenHeader != null) {
headersForm.setBlockTransaction(ElectrumServer.getProvenTransaction(blockTransaction, provenHeader));
+ } else if(ElectrumServer.isVerifyingTransactions()) {
+ //The server had its retries and did not substantiate the height, which is now the tab's to show. Asked of the capability rather
+ //than of the answer, since a server turning out not to implement the call at all returns the same nothing and has refused nothing
+ verificationUnprovenPair = pair;
}
//Redrawn whatever the answer, since a server turning out not to implement the call turns verification off, and the form would else be
@@ -1944,8 +1959,10 @@ public void chainReorg(ChainReorgEvent event) {
BlockTransaction reorganised = new BlockTransaction(blockTransaction.getHash(), blockTransaction.getHeight(), null,
blockTransaction.getFee(), blockTransaction.getTransaction(), null);
headersForm.setBlockTransaction(reorganised);
- //Both dropped, so that the proof is asked again and an answer already on its way is not applied to a chain it was not asked of
+ //All dropped, so that the proof is asked again, an answer already on its way is not applied to a chain it was not asked of, and a
+ //refusal to prove the block that was there is not shown against the block that replaced it
verificationRequestedPair = null;
+ verificationUnprovenPair = null;
verificationService = null;
updateBlockchainForm(reorganised, AppServices.getCurrentBlockHeight());
}
@@ -1963,6 +1980,7 @@ public void connection(ConnectionEvent event) {
@Subscribe
public void disconnection(DisconnectionEvent event) {
verificationRequestedPair = null;
+ verificationUnprovenPair = null;
broadcastProgressBar.setDisable(true);
if(broadcastProgressBar.getProgress() < 0) {
broadcastProgressBar.setProgress(0);
### src/main/resources/com/sparrowwallet/sparrow/transaction/headers.css
@@ -108,6 +108,15 @@
-fx-padding: 0 0 0 12;
}
+#unverifiedWarning {
+ -fx-text-fill: rgb(202, 18, 67);
+ -fx-padding: 0 0 0 12;
+}
+
+#unverifiedWarning .glyph-font {
+ -fx-text-fill: rgb(202, 18, 67);
+}
+
.unfinalized-txid, .edited > .text-field {
-fx-text-fill: #a0a1a7;
}
### src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml
@@ -189,6 +189,14 @@
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
<Field text="Status:">
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
+ <Label fx:id="unverifiedWarning" text="Unverified" contentDisplay="LEFT" graphicTextGap="5">
+ <graphic>
+ <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" />
+ </graphic>
+ <tooltip>
+ <Tooltip text="The connected server reported this height and would not prove the transaction was included in that block"/>
+ </tooltip>
+ </Label>
</Field>
<Field fx:id="blockHeightField" text="Block Height:">
<CopyableLabel fx:id="blockHeight" />
### src/test/java/com/sparrowwallet/sparrow/net/TransactionProofTest.java
@@ -208,8 +208,9 @@ public void provesATransactionOutsideAWallet() throws Exception {
}
/**
- * The three ways the answer is no, each of which leaves the tab showing the height as the server's word alone: a proof declined, one answered for
- * a different block than the one asked about, and one whose branch does not reconstruct.
+ * The three ways the answer is no, each of which leaves the tab showing the height as the server's word alone, and each raised with the user on the
+ * terms a wallet's would be: a proof declined, one answered for a different block than the one asked about, and one whose branch does not
+ * reconstruct. The pair is the same throughout, so the second refusal says nothing the first did not; being proven wrong is a different claim.
*/
@Test
public void doesNotProveATransactionTheServerWillNotSubstantiate() throws Exception {
@@ -218,17 +219,54 @@ public void doesNotProveATransactionTheServerWillNotSubstantiate() throws Except
//Declined: nothing served for this pair at all
assertNull(electrumServer.getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
+ assertEquals(1, listener.getRefusedEvents());
+ assertEquals(Set.of(reference(transaction, PROVEN_HEIGHT)), listener.getRefused());
+ assertNull(listener.getReportedWallet()); //reached outside any wallet, so there is no history to demote or to offer to refresh
//Answered for another block, which substantiates nothing about the height asked for
TransactionMerkleProof otherBlock = server.serveProof(transaction, PROVEN_HEIGHT, 0);
otherBlock.block_height = PROVEN_HEIGHT + 1;
assertNull(electrumServer.getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
+ assertEquals(1, listener.getRefusedEvents());
//Tampered: the branch does not reconstruct the merkle root of the header at that height
TransactionMerkleProof tampered = server.serveProof(transaction, PROVEN_HEIGHT, 0);
tampered.merkle.set(0, Sha256Hash.ZERO_HASH.toString());
assertNull(electrumServer.getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
+ assertEquals(1, listener.getFailedEvents());
+ assertEquals(Set.of(reference(transaction, PROVEN_HEIGHT)), listener.getFailed());
+ }
+
+ /**
+ * A server that refuses once and proves on a retry is one momentarily unable to answer rather than one contradicting itself. The tab spends the
+ * retry budget a wallet pass spends, so what recovers within it is never put to the user at all.
+ */
+ @Test
+ public void provesATransactionOutsideAWalletAfterARetry() throws Exception {
+ Transaction transaction = blockTransactions.getFirst();
+ server.serveProof(transaction, PROVEN_HEIGHT, 0);
+ server.refuseFirstAttempts(transaction, PROVEN_HEIGHT, 1);
+
+ assertNotNull(new ElectrumServer().getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
+
+ assertEquals(2, server.getProofRequests());
+ assertTrue(listener.isEmpty());
+ }
+
+ /**
+ * A server that never answers the call has refused nothing, so the session goes unverified rather than the transaction being reported against it.
+ * The tab then has no grounds to mark anything, verification being off for every transaction it shows.
+ */
+ @Test
+ public void doesNotReportATransactionAgainstAServerThatCannotSupplyProofs() throws Exception {
+ Transaction transaction = blockTransactions.getFirst();
+ server.serveProof(transaction, PROVEN_HEIGHT, 0);
+ server.setProofFailure(new ElectrumServerRpcException("Batch too large"));
+ assertNull(new ElectrumServer().getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
+
+ assertFalse(ElectrumServer.isVerifyingTransactions());
+ assertEquals(ElectrumServer.proofAttempts, server.getProofRequests()); //only after the retries are spent
assertTrue(listener.isEmpty());
}
@@ -290,19 +328,19 @@ public void carriesAProofOntoATransactionBuiltWithoutIt() throws Exception {
/**
* Only what was proven is remembered. A server that would not prove it is not necessarily the server that will be asked next, and caching the
- * refusal would outlast the connection that earned it.
+ * refusal would outlast the connection that earned it. Refused for a whole budget here, since anything less is recovered from within one call.
*/
@Test
public void doesNotRememberARefusal() throws Exception {
Transaction transaction = blockTransactions.getFirst();
server.serveProof(transaction, PROVEN_HEIGHT, 0);
- server.refuseFirstAttempts(transaction, PROVEN_HEIGHT, 1);
+ server.refuseFirstAttempts(transaction, PROVEN_HEIGHT, ElectrumServer.proofAttempts);
ElectrumServer electrumServer = new ElectrumServer();
assertNull(electrumServer.getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
assertNotNull(electrumServer.getProvenHeader(transaction.getTxId(), PROVEN_HEIGHT));
- assertEquals(2, server.getProofRequests());
+ assertEquals(ElectrumServer.proofAttempts + 1, server.getProofRequests());
}
/**
@@ -1418,21 +1456,28 @@ private static String key(BlockTransactionHash reference) {
public static class ProofListener {
private final Set<BlockTransactionHash> failed = new LinkedHashSet<>();
private final Set<BlockTransactionHash> refused = new LinkedHashSet<>();
+ private Wallet reportedWallet;
private int failedEvents;
private int refusedEvents;
@Subscribe
public void transactionProofsFailed(TransactionProofsFailedEvent event) {
failedEvents++;
+ reportedWallet = event.getWallet();
failed.addAll(event.getReferences());
}
@Subscribe
public void transactionProofsRefused(TransactionProofsRefusedEvent event) {
refusedEvents++;
+ reportedWallet = event.getWallet();
refused.addAll(event.getReferences());
}
+ public Wallet getReportedWallet() {
+ return reportedWallet;
+ }
+
public Set<BlockTransactionHash> getFailed() {
return failed;
}
@@ -1456,6 +1501,7 @@ public boolean isEmpty() {
public void reset() {
failed.clear();
refused.clear();
+ reportedWallet = null;
failedEvents = 0;
refusedEvents = 0;
}Why this scored 36/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.