diff --git a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java index 9cbcb3a6..05a7f18c 100644 --- a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java @@ -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 filteredTxHashes; - List filteredTxn; - OrphanBlock(Block block, @Nullable Set filteredTxHashes, @Nullable List filteredTxn) { + List filteredTxHashes; + Map filteredTxn; + OrphanBlock(Block block, @Nullable List filteredTxHashes, @Nullable Map 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 filteredTxnHashSet = new HashSet(block.getTransactionHashes()); - List 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 filteredTxHashList, @Nullable List filteredTxn) throws BlockStoreException, VerificationException, PrunedException { + @Nullable List filteredTxHashList, @Nullable Map 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 filteredTxHashList, - @Nullable final List filteredTxn) throws BlockStoreException, VerificationException, PrunedException { + @Nullable final List filteredTxHashList, + @Nullable final Map 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 filteredTxHashList, - @Nullable final List filteredTxn, + @Nullable final List filteredTxHashList, + @Nullable final Map 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 filteredTxHashList, - @Nullable List filteredTxn, + @Nullable List filteredTxHashList, + @Nullable Map 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); } } } diff --git a/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java b/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java index 6a416cdd..1a4e74f4 100644 --- a/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java +++ b/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java @@ -31,11 +31,11 @@ public class FilteredBlock extends Message { // The PartialMerkleTree of transactions private PartialMerkleTree merkleTree; - private Set cachedTransactionHashes = null; + private List 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 associatedTransactions = new LinkedList(); + private Map associatedTransactions = new HashMap(); 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 getTransactionHashes() throws VerificationException { + public List getTransactionHashes() throws VerificationException { if (cachedTransactionHashes != null) - return Collections.unmodifiableSet(cachedTransactionHashes); - Set hashesMatched = new HashSet(); + return Collections.unmodifiableList(cachedTransactionHashes); + List hashesMatched = new LinkedList(); 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 getAssociatedTransactions() { - return Collections.unmodifiableList(associatedTransactions); + public Map getAssociatedTransactions() { + return Collections.unmodifiableMap(associatedTransactions); } } diff --git a/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java b/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java index a10711bd..8be9d196 100644 --- a/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java +++ b/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java @@ -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 matchedHashes) throws VerificationException { + private Sha256Hash recursiveExtractHashes(int height, int pos, ValuesUsed used, List 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 matchedHashes) throws VerificationException { + public Sha256Hash getTxnHashAndMerkleRoot(List matchedHashes) throws VerificationException { matchedHashes.clear(); // An empty set will not work diff --git a/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java b/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java index 643c1aee..f0c141c7 100644 --- a/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java +++ b/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java @@ -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 txesMatched = block.getTransactionHashes(); + List 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 txHashList = filteredBlock.getTransactionHashes(); + List 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);