verify fetched transactions match requested txid

This commit is contained in:
Craig Raw
2026-08-02 14:52:01 +02:00
parent 2b9c3eb737
commit 3979694e2f
3 changed files with 100 additions and 16 deletions
@@ -865,6 +865,11 @@ public class ElectrumServer {
continue;
}
if(!transaction.getTxId().equals(hash)) {
log.error("Server returned transaction " + transaction.getTxId() + " for requested txid " + hash);
throw new IllegalStateException("Server returned a transaction that does not match the requested txid " + hash);
}
Optional<BlockTransactionHash> optionalReference = references.keySet().stream().filter(reference -> reference.getHash().equals(hash)).findFirst();
if(optionalReference.isEmpty()) {
throw new IllegalStateException("Returned transaction " + hash.toString() + " that was not requested");
@@ -913,7 +918,7 @@ public class ElectrumServer {
return transactionMap;
} catch (IllegalStateException e) {
throw new ServerException(e.getCause());
throw new ServerException(e.getMessage(), e);
} catch (ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e.getCause());
} catch (Exception e) {
@@ -1041,14 +1046,24 @@ public class ElectrumServer {
Map<String, VerboseTransaction> result = electrumServerRpc.getVerboseTransactions(getTransport(), txids, scriptHash);
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
for(String txid : result.keySet()) {
Sha256Hash hash = Sha256Hash.wrap(txid);
BlockTransaction blockTransaction = result.get(txid).getBlockTransaction();
transactionMap.put(hash, blockTransaction);
}
try {
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
for(String txid : result.keySet()) {
Sha256Hash hash = Sha256Hash.wrap(txid);
VerboseTransaction verboseTransaction = result.get(txid);
if(!hash.equals(Sha256Hash.wrap(verboseTransaction.txid))) {
log.error("Server returned transaction " + verboseTransaction.txid + " for requested txid " + hash);
throw new ServerException("Server returned a transaction that does not match the requested txid " + hash);
}
return transactionMap;
transactionMap.put(hash, verboseTransaction.getBlockTransaction());
}
return transactionMap;
} catch(RuntimeException e) {
log.error("Could not retrieve referenced transactions", e);
throw new ServerException("Could not retrieve referenced transactions", e);
}
}
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks, boolean useCached) throws ServerException {
@@ -1307,17 +1322,26 @@ public class ElectrumServer {
continue;
}
Transaction transaction;
try {
Transaction transaction = new Transaction(Utils.hexToBytes(strRawTx));
for(TransactionOutput txOutput : transaction.getOutputs()) {
if(txOutput.getScript().equals(outputScript)) {
transactionOutputs.add(txOutput);
}
}
transactions.add(transaction);
transaction = new Transaction(Utils.hexToBytes(strRawTx));
} catch(ProtocolException e) {
log.error("Could not parse tx: " + strRawTx);
continue;
}
if(!transaction.getTxId().toString().equalsIgnoreCase(txid)) {
log.error("Server returned transaction " + transaction.getTxId() + " for requested txid " + txid);
throw new ServerException("Server returned a transaction that does not match the requested txid " + txid);
}
for(TransactionOutput txOutput : transaction.getOutputs()) {
if(txOutput.getScript().equals(outputScript)) {
transactionOutputs.add(txOutput);
}
}
transactions.add(transaction);
}
for(Transaction transaction : transactions) {
@@ -44,6 +44,12 @@ public class VerboseTransaction {
}
public BlockTransaction getBlockTransaction() {
return new BlockTransaction(Sha256Hash.wrap(txid), getHeight(), getDate(), 0L, new Transaction(Utils.hexToBytes(hex)), blockhash == null ? null : Sha256Hash.wrap(blockhash));
Sha256Hash declaredTxid = Sha256Hash.wrap(txid);
Transaction transaction = new Transaction(Utils.hexToBytes(hex));
if(!transaction.getTxId().equals(declaredTxid)) {
throw new IllegalStateException("Server returned transaction " + transaction.getTxId() + " for declared txid " + declaredTxid);
}
return new BlockTransaction(declaredTxid, getHeight(), getDate(), 0L, transaction, blockhash == null ? null : Sha256Hash.wrap(blockhash));
}
}
@@ -0,0 +1,54 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class VerboseTransactionTest {
private static final String LEGACY_TXID = "b78d4708ed7e9d8f023231c770b35727e23689b28a09eae017965a38e41dec83";
private static final String LEGACY_HEX = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804e6ed5b1b02b000ffffffff0100f2052a01000000434104ab3779ba979cd2f7d76fd1b6a57f42bf4bdd9210409a693a46d6d426c0ba021aca2f364ce5141b7721b47fb5f34ce7301abbab24c067048b721c633ae65e1af0ac00000000";
private static final String SEGWIT_TXID = "f8fc281eb915a1b350f89ffc5090c71dc65acaf6e59e29dc5afe53d842f19d01";
private static final String SEGWIT_WTXID = "ef6da0d3f41a666c0eb914cf592caa803aa465c25cf630f786cd3384619605ad";
private static final String SEGWIT_HEX = "020000000001014596cc6219630c13cbca099838c2fb0920cde29de1e5473087de8bbce06b9f510100000000ffffffff02502a4b000000000017a914b1cd708c9d49c7ad6ec851ad7f24076233fa7cfb8772915600000000001600145279dddc177883923bcf3bd5aab50e725dca01f302483045022100e9056474685b7d885956c7c7e5ac77e1249373e5d222b13620dcde6a63e337d602206ebb59c1834e991e9c9f6129a78c7669cfc4c41c4d19c6be4dabe6749715d5ee01210278f5f957591a07a51fc5033c3407de2ff722b0a5f98e91c9a9e1e038c9b1b59300000000";
@Test
public void acceptsHexMatchingDeclaredTxid() {
BlockTransaction blockTransaction = verboseTransaction(LEGACY_TXID, LEGACY_HEX).getBlockTransaction();
assertEquals(LEGACY_TXID, blockTransaction.getHashAsString());
assertEquals(blockTransaction.getHash(), blockTransaction.getTransaction().getTxId());
}
/**
* Segwit transactions are requested and declared by their non-witness txid, but are serialized with their witness data.
* Comparing the wtxid here would reject every segwit transaction.
*/
@Test
public void acceptsSegwitTransactionDeclaredByTxid() {
BlockTransaction blockTransaction = verboseTransaction(SEGWIT_TXID, SEGWIT_HEX).getBlockTransaction();
assertEquals(SEGWIT_TXID, blockTransaction.getHashAsString());
assertEquals(SEGWIT_WTXID, blockTransaction.getTransaction().getWTxId().toString());
assertNotEquals(blockTransaction.getTransaction().getTxId(), blockTransaction.getTransaction().getWTxId());
}
@Test
public void rejectsHexNotMatchingDeclaredTxid() {
VerboseTransaction verboseTransaction = verboseTransaction(LEGACY_TXID, SEGWIT_HEX);
assertThrows(IllegalStateException.class, verboseTransaction::getBlockTransaction);
}
private VerboseTransaction verboseTransaction(String txid, String hex) {
VerboseTransaction verboseTransaction = new VerboseTransaction();
verboseTransaction.txid = txid;
verboseTransaction.hex = hex;
verboseTransaction.confirmations = 0;
return verboseTransaction;
}
}