diff --git a/core/src/main/java/com/google/bitcoin/core/MemoryPool.java b/core/src/main/java/com/google/bitcoin/core/MemoryPool.java new file mode 100644 index 00000000..4d802895 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/core/MemoryPool.java @@ -0,0 +1,237 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Tracks transactions that are being announced across the network. Typically one is created for you by a PeerGroup + * and then given to each Peer to update. The current purpose is to let Peers update the confidence (number of peers + * broadcasting). It helps address an attack scenario in which a malicious remote peer (or several) feeds you + * invalid transactions, eg, ones that spend coins which don't exist. If you don't see most of the peers announce the + * transaction within a reasonable time, it may be that the TX is not valid. Alternatively, an attacker may control + * your entire internet connection: in this scenario counting broadcasting peers does not help you.

+ * + * It is not at this time directly equivalent to the Satoshi clients memory pool, which tracks + * all transactions not currently included in the best chain - it's simply a cache. + */ +public class MemoryPool { + private static final Logger log = LoggerFactory.getLogger(MemoryPool.class); + + // For each transaction we may have seen: + // - only its hash in an inv packet + // - the full transaction itself, if we asked for it to be sent to us (or a peer sent it regardless) + // + // Before we see the full transaction, we need to track how many peers advertised it, so we can estimate its + // confidence pre-chain inclusion assuming an un-tampered with network connection. After we see the full transaction + // we need to switch from tracking that data in the Entry to tracking it in the TransactionConfidence object itself. + private class WeakTransactionReference extends WeakReference { + public Sha256Hash hash; + public WeakTransactionReference(Transaction tx, ReferenceQueue queue) { + super(tx, queue); + hash = tx.getHash(); + } + }; + private class Entry { + // Invariants: one of the two fields must be null, to indicate which is used. + Set addresses; + // We keep a weak reference to the transaction. This means that if no other bit of code finds the transaction + // worth keeping around it will drop out of memory and we will, at some point, forget about it, which means + // both addresses and tx.get() will be null. When this happens the WeakTransactionReference appears in the queue + // allowing us to delete the associated entry (the tx itself has already gone away). + WeakTransactionReference tx; + } + private LinkedHashMap memoryPool; + + // This ReferenceQueue gets entries added to it when they are only weakly reachable, ie, the MemoryPool is the + // only thing that is tracking the transaction anymore. We check it from time to time and delete memoryPool entries + // corresponding to expired transactions. In this way memory usage of the system is in line with however many + // transactions you actually care to track the confidence of. We can still end up with lots of hashes being stored + // if our peers flood us with invs but the MAX_SIZE param caps this. + private ReferenceQueue referenceQueue; + + /** The max size of a memory pool created with the no-args constructor. */ + public static final int MAX_SIZE = 1000; + + /** + * Creates a memory pool that will track at most the given number of transactions (allowing you to bound memory + * usage). + * @param size Max number of transactions to track. The pool will fill up to this size then stop growing. + */ + public MemoryPool(final int size) { + memoryPool = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry entry) { + // An arbitrary choice to stop the memory used by tracked transactions getting too huge in the event + // of some kind of DoS attack. + return size() > size; + } + }; + referenceQueue = new ReferenceQueue(); + } + + /** + * Creates a memory pool that will track at most {@link MemoryPool#MAX_SIZE} entries. You should normally use + * this constructor. + */ + public MemoryPool() { + this(MAX_SIZE); + } + + /** + * If any transactions have expired due to being only weakly reachable through us, go ahead and delete their + * memoryPool entries - it means we downloaded the transaction and sent it to various event listeners, none of + * which bothered to keep a reference. Typically, this is because the transaction does not involve any keys that + * are relevant to any of our wallets. + */ + private synchronized void cleanPool() { + Reference ref; + while ((ref = referenceQueue.poll()) != null) { + // Find which transaction got deleted by the GC. + WeakTransactionReference txRef = (WeakTransactionReference) ref; + // And remove the associated map entry so the other bits of memory can also be reclaimed. + log.info("Cleaned up tx {}", txRef.hash); + memoryPool.remove(txRef.hash); + } + } + + /** + * Returns the number of peers that have seen the given hash recently. + */ + public synchronized int numBroadcastPeers(Sha256Hash txHash) { + cleanPool(); + Entry entry = memoryPool.get(txHash); + if (entry == null) { + // No such TX known. + return 0; + } else if (entry.tx == null) { + // We've seen at least one peer announce with an inv. + Preconditions.checkNotNull(entry.addresses); + return entry.addresses.size(); + } else if (entry.tx.get() == null) { + // We previously downloaded this transaction, but nothing cared about it so the garbage collector threw + // it away. We also deleted the set that tracked which peers had seen it. Treat this case as a zero and + // just delete it from the map. + memoryPool.remove(txHash); + return 0; + } else { + Preconditions.checkState(entry.addresses == null); + return entry.tx.get().getConfidence().numBroadcastPeers(); + } + } + + /** + * Called by peers when they receive a "tx" message containing a valid serialized transaction. + * @param tx The TX deserialized from the wire. + * @param byPeer The Peer that received it. + * @return An object that is semantically the same TX but may be a different object instance. + */ + public synchronized Transaction seen(Transaction tx, PeerAddress byPeer) { + cleanPool(); + Entry entry = memoryPool.get(tx.getHash()); + if (entry != null) { + // This TX or its hash have been previously announced. + if (entry.tx != null) { + // We already downloaded it. + Preconditions.checkState(entry.addresses == null); + // We only want one canonical object instance for a transaction no matter how many times it is + // deserialized. + if (entry.tx.get() == null) { + // We previously downloaded this transaction, but the garbage collector threw it away because + // no other part of the system cared enough to keep it around (it's not relevant to us). + // Given the lack of interest last time we probably don't need to track it this time either. + log.info("{}: Provided with a transaction that we previously threw away: {}", byPeer, tx.getHash()); + } else { + // We saw it before and kept it around. Hand back the canonical copy. + tx = entry.tx.get(); + log.info("{}: Provided with a transaction downloaded before: [{}] {}", + new Object[] { byPeer, tx.getConfidence().numBroadcastPeers(), tx.getHash() }); + } + tx.getConfidence().markBroadcastBy(byPeer); + return tx; + } else { + // We received a transaction that we have previously seen announced but not downloaded until now. + Preconditions.checkNotNull(entry.addresses); + entry.tx = new WeakTransactionReference(tx, referenceQueue); + // Copy the previously announced peers into the confidence and then clear it out. + TransactionConfidence confidence = tx.getConfidence(); + for (PeerAddress a : entry.addresses) { + confidence.markBroadcastBy(a); + } + entry.addresses = null; + log.info("{}: Adding tx [{}] {} to the memory pool", + new Object[] { byPeer, confidence.numBroadcastPeers(), tx.getHashAsString() }); + return tx; + } + } else { + log.info("{}: Provided with a downloaded transaction we didn't see announced yet: {}", + byPeer, tx.getHashAsString()); + entry = new Entry(); + entry.tx = new WeakTransactionReference(tx, referenceQueue); + memoryPool.put(tx.getHash(), entry); + tx.getConfidence().markBroadcastBy(byPeer); + return tx; + } + } + + /** + * Called by peers when they see a transaction advertised in an "inv" message. It either will increase the + * confidence of the pre-existing transaction or will just keep a record of the address for future usage. + */ + public synchronized void seen(Sha256Hash hash, PeerAddress byPeer) { + cleanPool(); + Entry entry = memoryPool.get(hash); + if (entry != null) { + // This TX or its hash have been previously announced. + if (entry.tx != null) { + Preconditions.checkState(entry.addresses == null); + if (entry.tx.get() != null) { + Transaction tx = entry.tx.get(); + tx.getConfidence().markBroadcastBy(byPeer); + log.info("{}: Announced transaction we have seen before [{}] {}", + new Object[] { byPeer, tx.getConfidence().numBroadcastPeers(), tx.getHashAsString() }); + } else { + // The inv is telling us about a transaction that we previously downloaded, and threw away because + // nothing found it interesting enough to keep around. So do nothing. + } + } else { + Preconditions.checkNotNull(entry.addresses); + entry.addresses.add(byPeer); + log.info("{}: Announced transaction we have seen announced before [{}] {}", + new Object[] { byPeer, entry.addresses.size(), hash }); + } + } else { + // This TX has never been seen before. + entry = new Entry(); + // TODO: Using hashsets here is inefficient compared to just having an array. + entry.addresses = new HashSet(); + entry.addresses.add(byPeer); + memoryPool.put(hash, entry); + log.info("{}: Announced new transaction [1] {}", byPeer, hash); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/core/Peer.java b/core/src/main/java/com/google/bitcoin/core/Peer.java index 3e815e6d..271dc7fb 100644 --- a/core/src/main/java/com/google/bitcoin/core/Peer.java +++ b/core/src/main/java/com/google/bitcoin/core/Peer.java @@ -25,9 +25,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.*; /** @@ -49,8 +47,6 @@ public class Peer { // whilst waiting for the response. Synchronized on itself. Is not used for downloads Peer generates itself. // TODO: Make this work for transactions as well. private final List> pendingGetBlockFutures; - // Height of the chain advertised in the peers version message. - private int bestHeight; private PeerAddress address; private List eventListeners; // Whether to try and download blocks and transactions from this peer. Set to false by PeerGroup if not the @@ -60,26 +56,9 @@ public class Peer { // The version data to announce to the other side of the connections we make: useful for setting our "user agent" // equivalent and other things. private VersionMessage versionMessage; - - /** - * Size of the pending transactions pool. Override this to reduce memory usage on constrained platforms. The pool - * is used to keep track of how many peers announced a transaction. With an untampered-with internet connection, - * the more peers announce a transaction, the more confidence you can have that it's valid. - */ - public static int TRANSACTION_MEMORY_POOL_SIZE = 1000; - - // Maps announced transaction hashes to the Transaction objects. If this is not a download peer, the Transaction - // objects must be provided from elsewhere (ie, a PeerGroup object). If the Transaction hasn't been downloaded or - // provided yet, the map value is null. This is somewhat equivalent to the reference implementations memory pool. - private LinkedHashMap announcedTransactionHashes = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(Map.Entry sha256HashTransactionEntry) { - // An arbitrary choice to stop the memory used by tracked transactions getting too huge. Mobile platforms - // may want to reduce this. - return size() > TRANSACTION_MEMORY_POOL_SIZE; - } - }; - + // A class that tracks recent transactions that have been broadcast across the network, counts how many + // peers announced them and updates the transaction confidence data. It is passed to each Peer. + private MemoryPool memoryPool; // A time before which we only download block headers, after that point we download block bodies. private long fastCatchupTimeSecs; // Whether we are currently downloading headers only or block bodies. Defaults to true, if the fast catchup time @@ -138,6 +117,18 @@ public class Peer { return eventListeners.remove(listener); } + /** + * Tells the peer to insert received transactions/transaction announcements into the given {@link MemoryPool}. + * This is normally done for you by the {@link PeerGroup} so you don't have to think about it. Transactions stored + * in a memory pool will have their confidence levels updated when a peer announces it, to reflect the greater + * likelyhood that the transaction is valid. + * + * @param pool A new pool or null to unlink. + */ + public synchronized void setMemoryPool(MemoryPool pool) { + memoryPool = pool; + } + @Override public String toString() { if (address == null) { @@ -230,8 +221,8 @@ public class Peer { disconnect(); throw new PeerException(e); } catch (RuntimeException e) { + log.error("Unexpected exception in peer loop: ", e.getMessage()); disconnect(); - log.error("unexpected exception in peer loop: ", e.getMessage()); throw e; } @@ -314,6 +305,8 @@ public class Peer { private void processTransaction(Transaction m) { log.info("Received broadcast tx {}", m.getHashAsString()); + if (memoryPool != null) + memoryPool.seen(m, getAddress()); for (PeerEventListener listener : eventListeners) { synchronized (listener) { listener.onTransaction(this, m); @@ -377,7 +370,8 @@ public class Peer { private void processInv(InventoryMessage inv) throws IOException { // This should be called in the network loop thread for this peer. List items = inv.getItems(); - updateTransactionConfidenceLevels(items); + if (memoryPool != null) + updateTransactionConfidenceLevels(items); // If this peer isn't responsible for downloading stuff, don't go further. if (!downloadData) @@ -404,57 +398,11 @@ public class Peer { conn.writeMessage(getdata); } - /** - * When a peer broadcasts an "inv" containing a transaction hash, it means the peer validated it and won't accept - * double spends of those coins. So by measuring what proportion of our total connected peers have seen a - * transaction we can make a guesstimate of how likely it is to be included in a block, assuming our internet - * connection is trustworthy.

- * - * This method keeps a map of transaction hashes to {@link Transaction} objects. It may not have the associated - * transaction objects available, if they weren't downloaded yet. Once a Transaction is downloaded, it's set as - * the value in the txSeen map. If this Peer isn't the download peer, the {@link PeerGroup} will manage distributing - * the Transaction objects to every peer, at which point the peer is expected to update the - * {@link TransactionConfidence} object itself. - * - * @param items Inventory items that were just announced. - */ private void updateTransactionConfidenceLevels(List items) { - // Announced hashes may be updated by other threads in response to messages coming in from other peers. - synchronized (announcedTransactionHashes) { - for (InventoryItem item : items) { - if (item.type != InventoryItem.Type.Transaction) continue; - Transaction transaction = announcedTransactionHashes.get(item.hash); - if (transaction == null) { - // We didn't see this tx before. - log.debug("Newly announced undownloaded transaction ", item.hash); - announcedTransactionHashes.put(item.hash, null); - } else { - // It's been downloaded. Update the confidence levels. This may be called multiple times for - // the same transaction and the same peer, there is no obligation in the protocol to avoid - // redundant advertisements. - log.debug("Marking tx {} as seen by {}", item.hash, toString()); - transaction.getConfidence().markBroadcastBy(address); - } - } - } - } - - /** - * Called by {@link PeerGroup} to tell the Peer about a transaction that was just downloaded. If we have tracked - * the announcement, update the transactions confidence level at this time. Otherwise wait for it to appear. - */ - void trackTransaction(Transaction tx) { - // May run on arbitrary peer threads. - synchronized (announcedTransactionHashes) { - if (announcedTransactionHashes.containsKey(tx.getHash())) { - Transaction storedTx = announcedTransactionHashes.get(tx.getHash()); - Preconditions.checkState(storedTx == tx || storedTx == null, "single Transaction instance"); - log.debug("Provided with a downloaded transaction we have seen before: {}", tx.getHash()); - tx.getConfidence().markBroadcastBy(address); - } else { - log.debug("Provided with a downloaded transaction we didn't see broadcast yet: {}", tx.getHash()); - } - announcedTransactionHashes.put(tx.getHash(), tx); + Preconditions.checkNotNull(memoryPool); + for (InventoryItem item : items) { + if (item.type != InventoryItem.Type.Transaction) continue; + memoryPool.seen(item.hash, this.getAddress()); } } @@ -743,8 +691,8 @@ public class Peer { /** * @return the height of the best chain as claimed by peer. */ - public int getBestHeight() { - return bestHeight; + public long getBestHeight() { + return conn.getVersionMessage().bestHeight; } /** 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 9f251674..2dbf2d7d 100644 --- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java +++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java @@ -20,6 +20,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.discovery.PeerDiscovery; import com.google.bitcoin.discovery.PeerDiscoveryException; import com.google.bitcoin.utils.EventListenerInvoker; +import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +80,9 @@ public class PeerGroup { private Set peerDiscoverers; // The version message to use for new connections. private VersionMessage versionMessage; + // A class that tracks recent transactions that have been broadcast across the network, counts how many + // peers announced them and updates the transaction confidence data. It is passed to each Peer. + private MemoryPool memoryPool; private NetworkParameters params; private BlockChain chain; @@ -113,6 +117,8 @@ public class PeerGroup { // this is. this.versionMessage = new VersionMessage(params, chain.getBestChainHeight()); + this.memoryPool = new MemoryPool(); + inactives = new LinkedBlockingQueue(); // TODO: Remove usage of synchronized sets here in favor of simple coarse-grained locking. peers = Collections.synchronizedSet(new HashSet()); @@ -128,13 +134,6 @@ public class PeerGroup { // when we download a transaction, we can distribute it to each Peer in the pool so they can update the // transactions confidence level if they've seen it be announced/when they see it be announced. peerEventListeners = new ArrayList(); - addEventListener(new AbstractPeerEventListener() { - @Override - public void onTransaction(Peer peer, Transaction t) { - handleBroadcastTransaction(t); - } - }); - // This event listener is added to every peer. It's here so when we announce transactions via an "inv", every // peer can fetch them. getDataListener = new AbstractPeerEventListener() { @@ -160,14 +159,6 @@ public class PeerGroup { return new LinkedList(transactions.values()); } - private synchronized void handleBroadcastTransaction(Transaction tx) { - // Called on the download peer thread when we have downloaded an advertised Transaction. Distribute it to all - // the peers in the group so they can update the confidence if they saw it be advertised or when they do see it. - for (Peer p : peers) { - p.trackTransaction(tx); - } - } - /** * Sets the {@link VersionMessage} that will be announced on newly created connections. A version message is * primarily interesting because it lets you customize the "subVer" field which is used a bit like the User-Agent @@ -251,8 +242,7 @@ public class PeerGroup { */ public void addPeer(Peer peer) { synchronized (this) { - if (!running) - throw new IllegalStateException("Must call start() before adding peers."); + Preconditions.checkState(running, "Must call start() before adding peers."); log.info("Adding directly to group: {}", peer); } // This starts the peer thread running. Note: this is not synchronized. If it were, we could not @@ -648,7 +638,10 @@ public class PeerGroup { } protected synchronized void handleNewPeer(final Peer peer) { - log.info("Handling new {}", peer); + // Runs on a peer thread for every peer that is newly connected. + log.info("{}: New peer", peer); + // Link the peer to the memory pool so broadcast transactions have their confidence levels updated. + peer.setMemoryPool(memoryPool); // If we want to download the chain, and we aren't currently doing so, do so now. if (downloadListener != null && downloadPeer == null) { log.info(" starting block chain download"); @@ -658,10 +651,17 @@ public class PeerGroup { } else { peer.setDownloadData(false); } + // Make sure the peer knows how to upload transactions that are requested from us. + peer.addEventListener(getDataListener); // Now tell the peers about any transactions we have which didn't appear in the chain yet. These are not // necessarily spends we created. They may also be transactions broadcast across the network that we saw, // which are relevant to us, and which we therefore wish to help propagate (ie they send us coins). - peer.addEventListener(getDataListener); + // + // Note that this can cause a DoS attack against us if a malicious remote peer knows what keys we own, and + // then sends us fake relevant transactions. We'll attempt to relay the bad transactions, our badness score + // in the Satoshi client will increase and we'll get disconnected. + // + // TODO: Find a way to balance the desire to propagate useful transactions against obscure DoS attacks. announcePendingWalletTransactions(wallets, Collections.singleton(peer)); EventListenerInvoker.invoke(peerEventListeners, new EventListenerInvoker() { @Override diff --git a/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java b/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java index e1f1969d..55c07100 100644 --- a/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java +++ b/core/src/main/java/com/google/bitcoin/core/TransactionConfidence.java @@ -18,6 +18,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.utils.EventListenerInvoker; +import com.google.common.base.Preconditions; import java.io.Serializable; import java.math.BigInteger; @@ -66,6 +67,7 @@ public class TransactionConfidence implements Serializable { private Set broadcastBy; /** The Transaction that this confidence object is associated with. */ private Transaction transaction; + // Lazily created listeners array. private transient ArrayList listeners; // TODO: The advice below is a mess. There should be block chain listeners, see issue 94. @@ -80,16 +82,15 @@ public class TransactionConfidence implements Serializable { * confidence object to determine the new depth.

*/ public synchronized void addEventListener(Listener listener) { - if (listener == null) - throw new IllegalArgumentException("listener is null"); + Preconditions.checkNotNull(listener); if (listeners == null) listeners = new ArrayList(1); listeners.add(listener); } public synchronized void removeEventListener(Listener listener) { - if (listener == null) - throw new IllegalArgumentException("listener is null"); + Preconditions.checkNotNull(listener); + Preconditions.checkNotNull(listeners); listeners.remove(listener); } diff --git a/core/src/main/java/com/google/bitcoin/utils/EventListenerInvoker.java b/core/src/main/java/com/google/bitcoin/utils/EventListenerInvoker.java index 2f4f1d09..e2abfc26 100644 --- a/core/src/main/java/com/google/bitcoin/utils/EventListenerInvoker.java +++ b/core/src/main/java/com/google/bitcoin/utils/EventListenerInvoker.java @@ -39,6 +39,7 @@ public abstract class EventListenerInvoker { public static void invoke(List listeners, EventListenerInvoker invoker) { + if (listeners == null) return; synchronized (listeners) { for (int i = 0; i < listeners.size(); i++) { E l = listeners.get(i); diff --git a/core/src/test/java/com/google/bitcoin/core/MemoryPoolTest.java b/core/src/test/java/com/google/bitcoin/core/MemoryPoolTest.java new file mode 100644 index 00000000..2b23aed2 --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/core/MemoryPoolTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import com.google.bitcoin.utils.BriefLogFormatter; +import org.junit.Before; +import org.junit.Test; + +import java.net.InetAddress; + +import static org.junit.Assert.assertEquals; + +public class MemoryPoolTest { + private NetworkParameters params = NetworkParameters.unitTests(); + private Transaction tx1, tx2; + private PeerAddress address1, address2, address3; + + @Before + public void setup() throws Exception { + BriefLogFormatter.init(); + tx1 = TestUtils.createFakeTx(params, Utils.toNanoCoins(1, 0), new ECKey().toAddress(params)); + tx2 = new Transaction(params, tx1.bitcoinSerialize()); + + address1 = new PeerAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); + address2 = new PeerAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 2 })); + address3 = new PeerAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 3 })); + } + + @Test + public void canonicalInstance() throws Exception { + MemoryPool pool = new MemoryPool(); + // Check that if we repeatedly send it the same transaction but with different objects, we get back the same + // canonical instance with the confidences update. + assertEquals(0, pool.numBroadcastPeers(tx1.getHash())); + assertEquals(tx1, pool.seen(tx1, address1)); + assertEquals(1, tx1.getConfidence().numBroadcastPeers()); + assertEquals(1, pool.numBroadcastPeers(tx1.getHash())); + assertEquals(tx1, pool.seen(tx2, address2)); + assertEquals(2, tx1.getConfidence().numBroadcastPeers()); + assertEquals(2, pool.numBroadcastPeers(tx1.getHash())); + } + + @Test + public void invAndDownload() throws Exception { + MemoryPool pool = new MemoryPool(); + // Base case: we see a transaction announced twice and then download it. The count is in the confidence object. + assertEquals(0, pool.numBroadcastPeers(tx1.getHash())); + pool.seen(tx1.getHash(), address1); + assertEquals(1, pool.numBroadcastPeers(tx1.getHash())); + pool.seen(tx1.getHash(), address2); + assertEquals(2, pool.numBroadcastPeers(tx1.getHash())); + Transaction t = pool.seen(tx1, address1); + assertEquals(2, t.getConfidence().numBroadcastPeers()); + // And now we see another inv. + pool.seen(tx1.getHash(), address3); + assertEquals(3, t.getConfidence().numBroadcastPeers()); + assertEquals(3, pool.numBroadcastPeers(tx1.getHash())); + } +}