diff --git a/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java b/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java index a5aca7aa..6462c353 100644 --- a/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java +++ b/src/main/java/net/helix/hlx/network/TransactionRequesterWorker.java @@ -10,8 +10,10 @@ public interface TransactionRequesterWorker { /** * Works through the request queue by sending a request alongside a random tip to each of our neighbors.
+ * + * @return true when we have send the request to our neighbors, otherwise false */ - void processRequestQueue(); + boolean processRequestQueue(); /** * Starts the background worker that automatically calls {@link #processRequestQueue()} periodically to process the diff --git a/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java b/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java index 43d2f1b3..b8e12133 100644 --- a/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java +++ b/src/main/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImpl.java @@ -30,7 +30,7 @@ public class TransactionRequesterWorkerImpl implements TransactionRequesterWorke /** * The minimum amount of transactions in the request queue that are required for the worker to trigger.
*/ - private static final int REQUESTER_THREAD_ACTIVATION_THRESHOLD = 50; + public static final int REQUESTER_THREAD_ACTIVATION_THRESHOLD = 50; /** * The time (in milliseconds) that the worker waits between its iterations.
@@ -105,24 +105,42 @@ public TransactionRequesterWorkerImpl init(Tangle tangle, TransactionRequester t * traffic like transactions that get relayed by our node.
*/ @Override - public void processRequestQueue() { + public boolean processRequestQueue() { try { - if (transactionRequester.numberOfTransactionsToRequest() >= REQUESTER_THREAD_ACTIVATION_THRESHOLD) { + if (isActive()) { TransactionViewModel transaction = getTransactionToSendWithRequest(); - if (transaction != null && transaction.getType() != TransactionViewModel.PREFILLED_SLOT) { - for (Neighbor neighbor : node.getNeighbors()) { - try { - // automatically adds the hash of a requested transaction when sending a packet - node.sendPacket(transaction, neighbor); - } catch (Exception e) { - log.error("unexpected error while sending request to neighbour", e); - } - } + if (isValidTransaction(transaction)) { + sendToNodes(transaction); + return true; } } } catch (Exception e) { log.error("unexpected error while processing the request queue", e); } + return false; + } + + private void sendToNodes(TransactionViewModel transaction) { + for (Neighbor neighbor : node.getNeighbors()) { + try { + // automatically adds the hash of a requested transaction when sending a packet + node.sendPacket(transaction, neighbor); + } catch (Exception e) { + log.error("unexpected error while sending request to neighbour", e); + } + } + } + + //@VisibleForTesting + boolean isActive() { + return transactionRequester.numberOfTransactionsToRequest() >= REQUESTER_THREAD_ACTIVATION_THRESHOLD; + } + + //@VisibleForTesting + boolean isValidTransaction(TransactionViewModel transaction) { + return transaction != null && ( + transaction.getType() != TransactionViewModel.PREFILLED_SLOT + || transaction.getHash().equals(Hash.NULL_HASH)); } @Override @@ -145,7 +163,8 @@ public void shutdown() { * @return a random tip * @throws Exception if anything unexpected happens while trying to retrieve the random tip. */ - private TransactionViewModel getTransactionToSendWithRequest() throws Exception { + //@VisibleForTesting + TransactionViewModel getTransactionToSendWithRequest() throws Exception { Hash tip = tipsViewModel.getRandomSolidTipHash(); if (tip == null) { tip = tipsViewModel.getRandomNonSolidTipHash(); diff --git a/src/test/java/net/helix/hlx/TangleMockUtils.java b/src/test/java/net/helix/hlx/TangleMockUtils.java new file mode 100644 index 00000000..fe6d5855 --- /dev/null +++ b/src/test/java/net/helix/hlx/TangleMockUtils.java @@ -0,0 +1,50 @@ +package net.helix.hlx; + +import net.helix.hlx.controllers.TransactionViewModel; +import net.helix.hlx.model.Hash; +import net.helix.hlx.model.persistables.Transaction; +import net.helix.hlx.storage.Tangle; +import net.helix.hlx.utils.Pair; + +import org.mockito.Mockito; + + +public class TangleMockUtils { + + /** + * Creates an empty transaction, which is marked filled and parsed. + * This transaction is returned when the hash is asked to load in the tangle object + * + * @param tangle mocked tangle object that shall retrieve a milestone object when being queried for it + * @param hash transaction hash + * @return The newly created (empty) transaction + */ + public static Transaction mockTransaction(Tangle tangle, Hash hash) { + Transaction transaction = new Transaction(); + transaction.bytes = new byte[0]; + transaction.type = TransactionViewModel.FILLED_SLOT; + transaction.parsed = true; + + return mockTransaction(tangle, hash, transaction); + } + + /** + * Mocks the tangle object by checking for the hash and returning the transaction. + * + * @param tangle mocked tangle object that shall retrieve a milestone object when being queried for it + * @param hash transaction hash + * @param transaction the transaction we send back + * @return The transaction + */ + public static Transaction mockTransaction(Tangle tangle, Hash hash, Transaction transaction) { + try { + Mockito.when(tangle.load(Transaction.class, hash)).thenReturn(transaction); + Mockito.when(tangle.getLatest(Transaction.class, Hash.class)).thenReturn(new Pair<>(hash, transaction)); + } catch (Exception e) { + // the exception can not be raised since we mock + } + + return transaction; + } + +} diff --git a/src/test/java/net/helix/hlx/TransactionTestUtils.java b/src/test/java/net/helix/hlx/TransactionTestUtils.java index a6f93d13..1f3e9161 100644 --- a/src/test/java/net/helix/hlx/TransactionTestUtils.java +++ b/src/test/java/net/helix/hlx/TransactionTestUtils.java @@ -1,46 +1,82 @@ -package net.helix.hlx; - -import java.util.Random; - -import net.helix.hlx.controllers.TransactionViewModel; -import net.helix.hlx.crypto.SpongeFactory; -import net.helix.hlx.model.Hash; -import net.helix.hlx.model.TransactionHash; -import org.apache.commons.lang3.StringUtils; -import org.bouncycastle.util.encoders.Hex; - -public class TransactionTestUtils { - - private static final Random RND = new Random(); - - /** - * Generates a transaction with the provided hex string. - * Transaction hash is calculated and added. - * - * @param hex The transaction hex to use - * @return The transaction - */ - public static TransactionViewModel createTransactionWithHex(String hex) { - byte[] hbytes = Hex.decode(hex); - return new TransactionViewModel(hbytes, TransactionHash.calculate(SpongeFactory.Mode.S256, hbytes)); - } - - /** - * @param hex The hex to change. - * @return The changed hex - */ - public static String nextWord(String hex, int index) { - return pad(Integer.toHexString(index+1)); - } - - private static String pad(String hex) { - return StringUtils.rightPad(hex, TransactionViewModel.SIZE*2, '0'); - } - - public static Hash getTransactionHash() { - byte[] bytes = new byte[Hash.SIZE_IN_BYTES]; - RND.nextBytes(bytes); - return net.helix.hlx.model.HashFactory.TRANSACTION.create(bytes); - } - -} +package net.helix.hlx; + +import java.util.Random; + +import net.helix.hlx.controllers.TransactionViewModel; +import net.helix.hlx.crypto.SpongeFactory; +import net.helix.hlx.model.Hash; +import net.helix.hlx.model.TransactionHash; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.encoders.Hex; + +public class TransactionTestUtils { + + private static final Random RND = new Random(); + + /** + * Generates a transaction with the provided hex string. + * Transaction hash is calculated and added. + * + * @param hex The transaction hex to use + * @return The transaction + */ + public static TransactionViewModel createTransactionWithHex(String hex) { + byte[] hbytes = Hex.decode(hex); + return new TransactionViewModel(hbytes, TransactionHash.calculate(SpongeFactory.Mode.S256, hbytes)); + } + + /** + * @param hex The hex to change. + * @return The changed hex + */ + public static String nextWord(String hex, int index) { + return pad(Integer.toHexString(index+1)); + } + + private static String pad(String hex) { + return StringUtils.rightPad(hex, TransactionViewModel.SIZE*2, '0'); + } + + public static Hash getTransactionHash() { + byte[] bytes = new byte[Hash.SIZE_IN_BYTES]; + RND.nextBytes(bytes); + return net.helix.hlx.model.HashFactory.TRANSACTION.create(bytes); + } + + /** + * Generates a transaction. + * + * @return The transaction + */ + public static Transaction getTransaction() { + byte[] bytes = new byte[TransactionViewModel.SIZE]; + RND.nextBytes(bytes); + return buildTransaction(bytes); + } + + /** + * Generates a transaction with only 0s. + * + * @return The transaction + */ + public static Transaction get0Transaction() { + byte[] bytes = new byte[TransactionViewModel.SIZE]; + return buildTransaction(bytes); + } + + /** + * Builds a transaction from the bytes. + * Make sure the bytes are in the correct order + * + * @param bytes The bytes to build the transaction + * @return The created transaction + */ + public static Transaction buildTransaction(byte[] bytes) { + Transaction t = new Transaction(); + t.read(bytes); + t.readMetadata(bytes); + return t; + } + +} diff --git a/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java b/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java new file mode 100644 index 00000000..b2b70082 --- /dev/null +++ b/src/test/java/net/helix/hlx/network/impl/TransactionRequesterWorkerImplTest.java @@ -0,0 +1,148 @@ +package net.helix.hlx.network.impl; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import static org.mockito.Mockito.when; + +import net.helix.hlx.controllers.TipsViewModel; +import net.helix.hlx.controllers.TransactionViewModel; +import net.helix.hlx.model.Hash; +import net.helix.hlx.model.persistables.Transaction; +import net.helix.hlx.network.Node; +import net.helix.hlx.network.TransactionRequester; +import net.helix.hlx.service.snapshot.SnapshotProvider; +import net.helix.hlx.storage.Tangle; + +import net.helix.hlx.TangleMockUtils; +import static net.helix.hlx.TransactionTestUtils.getTransaction; +import static net.helix.hlx.TransactionTestUtils.get0Transaction; +import static net.helix.hlx.TransactionTestUtils.buildTransaction; +import static net.helix.hlx.TransactionTestUtils.getTransactionHash; + + +public class TransactionRequesterWorkerImplTest { + + //Good + private static final TransactionViewModel TVMRandomNull = new TransactionViewModel( + getTransaction(), Hash.NULL_HASH); + private static final TransactionViewModel TVMRandomNotNull = new TransactionViewModel( + getTransaction(), getTransactionHash()); + private static final TransactionViewModel TVMAll0Null = new TransactionViewModel( + get0Transaction(), Hash.NULL_HASH); + private static final TransactionViewModel TVMAll0NotNull = new TransactionViewModel( + get0Transaction(), getTransactionHash()); + + //Bad + private static final TransactionViewModel TVMNullNull = new TransactionViewModel((Transaction)null, Hash.NULL_HASH); + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private static SnapshotProvider snapshotProvider; + + private static TransactionRequester requester; + private static TransactionRequesterWorkerImpl worker; + + @Mock + private Tangle tangle; + + @Mock + private Node node; + + @Mock + private TipsViewModel tipsVM; + + @Before + public void before() { + requester = new TransactionRequester(tangle, snapshotProvider); + + worker = new TransactionRequesterWorkerImpl(); + worker.init(tangle, requester, tipsVM, node); + } + + @After + public void tearDown() { + worker.shutdown(); + } + + @Test + public void workerActive() throws Exception { + assertFalse("Empty worker should not be active", worker.isActive()); + + fillRequester(); + + assertTrue("Worker should be active when it requester is over threshold", worker.isActive()); + } + + @Test + public void processRequestQueueTest() throws Exception { + //getTransactionToSendWithRequest starts reading from solid tips, so mock data from that call + when(tipsVM.getRandomSolidTipHash()).thenReturn( + TVMRandomNull.getHash(), + TVMRandomNotNull.getHash(), + TVMAll0Null.getHash(), + TVMAll0NotNull.getHash(), + TVMNullNull.getHash(), + null); + + assertFalse("Unfilled queue shouldnt process", worker.processRequestQueue()); + + //Requester never goes down since nodes don't really request + fillRequester(); + + TangleMockUtils.mockTransaction(tangle, TVMRandomNull.getHash(), buildTransaction(TVMRandomNull.getBytes())); + assertTrue("Null transaction hash should be processed", worker.processRequestQueue()); + + TangleMockUtils.mockTransaction(tangle, TVMRandomNotNull.getHash(), buildTransaction(TVMRandomNotNull.getBytes())); + assertTrue("Not null transaction hash should be processed", worker.processRequestQueue()); + + TangleMockUtils.mockTransaction(tangle, TVMAll0Null.getHash(), buildTransaction(TVMAll0Null.getBytes())); + assertTrue("Null transaction hash should be processed", worker.processRequestQueue()); + + TangleMockUtils.mockTransaction(tangle, TVMAll0NotNull.getHash(), buildTransaction(TVMAll0NotNull.getBytes())); + assertTrue("All 9s transaction should be processed", worker.processRequestQueue()); + + // Null gets loaded as all 0, so type is 0 -> Filled + TangleMockUtils.mockTransaction(tangle, TVMNullNull.getHash(), null); + assertTrue("0 transaction should be processed", worker.processRequestQueue()); + + // null -> NULL_HASH -> gets loaded as all 0 -> filled + assertTrue("Null transaction should be processed", worker.processRequestQueue()); + } + + @Test + public void validTipToAddTest() throws Exception { + assertTrue("Null transaction hash should always be accepted", worker.isValidTransaction(TVMRandomNull)); + assertTrue("Not null transaction hash should always be accepted", worker.isValidTransaction(TVMRandomNotNull)); + assertTrue("Null transaction hash should always be accepted", worker.isValidTransaction(TVMAll0Null)); + assertTrue("All 9s transaction should be accepted", worker.isValidTransaction(TVMAll0NotNull)); + + // Null gets loaded as all 0, so type is 0 -> Filled + assertTrue("0 transaction should be accepted", worker.isValidTransaction(TVMNullNull)); + + assertFalse("Null transaction should not be accepted", worker.isValidTransaction(null)); + } + + private void fillRequester() throws Exception { + for (int i=0; i< TransactionRequesterWorkerImpl.REQUESTER_THREAD_ACTIVATION_THRESHOLD; i++) { + addRequest(); + } + } + + private void addRequest() throws Exception { + Hash randomHash = getTransactionHash(); + TangleMockUtils.mockTransaction(tangle, randomHash); + requester.requestTransaction(randomHash, false); + } + +}