3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00

Announce transactions to wallet in order even from FilteredBlocks

This commit is contained in:
Matt Corallo 2013-07-08 17:35:41 +02:00 committed by Mike Hearn
parent a05d69537d
commit 09752848bb
4 changed files with 47 additions and 42 deletions

View File

@ -108,9 +108,9 @@ public abstract class AbstractBlockChain {
// Holds a block header and, optionally, a list of tx hashes or block's transactions
protected static class OrphanBlock {
Block block;
Set<Sha256Hash> filteredTxHashes;
List<Transaction> filteredTxn;
OrphanBlock(Block block, @Nullable Set<Sha256Hash> filteredTxHashes, @Nullable List<Transaction> filteredTxn) {
List<Sha256Hash> filteredTxHashes;
Map<Sha256Hash, Transaction> filteredTxn;
OrphanBlock(Block block, @Nullable List<Sha256Hash> filteredTxHashes, @Nullable Map<Sha256Hash, Transaction> filteredTxn) {
final boolean filtered = filteredTxHashes != null && filteredTxn != null;
Preconditions.checkArgument((block.transactions == null && filtered)
|| (block.transactions != null && !filtered));
@ -259,12 +259,7 @@ public abstract class AbstractBlockChain {
// a false positive, as expected in any Bloom filtering scheme). The filteredTxn list here will usually
// only be full of data when we are catching up to the head of the chain and thus haven't witnessed any
// of the transactions.
Set<Sha256Hash> filteredTxnHashSet = new HashSet<Sha256Hash>(block.getTransactionHashes());
List<Transaction> filteredTxn = block.getAssociatedTransactions();
for (Transaction tx : filteredTxn) {
checkState(filteredTxnHashSet.remove(tx.getHash()));
}
return add(block.getBlockHeader(), true, filteredTxnHashSet, filteredTxn);
return add(block.getBlockHeader(), true, block.getTransactionHashes(), block.getAssociatedTransactions());
} catch (BlockStoreException e) {
// TODO: Figure out a better way to propagate this exception to the user.
throw new RuntimeException(e);
@ -311,9 +306,10 @@ public abstract class AbstractBlockChain {
private long statsLastTime = System.currentTimeMillis();
private long statsBlocksAdded;
// filteredTxHashList and filteredTxn[i].GetHash() should be mutually exclusive
// filteredTxHashList contains all transactions, filteredTxn just a subset
private boolean add(Block block, boolean tryConnecting,
@Nullable Set<Sha256Hash> filteredTxHashList, @Nullable List<Transaction> filteredTxn) throws BlockStoreException, VerificationException, PrunedException {
@Nullable List<Sha256Hash> filteredTxHashList, @Nullable Map<Sha256Hash, Transaction> filteredTxn)
throws BlockStoreException, VerificationException, PrunedException {
lock.lock();
try {
// TODO: Use read/write locks to ensure that during chain download properties are still low latency.
@ -396,8 +392,8 @@ public abstract class AbstractBlockChain {
// than the previous one when connecting (eg median timestamp check)
// It could be exposed, but for now we just set it to shouldVerifyTransactions()
private void connectBlock(final Block block, StoredBlock storedPrev, boolean expensiveChecks,
@Nullable final Set<Sha256Hash> filteredTxHashList,
@Nullable final List<Transaction> filteredTxn) throws BlockStoreException, VerificationException, PrunedException {
@Nullable final List<Sha256Hash> filteredTxHashList,
@Nullable final Map<Sha256Hash, Transaction> filteredTxn) throws BlockStoreException, VerificationException, PrunedException {
checkState(lock.isLocked());
boolean filtered = filteredTxHashList != null && filteredTxn != null;
boolean fullBlock = block.transactions != null && !filtered;
@ -420,7 +416,6 @@ public abstract class AbstractBlockChain {
log.info("Block {} connects to top of best chain with {} transaction(s)",
block.getHashAsString(), filteredTxn.size() + filteredTxHashList.size());
for (Sha256Hash hash : filteredTxHashList) log.info(" matched tx {}", hash);
for (Transaction tx : filteredTxn) log.info(" matched tx {}", tx.getHash());
}
if (expensiveChecks && block.getTimeSeconds() <= getMedianTimestampOfRecentBlocks(head, blockStore))
throw new VerificationException("Block's timestamp is too early");
@ -481,8 +476,8 @@ public abstract class AbstractBlockChain {
}
private void informListenersForNewBlock(final Block block, final NewBlockType newBlockType,
@Nullable final Set<Sha256Hash> filteredTxHashList,
@Nullable final List<Transaction> filteredTxn,
@Nullable final List<Sha256Hash> filteredTxHashList,
@Nullable final Map<Sha256Hash, Transaction> filteredTxn,
final StoredBlock newStoredBlock) throws VerificationException {
// Notify the listeners of the new block, so the depth and workDone of stored transactions can be updated
// (in the case of the listener being a wallet). Wallets need to know how deep each transaction is so
@ -519,22 +514,28 @@ public abstract class AbstractBlockChain {
}
private static void informListenerForNewTransactions(Block block, NewBlockType newBlockType,
@Nullable Set<Sha256Hash> filteredTxHashList,
@Nullable List<Transaction> filteredTxn,
@Nullable List<Sha256Hash> filteredTxHashList,
@Nullable Map<Sha256Hash, Transaction> filteredTxn,
StoredBlock newStoredBlock, boolean first,
BlockChainListener listener) throws VerificationException {
if (block.transactions != null || filteredTxn != null) {
if (block.transactions != null) {
// If this is not the first wallet, ask for the transactions to be duplicated before being given
// to the wallet when relevant. This ensures that if we have two connected wallets and a tx that
// is relevant to both of them, they don't end up accidentally sharing the same object (which can
// result in temporary in-memory corruption during re-orgs). See bug 257. We only duplicate in
// the case of multiple wallets to avoid an unnecessary efficiency hit in the common case.
sendTransactionsToListener(newStoredBlock, newBlockType, listener,
block.transactions != null ? block.transactions : filteredTxn, !first);
}
if (filteredTxHashList != null) {
sendTransactionsToListener(newStoredBlock, newBlockType, listener, block.transactions, !first);
} else if (filteredTxHashList != null) {
checkArgument(filteredTxn != null);
// We must send transactions to listeners in the order they appeared in the block - thus we iterate over the
// set of hashes and call sendTransactionsToListener with individual txn when they have not already been
// seen in loose broadcasts - otherwise notifyTransactionIsInBlock on the hash
for (Sha256Hash hash : filteredTxHashList) {
listener.notifyTransactionIsInBlock(hash, newStoredBlock, newBlockType);
Transaction tx = filteredTxn.get(hash);
if (tx != null)
sendTransactionsToListener(newStoredBlock, newBlockType, listener, Arrays.asList(tx), !first);
else
listener.notifyTransactionIsInBlock(hash, newStoredBlock, newBlockType);
}
}
}

View File

@ -31,11 +31,11 @@ public class FilteredBlock extends Message {
// The PartialMerkleTree of transactions
private PartialMerkleTree merkleTree;
private Set<Sha256Hash> cachedTransactionHashes = null;
private List<Sha256Hash> cachedTransactionHashes = null;
// A set of transactions who's hashes are a subset of getTransactionHashes()
// These were relayed as a part of the filteredblock getdata, ie likely weren't previously received as loose transactions
private List<Transaction> associatedTransactions = new LinkedList<Transaction>();
private Map<Sha256Hash, Transaction> associatedTransactions = new HashMap<Sha256Hash, Transaction>();
public FilteredBlock(NetworkParameters params, byte[] payloadBytes) throws ProtocolException {
super(params, payloadBytes, 0);
@ -66,13 +66,13 @@ public class FilteredBlock extends Message {
*
* @throws ProtocolException If the partial merkle block is invalid or the merkle root of the partial merkle block doesnt match the block header
*/
public Set<Sha256Hash> getTransactionHashes() throws VerificationException {
public List<Sha256Hash> getTransactionHashes() throws VerificationException {
if (cachedTransactionHashes != null)
return Collections.unmodifiableSet(cachedTransactionHashes);
Set<Sha256Hash> hashesMatched = new HashSet<Sha256Hash>();
return Collections.unmodifiableList(cachedTransactionHashes);
List<Sha256Hash> hashesMatched = new LinkedList<Sha256Hash>();
if (header.getMerkleRoot().equals(merkleTree.getTxnHashAndMerkleRoot(hashesMatched))) {
cachedTransactionHashes = hashesMatched;
return Collections.unmodifiableSet(cachedTransactionHashes);
return Collections.unmodifiableList(cachedTransactionHashes);
} else
throw new VerificationException("Merkle root of block header does not match merkle root of partial merkle tree.");
}
@ -94,15 +94,16 @@ public class FilteredBlock extends Message {
* @returns false if the tx is not relevant to this FilteredBlock
*/
public boolean provideTransaction(Transaction tx) throws VerificationException {
if (getTransactionHashes().contains(tx.getHash())) {
associatedTransactions.add(tx);
Sha256Hash hash = tx.getHash();
if (getTransactionHashes().contains(hash)) {
associatedTransactions.put(hash, tx);
return true;
} else
return false;
}
/** Gets the set of transactions which were provided using provideTransaction() which match in getTransactionHashes() */
public List<Transaction> getAssociatedTransactions() {
return Collections.unmodifiableList(associatedTransactions);
public Map<Sha256Hash, Transaction> getAssociatedTransactions() {
return Collections.unmodifiableMap(associatedTransactions);
}
}

View File

@ -20,6 +20,7 @@ package com.google.bitcoin.core;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@ -98,7 +99,7 @@ public class PartialMerkleTree extends Message {
// recursive function that traverses tree nodes, consuming the bits and hashes produced by TraverseAndBuild.
// it returns the hash of the respective node.
private Sha256Hash recursiveExtractHashes(int height, int pos, ValuesUsed used, Set<Sha256Hash> matchedHashes) throws VerificationException {
private Sha256Hash recursiveExtractHashes(int height, int pos, ValuesUsed used, List<Sha256Hash> matchedHashes) throws VerificationException {
if (used.bitsUsed >= matchedChildBits.length*8) {
// overflowed the bits array - failure
throw new VerificationException("CPartialMerkleTree overflowed its bits array");
@ -135,10 +136,11 @@ public class PartialMerkleTree extends Message {
* merkle root contained in the block header for security.
*
* @param matchedHashes A list which will contain the matched txn (will be cleared)
* Required to be a LinkedHashSet in order to retain order or transactions in the block
* @return the merkle root of this merkle tree
* @throws ProtocolException if this partial merkle tree is invalid
*/
public Sha256Hash getTxnHashAndMerkleRoot(Set<Sha256Hash> matchedHashes) throws VerificationException {
public Sha256Hash getTxnHashAndMerkleRoot(List<Sha256Hash> matchedHashes) throws VerificationException {
matchedHashes.clear();
// An empty set will not work

View File

@ -8,6 +8,7 @@ import org.spongycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@ -27,7 +28,7 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
assertTrue(block.getBlockHeader().getHash().equals(new Sha256Hash("000000000000dab0130bbcc991d3d7ae6b81aa6f50a798888dfe62337458dc45")));
// Check that the partial merkle tree is correct
Set<Sha256Hash> txesMatched = block.getTransactionHashes();
List<Sha256Hash> txesMatched = block.getTransactionHashes();
assertTrue(txesMatched.size() == 1);
assertTrue(txesMatched.contains(new Sha256Hash("63194f18be0af63f2c6bc9dc0f777cbefed3d9415c4af83f3ee3a3d669c00cb5")));
}
@ -45,25 +46,25 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
assertTrue(block.getHash().equals(new Sha256Hash("00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090")));
assertTrue(filteredBlock.getHash().equals(block.getHash()));
Set<Sha256Hash> txHashList = filteredBlock.getTransactionHashes();
List<Sha256Hash> txHashList = filteredBlock.getTransactionHashes();
assertTrue(txHashList.size() == 4);
// Four transactions (0, 1, 2, 6) from block 100001
Transaction tx0 = new Transaction(unitTestParams, Hex.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff07044c86041b010dffffffff0100f2052a01000000434104b27f7e9475ccf5d9a431cb86d665b8302c140144ec2397fce792f4a4e7765fecf8128534eaa71df04f93c74676ae8279195128a1506ebf7379d23dab8fca0f63ac00000000"));
assertTrue(tx0.getHash().equals(new Sha256Hash("bb28a1a5b3a02e7657a81c38355d56c6f05e80b9219432e3352ddcfc3cb6304c")));
assertTrue(txHashList.contains(tx0.getHash()));
assertEquals(tx0.getHash(), txHashList.get(0));
Transaction tx1 = new Transaction(unitTestParams, Hex.decode("0100000001d992e5a888a86d4c7a6a69167a4728ee69497509740fc5f456a24528c340219a000000008b483045022100f0519bdc9282ff476da1323b8ef7ffe33f495c1a8d52cc522b437022d83f6a230220159b61d197fbae01b4a66622a23bc3f1def65d5fa24efd5c26fa872f3a246b8e014104839f9023296a1fabb133140128ca2709f6818c7d099491690bd8ac0fd55279def6a2ceb6ab7b5e4a71889b6e739f09509565eec789e86886f6f936fa42097adeffffffff02000fe208010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac00e32321000000001976a9140c34f4e29ab5a615d5ea28d4817f12b137d62ed588ac00000000"));
assertTrue(tx1.getHash().equals(new Sha256Hash("fbde5d03b027d2b9ba4cf5d4fecab9a99864df2637b25ea4cbcb1796ff6550ca")));
assertTrue(txHashList.contains(tx1.getHash()));
assertEquals(tx1.getHash(), txHashList.get(1));
Transaction tx2 = new Transaction(unitTestParams, Hex.decode("01000000059daf0abe7a92618546a9dbcfd65869b6178c66ec21ccfda878c1175979cfd9ef000000004a493046022100c2f7f25be5de6ce88ac3c1a519514379e91f39b31ddff279a3db0b1a229b708b022100b29efbdbd9837cc6a6c7318aa4900ed7e4d65662c34d1622a2035a3a5534a99a01ffffffffd516330ebdf075948da56db13d22632a4fb941122df2884397dda45d451acefb0000000048473044022051243debe6d4f2b433bee0cee78c5c4073ead0e3bde54296dbed6176e128659c022044417bfe16f44eb7b6eb0cdf077b9ce972a332e15395c09ca5e4f602958d266101ffffffffe1f5aa33961227b3c344e57179417ce01b7ccd421117fe2336289b70489883f900000000484730440220593252bb992ce3c85baf28d6e3aa32065816271d2c822398fe7ee28a856bc943022066d429dd5025d3c86fd8fd8a58e183a844bd94aa312cefe00388f57c85b0ca3201ffffffffe207e83718129505e6a7484831442f668164ae659fddb82e9e5421a081fb90d50000000049483045022067cf27eb733e5bcae412a586b25a74417c237161a084167c2a0b439abfebdcb2022100efcc6baa6824b4c5205aa967e0b76d31abf89e738d4b6b014e788c9a8cccaf0c01ffffffffe23b8d9d80a9e9d977fab3c94dbe37befee63822443c3ec5ae5a713ede66c3940000000049483045022020f2eb35036666b1debe0d1d2e77a36d5d9c4e96c1dba23f5100f193dbf524790221008ce79bc1321fb4357c6daee818038d41544749127751726e46b2b320c8b565a201ffffffff0200ba1dd2050000001976a914366a27645806e817a6cd40bc869bdad92fe5509188ac40420f00000000001976a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac00000000"));
assertTrue(tx2.getHash().equals(new Sha256Hash("8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb")));
assertTrue(txHashList.contains(tx2.getHash()));
assertEquals(tx2.getHash(), txHashList.get(2));
Transaction tx3 = new Transaction(unitTestParams, Hex.decode("01000000011b56cf3aab3286d582c055a42af3a911ee08423f276da702bb67f1222ac1a5b6000000008c4930460221009e9fba682e162c9627b96b7df272006a727988680b956c61baff869f0907b8fb022100a9c19adc7c36144bafe526630783845e5cb9554d30d3edfb56f0740274d507f30141046e0efbfac7b1615ad553a6f097615bc63b7cdb3b8e1cb3263b619ba63740012f51c7c5b09390e3577e377b7537e61226e315f95f926444fc5e5f2978c112e448ffffffff02c0072b11010000001976a914b73e9e01933351ca076faf8e0d94dd58079d0b1f88ac80b63908000000001976a9141aca0bdf0d2cee63db19aa4a484f45a4e26a880c88ac00000000"));
assertTrue(tx3.getHash().equals(new Sha256Hash("c5abc61566dbb1c4bce5e1fda7b66bed22eb2130cea4b721690bc1488465abc9")));
assertTrue(txHashList.contains(tx3.getHash()));
assertEquals(tx3.getHash(),txHashList.get(3));
// A wallet which contains a pubkey used in each transaction from above
Wallet wallet = new Wallet(unitTestParams);