Peer: Configure maximum recursion level when requesting dependent transactions.

The idea is to limit download to a sane amount, rather than disabling it completely.
This commit is contained in:
Andreas Schildbach
2016-02-09 21:50:09 +01:00
parent 4217a5c231
commit afffd8b2c7
3 changed files with 101 additions and 39 deletions

View File

@@ -83,8 +83,8 @@ public class Peer extends PeerSocketHandler {
// 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 final VersionMessage versionMessage;
// Switch for enabling download of pending transaction dependencies.
private volatile boolean vDownloadTxDependencies;
// Maximum depth up to which pending transaction dependencies are downloaded, or 0 for disabled.
private volatile int vDownloadTxDependencyDepth;
// How many block messages the peer has announced to us. Peers only announce blocks that attach to their best chain
// so we can use this to calculate the height of the peers chain, by adding it to the initial height in the version
// message. This method can go wrong if the peer re-orgs onto a shorter (but harder) chain, however, this is rare.
@@ -191,7 +191,7 @@ public class Peer extends PeerSocketHandler {
*/
public Peer(NetworkParameters params, VersionMessage ver, PeerAddress remoteAddress,
@Nullable AbstractBlockChain chain) {
this(params, ver, remoteAddress, chain, true);
this(params, ver, remoteAddress, chain, Integer.MAX_VALUE);
}
/**
@@ -209,11 +209,11 @@ public class Peer extends PeerSocketHandler {
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
*/
public Peer(NetworkParameters params, VersionMessage ver, PeerAddress remoteAddress,
@Nullable AbstractBlockChain chain, boolean downloadTxDependencies) {
@Nullable AbstractBlockChain chain, int downloadTxDependencyDepth) {
super(params, remoteAddress);
this.params = Preconditions.checkNotNull(params);
this.versionMessage = Preconditions.checkNotNull(ver);
this.vDownloadTxDependencies = chain != null && downloadTxDependencies;
this.vDownloadTxDependencyDepth = chain != null ? downloadTxDependencyDepth : 0;
this.blockChain = chain; // Allowed to be null.
this.vDownloadData = chain != null;
this.getDataFutures = new CopyOnWriteArrayList<GetDataRequest>();
@@ -754,7 +754,7 @@ public class Peer extends PeerSocketHandler {
for (final Wallet wallet : wallets) {
try {
if (wallet.isPendingTransactionRelevant(tx)) {
if (vDownloadTxDependencies) {
if (vDownloadTxDependencyDepth > 0) {
// This transaction seems interesting to us, so let's download its dependencies. This has
// several purposes: we can check that the sender isn't attacking us by engaging in protocol
// abuse games, like depending on a time-locked transaction that will never confirm, or
@@ -836,7 +836,8 @@ public class Peer extends PeerSocketHandler {
log.info("{}: Downloading dependencies of {}", getAddress(), tx.getHashAsString());
final LinkedList<Transaction> results = new LinkedList<Transaction>();
// future will be invoked when the entire dependency tree has been walked and the results compiled.
final ListenableFuture<Object> future = downloadDependenciesInternal(tx, new Object(), results);
final ListenableFuture<Object> future = downloadDependenciesInternal(vDownloadTxDependencyDepth, 0, tx,
new Object(), results);
final SettableFuture<List<Transaction>> resultFuture = SettableFuture.create();
Futures.addCallback(future, new FutureCallback<Object>() {
@Override
@@ -853,9 +854,9 @@ public class Peer extends PeerSocketHandler {
}
// The marker object in the future returned is the same as the parameter. It is arbitrary and can be anything.
protected ListenableFuture<Object> downloadDependenciesInternal(final Transaction tx,
final Object marker,
final List<Transaction> results) {
protected ListenableFuture<Object> downloadDependenciesInternal(final int maxDepth, final int depth,
final Transaction tx, final Object marker, final List<Transaction> results) {
final SettableFuture<Object> resultFuture = SettableFuture.create();
final Sha256Hash rootTxHash = tx.getHash();
// We want to recursively grab its dependencies. This is so listeners can learn important information like
@@ -874,7 +875,7 @@ public class Peer extends PeerSocketHandler {
List<ListenableFuture<Transaction>> futures = Lists.newArrayList();
GetDataMessage getdata = new GetDataMessage(params);
if (needToRequest.size() > 1)
log.info("{}: Requesting {} transactions for dep resolution", getAddress(), needToRequest.size());
log.info("{}: Requesting {} transactions for depth {} dep resolution", getAddress(), needToRequest.size(), depth + 1);
for (Sha256Hash hash : needToRequest) {
getdata.addTransaction(hash);
GetDataRequest req = new GetDataRequest(hash, SettableFuture.create());
@@ -893,7 +894,8 @@ public class Peer extends PeerSocketHandler {
log.info("{}: Downloaded dependency of {}: {}", getAddress(), rootTxHash, tx.getHashAsString());
results.add(tx);
// Now recurse into the dependencies of this transaction too.
childFutures.add(downloadDependenciesInternal(tx, marker, results));
if (depth + 1 < maxDepth)
childFutures.add(downloadDependenciesInternal(maxDepth, depth + 1, tx, marker, results));
}
if (childFutures.size() == 0) {
// Short-circuit: we're at the bottom of this part of the tree.
@@ -1792,7 +1794,7 @@ public class Peer extends PeerSocketHandler {
* to try and discover if a pending tx might be at risk of double spending.
*/
public boolean isDownloadTxDependencies() {
return vDownloadTxDependencies;
return vDownloadTxDependencyDepth > 0;
}
/**
@@ -1800,7 +1802,16 @@ public class Peer extends PeerSocketHandler {
* before handing the transaction off to the wallet. The wallet can do risk analysis on pending/recent transactions
* to try and discover if a pending tx might be at risk of double spending.
*/
public void setDownloadTxDependencies(boolean value) {
vDownloadTxDependencies = value;
public void setDownloadTxDependencies(boolean enable) {
vDownloadTxDependencyDepth = enable ? Integer.MAX_VALUE : 0;
}
/**
* Sets if this peer will use getdata/notfound messages to walk backwards through transaction dependencies
* before handing the transaction off to the wallet. The wallet can do risk analysis on pending/recent transactions
* to try and discover if a pending tx might be at risk of double spending.
*/
public void setDownloadTxDependencies(int depth) {
vDownloadTxDependencyDepth = depth;
}
}

View File

@@ -140,8 +140,8 @@ public class PeerGroup implements TransactionBroadcaster {
private final CopyOnWriteArraySet<PeerDiscovery> peerDiscoverers;
// The version message to use for new connections.
@GuardedBy("lock") private VersionMessage versionMessage;
// Switch for enabling download of pending transaction dependencies.
@GuardedBy("lock") protected boolean downloadTxDependencies;
// Maximum depth up to which pending transaction dependencies are downloaded, or 0 for disabled.
@GuardedBy("lock") private int downloadTxDependencyDepth;
// How many connections we want to have open at the current time. If we lose connections, we'll try opening more
// until we reach this count.
@GuardedBy("lock") private int maxConnections;
@@ -413,7 +413,7 @@ public class PeerGroup implements TransactionBroadcaster {
// We never request that the remote node wait for a bloom filter yet, as we have no wallets
versionMessage.relayTxesBeforeFilter = true;
downloadTxDependencies = true;
downloadTxDependencyDepth = Integer.MAX_VALUE;
inactives = new PriorityQueue<PeerAddress>(1, new Comparator<PeerAddress>() {
@SuppressWarnings("FieldAccessNotGuarded") // only called when inactives is accessed, and lock is held then.
@@ -485,13 +485,13 @@ public class PeerGroup implements TransactionBroadcaster {
}
/**
* Switch for enabling download of pending transaction dependencies. A change of value only takes effect for newly
* connected peers.
* Configure download of pending transaction dependencies. A change of values only takes effect for newly connected
* peers.
*/
public void setDownloadTxDependencies(boolean downloadTxDependencies) {
public void setDownloadTxDependencies(int depth) {
lock.lock();
try {
this.downloadTxDependencies = downloadTxDependencies;
this.downloadTxDependencyDepth = depth;
} finally {
lock.unlock();
}
@@ -1511,7 +1511,7 @@ public class PeerGroup implements TransactionBroadcaster {
/** You can override this to customise the creation of {@link Peer} objects. */
@GuardedBy("lock")
protected Peer createPeer(PeerAddress address, VersionMessage ver) {
return new Peer(params, ver, address, chain, downloadTxDependencies);
return new Peer(params, ver, address, chain, downloadTxDependencyDepth);
}
/**

View File

@@ -554,8 +554,7 @@ public class PeerTest extends TestWithNetworkConnections {
@Test
public void recursiveDependencyDownload() throws Exception {
// Using ping or notfound?
connectWithVersion(70001, VersionMessage.NODE_NETWORK);
connect();
// Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
ECKey to = new ECKey();
@@ -574,9 +573,9 @@ public class PeerTest extends TestWithNetworkConnections {
// -> [t8]
// The ones in brackets are assumed to be in the chain and are represented only by hashes.
Transaction t2 = FakeTxBuilder.createFakeTx(params, COIN, to);
Sha256Hash t5 = t2.getInput(0).getOutpoint().getHash();
Sha256Hash t5hash = t2.getInput(0).getOutpoint().getHash();
Transaction t4 = FakeTxBuilder.createFakeTx(params, COIN, new ECKey());
Sha256Hash t6 = t4.getInput(0).getOutpoint().getHash();
Sha256Hash t6hash = t4.getInput(0).getOutpoint().getHash();
t4.addOutput(COIN, new ECKey());
Transaction t3 = new Transaction(params);
t3.addInput(t4.getOutput(0));
@@ -584,10 +583,10 @@ public class PeerTest extends TestWithNetworkConnections {
Transaction t1 = new Transaction(params);
t1.addInput(t2.getOutput(0));
t1.addInput(t3.getOutput(0));
Sha256Hash someHash = Sha256Hash.wrap("2b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6");
t1.addInput(new TransactionInput(params, t1, new byte[]{}, new TransactionOutPoint(params, 0, someHash)));
Sha256Hash anotherHash = Sha256Hash.wrap("3b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6");
t1.addInput(new TransactionInput(params, t1, new byte[]{}, new TransactionOutPoint(params, 1, anotherHash)));
Sha256Hash t7hash = Sha256Hash.wrap("2b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6");
t1.addInput(new TransactionInput(params, t1, new byte[]{}, new TransactionOutPoint(params, 0, t7hash)));
Sha256Hash t8hash = Sha256Hash.wrap("3b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6");
t1.addInput(new TransactionInput(params, t1, new byte[]{}, new TransactionOutPoint(params, 1, t8hash)));
t1.addOutput(COIN, to);
t1 = FakeTxBuilder.roundTripTransaction(params, t1);
t2 = FakeTxBuilder.roundTripTransaction(params, t2);
@@ -607,19 +606,19 @@ public class PeerTest extends TestWithNetworkConnections {
// We want its dependencies so ask for them.
ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1);
assertFalse(futures.isDone());
// It will recursively ask for the dependencies of t1: t2, t3, someHash and anotherHash.
// It will recursively ask for the dependencies of t1: t2, t3, t7, t8.
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(4, getdata.getItems().size());
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
assertEquals(t3.getHash(), getdata.getItems().get(1).hash);
assertEquals(someHash, getdata.getItems().get(2).hash);
assertEquals(anotherHash, getdata.getItems().get(3).hash);
assertEquals(t7hash, getdata.getItems().get(2).hash);
assertEquals(t8hash, getdata.getItems().get(3).hash);
// Deliver the requested transactions.
inbound(writeTarget, t2);
inbound(writeTarget, t3);
NotFoundMessage notFound = new NotFoundMessage(params);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, someHash));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, anotherHash));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t7hash));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t8hash));
inbound(writeTarget, notFound);
assertFalse(futures.isDone());
// It will recursively ask for the dependencies of t2: t5 and t4, but not t3 because it already found t4.
@@ -627,7 +626,7 @@ public class PeerTest extends TestWithNetworkConnections {
assertEquals(getdata.getItems().get(0).hash, t2.getInput(0).getOutpoint().getHash());
// t5 isn't found and t4 is.
notFound = new NotFoundMessage(params);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5hash));
inbound(writeTarget, notFound);
assertFalse(futures.isDone());
// Request t4 ...
@@ -636,19 +635,71 @@ public class PeerTest extends TestWithNetworkConnections {
inbound(writeTarget, t4);
// Continue to explore the t4 branch and ask for t6, which is in the chain.
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(t6, getdata.getItems().get(0).hash);
assertEquals(t6hash, getdata.getItems().get(0).hash);
notFound = new NotFoundMessage(params);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6hash));
inbound(writeTarget, notFound);
pingAndWait(writeTarget);
// That's it, we explored the entire tree.
assertTrue(futures.isDone());
List<Transaction> results = futures.get();
assertEquals(3, results.size());
assertTrue(results.contains(t2));
assertTrue(results.contains(t3));
assertTrue(results.contains(t4));
}
@Test
public void recursiveDependencyDownload_depthLimited() throws Exception {
peer.setDownloadTxDependencies(1); // Depth limit
connect();
// Make some fake transactions in the following graph:
// t1 -> t2 -> t3 -> [t4]
// The ones in brackets are assumed to be in the chain and are represented only by hashes.
Sha256Hash t4hash = Sha256Hash.wrap("2b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6");
Transaction t3 = new Transaction(params);
t3.addInput(new TransactionInput(params, t3, new byte[]{}, new TransactionOutPoint(params, 0, t4hash)));
t3.addOutput(COIN, new ECKey());
t3 = FakeTxBuilder.roundTripTransaction(params, t3);
Transaction t2 = new Transaction(params);
t2.addInput(t3.getOutput(0));
t2.addOutput(COIN, new ECKey());
t2 = FakeTxBuilder.roundTripTransaction(params, t2);
Transaction t1 = new Transaction(params);
t1.addInput(t2.getOutput(0));
t1.addOutput(COIN, new ECKey());
t1 = FakeTxBuilder.roundTripTransaction(params, t1);
// Announce the first one. Wait for it to be downloaded.
InventoryMessage inv = new InventoryMessage(params);
inv.addTransaction(t1);
inbound(writeTarget, inv);
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
Threading.waitForUserCode();
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
inbound(writeTarget, t1);
pingAndWait(writeTarget);
// We want its dependencies so ask for them.
ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1);
assertFalse(futures.isDone());
// level 1
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(1, getdata.getItems().size());
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
inbound(writeTarget, t2);
// no level 2
getdata = (GetDataMessage) outbound(writeTarget);
assertNull(getdata);
// That's it, now double check what we've got
pingAndWait(writeTarget);
assertTrue(futures.isDone());
List<Transaction> results = futures.get();
assertEquals(1, results.size());
assertTrue(results.contains(t2));
}
@Test
public void timeLockedTransactionNew() throws Exception {
connectWithVersion(70001, VersionMessage.NODE_NETWORK);