cachedTransactionHashes = null;
@@ -40,7 +39,13 @@ public class FilteredBlock extends Message {
public FilteredBlock(NetworkParameters params, byte[] payloadBytes) throws ProtocolException {
super(params, payloadBytes, 0);
}
-
+
+ public FilteredBlock(NetworkParameters params, Block header, PartialMerkleTree pmt) {
+ super(params);
+ this.header = header;
+ this.merkleTree = pmt;
+ }
+
@Override
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
if (header.transactions == null)
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 37c3d0750..894218a19 100644
--- a/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java
+++ b/core/src/main/java/com/google/bitcoin/core/PartialMerkleTree.java
@@ -23,6 +23,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import static com.google.bitcoin.core.Utils.*;
+
/**
* A data structure that contains proofs of block inclusion for one or more transactions, in an efficient manner.
*
@@ -62,14 +64,44 @@ public class PartialMerkleTree extends Message {
public PartialMerkleTree(NetworkParameters params, byte[] payloadBytes, int offset) throws ProtocolException {
super(params, payloadBytes, offset);
}
-
+
+ /**
+ * Constructs a new PMT with the given bit set (little endian) and the raw list of hashes including internal hashes,
+ * taking ownership of the list.
+ */
+ public PartialMerkleTree(NetworkParameters params, byte[] bits, List hashes, int origTxCount) {
+ super(params);
+ this.matchedChildBits = bits;
+ this.hashes = hashes;
+ this.transactionCount = origTxCount;
+ }
+
+ /**
+ * Calculates a PMT given the list of leaf hashes and which leaves need to be included. The relevant interior hashes
+ * are calculated and a new PMT returned.
+ */
+ public static PartialMerkleTree buildFromLeaves(NetworkParameters params, byte[] includeBits, List allLeafHashes) {
+ // Calculate height of the tree.
+ int height = 0;
+ while (getTreeWidth(allLeafHashes.size(), height) > 1)
+ height++;
+ List bitList = new ArrayList();
+ List hashes = new ArrayList();
+ traverseAndBuild(height, 0, allLeafHashes, includeBits, bitList, hashes);
+ byte[] bits = new byte[(int)Math.ceil(bitList.size() / 8.0)];
+ for (int i = 0; i < bitList.size(); i++)
+ if (bitList.get(i))
+ Utils.setBitLE(bits, i);
+ return new PartialMerkleTree(params, bits, hashes, allLeafHashes.size());
+ }
+
@Override
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
- Utils.uint32ToByteStreamLE(transactionCount, stream);
+ uint32ToByteStreamLE(transactionCount, stream);
stream.write(new VarInt(hashes.size()).encode());
for (Sha256Hash hash : hashes)
- stream.write(Utils.reverseBytes(hash.getBytes()));
+ stream.write(reverseBytes(hash.getBytes()));
stream.write(new VarInt(matchedChildBits.length).encode());
stream.write(matchedChildBits);
@@ -86,18 +118,62 @@ public class PartialMerkleTree extends Message {
int nFlagBytes = (int) readVarInt();
matchedChildBits = readBytes(nFlagBytes);
-
+
length = cursor - offset;
}
-
+
+ // Based on CPartialMerkleTree::TraverseAndBuild in Bitcoin Core.
+ private static void traverseAndBuild(int height, int pos, List allLeafHashes, byte[] includeBits,
+ List matchedChildBits, List resultHashes) {
+ boolean parentOfMatch = false;
+ // Is this node a parent of at least one matched hash?
+ for (int p = pos << height; p < (pos+1) << height && p < allLeafHashes.size(); p++) {
+ if (Utils.checkBitLE(includeBits, p)) {
+ parentOfMatch = true;
+ break;
+ }
+ }
+ // Store as a flag bit.
+ matchedChildBits.add(parentOfMatch);
+ if (height == 0 || !parentOfMatch) {
+ // If at height 0, or nothing interesting below, store hash and stop.
+ resultHashes.add(calcHash(height, pos, allLeafHashes));
+ } else {
+ // Otherwise descend into the subtrees.
+ int h = height - 1;
+ int p = pos * 2;
+ traverseAndBuild(h, p, allLeafHashes, includeBits, matchedChildBits, resultHashes);
+ if (p + 1 < getTreeWidth(allLeafHashes.size(), h))
+ traverseAndBuild(h, p + 1, allLeafHashes, includeBits, matchedChildBits, resultHashes);
+ }
+ }
+
+ private static Sha256Hash calcHash(int height, int pos, List hashes) {
+ if (height == 0) {
+ // Hash at height 0 is just the regular tx hash itself.
+ return hashes.get(pos);
+ }
+ int h = height - 1;
+ int p = pos * 2;
+ Sha256Hash left = calcHash(h, p, hashes);
+ // Calculate right hash if not beyond the end of the array - copy left hash otherwise.
+ Sha256Hash right;
+ if (p + 1 < getTreeWidth(hashes.size(), h)) {
+ right = calcHash(h, p + 1, hashes);
+ } else {
+ right = left;
+ }
+ return combineLeftRight(left.getBytes(), right.getBytes());
+ }
+
@Override
protected void parseLite() {
}
// helper function to efficiently calculate the number of nodes at given height in the merkle tree
- private int getTreeWidth(int height) {
- return (transactionCount+(1 << height)-1) >> height;
+ private static int getTreeWidth(int transactionCount, int height) {
+ return (transactionCount + (1 << height) - 1) >> height;
}
private static class ValuesUsed {
@@ -111,30 +187,35 @@ public class PartialMerkleTree extends Message {
// overflowed the bits array - failure
throw new VerificationException("CPartialMerkleTree overflowed its bits array");
}
- boolean parentOfMatch = Utils.checkBitLE(matchedChildBits, used.bitsUsed++);
+ boolean parentOfMatch = checkBitLE(matchedChildBits, used.bitsUsed++);
if (height == 0 || !parentOfMatch) {
// if at height 0, or nothing interesting below, use stored hash and do not descend
if (used.hashesUsed >= hashes.size()) {
// overflowed the hash array - failure
throw new VerificationException("CPartialMerkleTree overflowed its hash array");
}
+ Sha256Hash hash = hashes.get(used.hashesUsed++);
if (height == 0 && parentOfMatch) // in case of height 0, we have a matched txid
- matchedHashes.add(hashes.get(used.hashesUsed));
- return hashes.get(used.hashesUsed++);
+ matchedHashes.add(hash);
+ return hash;
} else {
// otherwise, descend into the subtrees to extract matched txids and hashes
- byte[] left = recursiveExtractHashes(height-1, pos*2, used, matchedHashes).getBytes(), right;
- if (pos*2+1 < getTreeWidth(height-1))
- right = recursiveExtractHashes(height-1, pos*2+1, used, matchedHashes).getBytes();
+ byte[] left = recursiveExtractHashes(height - 1, pos * 2, used, matchedHashes).getBytes(), right;
+ if (pos * 2 + 1 < getTreeWidth(transactionCount, height-1))
+ right = recursiveExtractHashes(height - 1, pos * 2 + 1, used, matchedHashes).getBytes();
else
right = left;
// and combine them before returning
- return new Sha256Hash(Utils.reverseBytes(Utils.doubleDigestTwoBuffers(
- Utils.reverseBytes(left), 0, 32,
- Utils.reverseBytes(right), 0, 32)));
+ return combineLeftRight(left, right);
}
}
-
+
+ private static Sha256Hash combineLeftRight(byte[] left, byte[] right) {
+ return new Sha256Hash(reverseBytes(doubleDigestTwoBuffers(
+ reverseBytes(left), 0, 32,
+ reverseBytes(right), 0, 32)));
+ }
+
/**
* Extracts tx hashes that are in this merkle tree
* and returns the merkle root of this tree.
@@ -164,7 +245,7 @@ public class PartialMerkleTree extends Message {
throw new VerificationException("Got a CPartialMerkleTree with fewer matched bits than hashes");
// calculate height of tree
int height = 0;
- while (getTreeWidth(height) > 1)
+ while (getTreeWidth(transactionCount, height) > 1)
height++;
// traverse the partial tree
ValuesUsed used = new ValuesUsed();
diff --git a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
index 2a3ba8a8f..e1572715c 100644
--- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
+++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
@@ -143,7 +143,9 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
final double rate = checkNotNull(chain).getFalsePositiveRate();
final double target = bloomFilterMerger.getBloomFilterFPRate() * MAX_FP_RATE_INCREASE;
if (rate > target) {
- log.info("Force update Bloom filter due to high false positive rate ({} vs {})", rate, target);
+ // TODO: Avoid hitting this path if the remote peer didn't acknowledge applying a new filter yet.
+ if (log.isDebugEnabled())
+ log.debug("Force update Bloom filter due to high false positive rate ({} vs {})", rate, target);
recalculateFastCatchupAndFilter(FilterRecalculateMode.FORCE_SEND_FOR_REFRESH);
}
}
diff --git a/core/src/main/java/com/google/bitcoin/testing/FakeTxBuilder.java b/core/src/main/java/com/google/bitcoin/testing/FakeTxBuilder.java
index 3f1a5f88f..f29e39254 100644
--- a/core/src/main/java/com/google/bitcoin/testing/FakeTxBuilder.java
+++ b/core/src/main/java/com/google/bitcoin/testing/FakeTxBuilder.java
@@ -208,4 +208,14 @@ public class FakeTxBuilder {
b.solve();
return b;
}
+
+ public static Block makeSolvedTestBlock(Block prev, Address to, Transaction... transactions) throws BlockStoreException {
+ Block b = prev.createNextBlock(to);
+ // Coinbase tx already exists.
+ for (Transaction tx : transactions) {
+ b.addTransaction(tx);
+ }
+ b.solve();
+ return b;
+ }
}
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 6857bac66..c793788fa 100644
--- a/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java
+++ b/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java
@@ -20,6 +20,7 @@ package com.google.bitcoin.core;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.store.MemoryBlockStore;
+import com.google.bitcoin.testing.FakeTxBuilder;
import com.google.bitcoin.testing.InboundMessageQueuer;
import com.google.bitcoin.testing.TestWithPeerGroup;
import com.google.bitcoin.wallet.KeyChainGroup;
@@ -68,7 +69,25 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
// Check round tripping.
assertEquals(block, new FilteredBlock(params, block.bitcoinSerialize()));
}
-
+
+ @Test
+ public void createFilteredBlock() throws Exception {
+ ECKey key1 = new ECKey();
+ ECKey key2 = new ECKey();
+ Transaction tx1 = FakeTxBuilder.createFakeTx(params, Coin.COIN, key1);
+ Transaction tx2 = FakeTxBuilder.createFakeTx(params, Coin.FIFTY_COINS, key2.toAddress(params));
+ Block block = FakeTxBuilder.makeSolvedTestBlock(params.getGenesisBlock(), new Address(params, "msg2t2V2sWNd85LccoddtWysBTR8oPnkzW"), tx1, tx2);
+ BloomFilter filter = new BloomFilter(4, 0.1, 1);
+ filter.insert(key1);
+ filter.insert(key2);
+ FilteredBlock filteredBlock = filter.applyAndUpdate(block);
+ assertEquals(4, filteredBlock.getTransactionCount());
+ // This call triggers verification of the just created data.
+ List txns = filteredBlock.getTransactionHashes();
+ assertTrue(txns.contains(tx1.getHash()));
+ assertTrue(txns.contains(tx2.getHash()));
+ }
+
@Test
public void serializeDownloadBlockWithWallet() throws Exception {
unitTestParams = UnitTestParams.get();