diff --git a/build.gradle b/build.gradle index dc918be..0f6cf26 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ subprojects { sourceCompatibility = 1.7 group = 'ch.dissem.jabit' - version = '1.0.0' + version = '1.0.1' ext.isReleaseVersion = !version.endsWith("SNAPSHOT") @@ -34,7 +34,7 @@ subprojects { } signing { - required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") } + required { isReleaseVersion && project.getProperties().get("signing.keyId")?.length() > 0 } sign configurations.archives } diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 28e8483..c99acfb 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -17,20 +17,22 @@ package ch.dissem.bitmessage; import ch.dissem.bitmessage.entity.*; -import ch.dissem.bitmessage.entity.payload.*; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; -import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Property; +import ch.dissem.bitmessage.utils.TTL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; -import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.*; @@ -38,9 +40,7 @@ import java.util.concurrent.*; import static ch.dissem.bitmessage.entity.Plaintext.Status.*; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; -import static ch.dissem.bitmessage.utils.UnixTime.HOUR; -import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static ch.dissem.bitmessage.utils.UnixTime.*; /** *
Use this class if you want to create a Bitmessage client.
@@ -122,67 +122,24 @@ public class BitmessageContext { } public void broadcast(final BitmessageAddress from, final String subject, final String message) { - pool.submit(new Runnable() { - @Override - public void run() { - Plaintext msg = new Plaintext.Builder(BROADCAST) - .from(from) - .message(subject, message) - .build(); - - LOG.info("Sending message."); - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - from, - from, - Factory.getBroadcast(from, msg), - +2 * DAY - ); - msg.setStatus(SENT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.BROADCAST, Label.Type.SENT)); - ctx.getMessageRepository().save(msg); - } - }); + Plaintext msg = new Plaintext.Builder(BROADCAST) + .from(from) + .message(subject, message) + .build(); + send(msg); } public void send(final BitmessageAddress from, final BitmessageAddress to, final String subject, final String message) { if (from.getPrivateKey() == null) { throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); } - pool.submit(new Runnable() { - @Override - public void run() { - Plaintext msg = new Plaintext.Builder(MSG) - .from(from) - .to(to) - .message(subject, message) - .labels(messages().getLabels(Label.Type.SENT)) - .build(); - if (to.getPubkey() == null) { - tryToFindMatchingPubkey(to); - } - if (to.getPubkey() == null) { - LOG.info("Public key is missing from recipient. Requesting."); - requestPubkey(from, to); - msg.setStatus(PUBKEY_REQUESTED); - ctx.getMessageRepository().save(msg); - } else { - LOG.info("Sending message."); - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - from, - to, - new Msg(msg), - +2 * DAY - ); - msg.setStatus(SENT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); - ctx.getMessageRepository().save(msg); - } - } - }); + Plaintext msg = new Plaintext.Builder(MSG) + .from(from) + .to(to) + .message(subject, message) + .labels(messages().getLabels(Label.Type.SENT)) + .build(); + send(msg); } public void send(final Plaintext msg) { @@ -193,15 +150,18 @@ public class BitmessageContext { @Override public void run() { BitmessageAddress to = msg.getTo(); - if (to.getPubkey() == null) { - tryToFindMatchingPubkey(to); + if (to != null) { + if (to.getPubkey() == null) { + LOG.info("Public key is missing from recipient. Requesting."); + ctx.requestPubkey(to); + } + if (to.getPubkey() == null) { + msg.setStatus(PUBKEY_REQUESTED); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX)); + ctx.getMessageRepository().save(msg); + } } - if (to.getPubkey() == null) { - LOG.info("Public key is missing from recipient. Requesting."); - requestPubkey(msg.getFrom(), to); - msg.setStatus(PUBKEY_REQUESTED); - ctx.getMessageRepository().save(msg); - } else { + if (to == null || to.getPubkey() != null) { LOG.info("Sending message."); msg.setStatus(DOING_PROOF_OF_WORK); ctx.getMessageRepository().save(msg); @@ -209,7 +169,7 @@ public class BitmessageContext { msg.getFrom(), to, new Msg(msg), - +2 * DAY + TTL.msg() ); msg.setStatus(SENT); msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); @@ -219,15 +179,6 @@ public class BitmessageContext { }); } - private void requestPubkey(BitmessageAddress requestingIdentity, BitmessageAddress address) { - ctx.send( - requestingIdentity, - address, - new GetPubkey(address), - +28 * DAY - ); - } - public void startup() { ctx.getNetworkHandler().start(networkListener); } @@ -280,41 +231,11 @@ public class BitmessageContext { public void addContact(BitmessageAddress contact) { ctx.getAddressRepository().save(contact); - tryToFindMatchingPubkey(contact); if (contact.getPubkey() == null) { ctx.requestPubkey(contact); } } - private void tryToFindMatchingPubkey(BitmessageAddress address) { - for (ObjectMessage object : ctx.getInventory().getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { - try { - Pubkey pubkey = (Pubkey) object.getPayload(); - if (address.getVersion() == 4) { - V4Pubkey v4Pubkey = (V4Pubkey) pubkey; - if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { - v4Pubkey.decrypt(address.getPublicDecryptionKey()); - if (object.isSignatureValid(v4Pubkey)) { - address.setPubkey(v4Pubkey); - ctx.getAddressRepository().save(address); - break; - } else { - LOG.info("Found pubkey for " + address + " but signature is invalid"); - } - } - } else { - if (Arrays.equals(pubkey.getRipe(), address.getRipe())) { - address.setPubkey(pubkey); - ctx.getAddressRepository().save(address); - break; - } - } - } catch (Exception e) { - LOG.debug(e.getMessage(), e); - } - } - } - public void addSubscribtion(BitmessageAddress address) { address.setSubscribed(true); ctx.getAddressRepository().save(address); @@ -368,7 +289,6 @@ public class BitmessageContext { int connectionLimit = 150; long connectionTTL = 30 * MINUTE; boolean sendPubkeyOnIdentityCreation = true; - long pubkeyTTL = 28; public Builder() { } @@ -463,7 +383,7 @@ public class BitmessageContext { */ public Builder pubkeyTTL(long days) { if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days"); - this.pubkeyTTL = days; + TTL.pubkey(days); return this; } diff --git a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java index 9971b9b..bfd4f30 100644 --- a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java @@ -19,16 +19,16 @@ package ch.dissem.bitmessage; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Encrypted; import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.payload.Broadcast; -import ch.dissem.bitmessage.entity.payload.GetPubkey; -import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TTL; import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Arrays; import java.util.TreeSet; /** @@ -59,7 +59,6 @@ public class InternalContext { private final long clientNonce; private final long networkNonceTrialsPerByte = 1000; private final long networkExtraBytes = 1000; - private final long pubkeyTTL; private long connectionTTL; private int connectionLimit; @@ -79,7 +78,6 @@ public class InternalContext { this.port = builder.port; this.connectionLimit = builder.connectionLimit; this.connectionTTL = builder.connectionTTL; - this.pubkeyTTL = builder.pubkeyTTL; Singleton.initialize(cryptography); @@ -195,7 +193,7 @@ public class InternalContext { public void sendPubkey(final BitmessageAddress identity, final long targetStream) { try { - long expires = UnixTime.now(pubkeyTTL); + long expires = UnixTime.now(TTL.pubkey()); LOG.info("Expires at " + expires); final ObjectMessage response = new ObjectMessage.Builder() .stream(targetStream) @@ -212,16 +210,71 @@ public class InternalContext { } } + /** + * Be aware that if the pubkey already exists in the inventory, the metods will not request it and the callback + * for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB. + */ public void requestPubkey(final BitmessageAddress contact) { - long expires = UnixTime.now(+pubkeyTTL); + BitmessageAddress stored = addressRepository.getAddress(contact.getAddress()); + + tryToFindMatchingPubkey(contact); + if (contact.getPubkey() != null) { + if (stored != null) { + stored.setPubkey(contact.getPubkey()); + addressRepository.save(stored); + } else { + addressRepository.save(contact); + } + return; + } + + if (stored == null) { + addressRepository.save(contact); + } + + long expires = UnixTime.now(TTL.getpubkey()); LOG.info("Expires at " + expires); - final ObjectMessage response = new ObjectMessage.Builder() + final ObjectMessage request = new ObjectMessage.Builder() .stream(contact.getStream()) .expiresTime(expires) .payload(new GetPubkey(contact)) .build(); - messageCallback.proofOfWorkStarted(response.getPayload()); - proofOfWorkService.doProofOfWork(response); + messageCallback.proofOfWorkStarted(request.getPayload()); + proofOfWorkService.doProofOfWork(request); + } + + private void tryToFindMatchingPubkey(BitmessageAddress address) { + BitmessageAddress stored = addressRepository.getAddress(address.getAddress()); + if (stored != null) { + address.setAlias(stored.getAlias()); + address.setSubscribed(stored.isSubscribed()); + } + for (ObjectMessage object : inventory.getObjects(address.getStream(), address.getVersion(), ObjectType.PUBKEY)) { + try { + Pubkey pubkey = (Pubkey) object.getPayload(); + if (address.getVersion() == 4) { + V4Pubkey v4Pubkey = (V4Pubkey) pubkey; + if (Arrays.equals(address.getTag(), v4Pubkey.getTag())) { + v4Pubkey.decrypt(address.getPublicDecryptionKey()); + if (object.isSignatureValid(v4Pubkey)) { + address.setPubkey(v4Pubkey); + addressRepository.save(address); + break; + } else { + LOG.info("Found pubkey for " + address + " but signature is invalid"); + } + } + } else { + if (Arrays.equals(pubkey.getRipe(), address.getRipe())) { + address.setPubkey(pubkey); + addressRepository.save(address); + break; + } + } + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + } } public long getClientNonce() { diff --git a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java index 19e231f..ecd1099 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java +++ b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java @@ -4,6 +4,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; @@ -42,10 +43,10 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC } public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) { - long nonceTrialsPerByte = recipient == null ? - ctx.getNetworkNonceTrialsPerByte() : recipient.getPubkey().getNonceTrialsPerByte(); - long extraBytes = recipient == null ? - ctx.getNetworkExtraBytes() : recipient.getPubkey().getExtraBytes(); + Pubkey pubkey = recipient == null ? null : recipient.getPubkey(); + + long nonceTrialsPerByte = pubkey == null ? ctx.getNetworkNonceTrialsPerByte() : pubkey.getNonceTrialsPerByte(); + long extraBytes = pubkey == null ? ctx.getNetworkExtraBytes() : pubkey.getExtraBytes(); powRepo.putObject(object, nonceTrialsPerByte, extraBytes); if (object.getPayload() instanceof PlaintextHolder) { diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java index 7c37973..02f0384 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java @@ -79,6 +79,7 @@ public class Label implements Serializable { INBOX, BROADCAST, DRAFT, + OUTBOX, SENT, UNREAD, TRASH diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java index b1ace02..9d0c078 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Numbers.java @@ -1,7 +1,7 @@ package ch.dissem.bitmessage.utils; /** - * Created by chrig on 07.12.2015. + * @author Christian Basler */ public class Numbers { public static long max(long a, long b) { diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java new file mode 100644 index 0000000..17e31e5 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java @@ -0,0 +1,40 @@ +package ch.dissem.bitmessage.utils; + +import static ch.dissem.bitmessage.utils.UnixTime.DAY; + +/** + * Stores times to live for different object types. Usually this shouldn't be messed with, + * but for tests it might be a good idea to reduce it to a minimum, and on mobile clients + * you might want to optimize it as well. + * + * @author Christian Basler + */ +public class TTL { + private static long msg = 2 * DAY; + private static long getpubkey = 2 * DAY; + private static long pubkey = 28 * DAY; + + public static long msg() { + return msg; + } + + public static void msg(long msg) { + TTL.msg = msg; + } + + public static long getpubkey() { + return getpubkey; + } + + public static void getpubkey(long getpubkey) { + TTL.getpubkey = getpubkey; + } + + public static long pubkey() { + return pubkey; + } + + public static void pubkey(long pubkey) { + TTL.pubkey = pubkey; + } +} diff --git a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java index 3a68968..d4ab2ad 100644 --- a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java +++ b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java @@ -1,11 +1,11 @@ package ch.dissem.bitmessage.security; import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.GenericPayload; import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.utils.CallbackWaiter; import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.UnixTime; @@ -15,13 +15,13 @@ import javax.xml.bind.DatatypeConverter; import java.io.ByteArrayInputStream; import java.io.IOException; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** - * Created by chris on 19.07.15. + * @author Christian Basler */ public class CryptographyTest { public static final byte[] TEST_VALUE = "teststring".getBytes(); @@ -72,7 +72,7 @@ public class CryptographyTest { public void testProofOfWorkFails() throws IOException { ObjectMessage objectMessage = new ObjectMessage.Builder() .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * DAY)) // 5 minutes + .expiresTime(UnixTime.now(+2 * MINUTE)) .objectType(0) .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) .build(); @@ -83,7 +83,7 @@ public class CryptographyTest { public void testDoProofOfWork() throws Exception { ObjectMessage objectMessage = new ObjectMessage.Builder() .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * DAY)) + .expiresTime(UnixTime.now(+2 * MINUTE)) .objectType(0) .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) .build(); diff --git a/demo/build.gradle b/demo/build.gradle index 84d5907..d87b0f3 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -16,6 +16,8 @@ uploadArchives { sourceCompatibility = 1.8 +test.enabled = Boolean.valueOf(systemTestsEnabled) + task fatCapsule(type: FatCapsule) { applicationClass 'ch.dissem.bitmessage.demo.Main' } @@ -30,4 +32,5 @@ dependencies { compile 'args4j:args4j:2.32' compile 'com.h2database:h2:1.4.190' testCompile 'junit:junit:4.11' + testCompile 'org.mockito:mockito-core:1.10.19' } diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java new file mode 100644 index 0000000..1309336 --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -0,0 +1,84 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.repository.*; +import ch.dissem.bitmessage.utils.TTL; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Basler + */ +public class SystemTest { + static BitmessageContext alice; + static TestListener aliceListener = new TestListener(); + static BitmessageAddress aliceIdentity; + + static BitmessageContext bob; + static TestListener bobListener = new TestListener(); + static BitmessageAddress bobIdentity; + + @BeforeClass + public static void setUp() { + TTL.msg(5 * MINUTE); + TTL.getpubkey(5 * MINUTE); + TTL.pubkey(5 * MINUTE); + JdbcConfig aliceDB = new JdbcConfig("jdbc:h2:mem:alice;DB_CLOSE_DELAY=-1", "sa", ""); + alice = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(aliceDB)) + .inventory(new JdbcInventory(aliceDB)) + .messageRepo(new JdbcMessageRepository(aliceDB)) + .powRepo(new JdbcProofOfWorkRepository(aliceDB)) + .port(6001) + .nodeRegistry(new TestNodeRegistry(6002)) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(aliceListener) + .build(); + alice.startup(); + aliceIdentity = alice.createIdentity(false); + + JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); + bob = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(bobDB)) + .inventory(new JdbcInventory(bobDB)) + .messageRepo(new JdbcMessageRepository(bobDB)) + .powRepo(new JdbcProofOfWorkRepository(bobDB)) + .port(6002) + .nodeRegistry(new TestNodeRegistry(6001)) + .networkHandler(new DefaultNetworkHandler()) + .cryptography(new BouncyCryptography()) + .listener(bobListener) + .build(); + bob.startup(); + bobIdentity = bob.createIdentity(false); + } + + @AfterClass + public static void tearDown() { + alice.shutdown(); + bob.shutdown(); + } + + @Test + public void ensureAliceCanSendMessageToBob() throws Exception { + bobListener.reset(); + String originalMessage = UUID.randomUUID().toString(); + alice.send(aliceIdentity, new BitmessageAddress(bobIdentity.getAddress()), "Subject", originalMessage); + + Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + + assertThat(plaintext.getText(), equalTo(originalMessage)); + } +} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestListener.java b/demo/src/test/java/ch/dissem/bitmessage/TestListener.java new file mode 100644 index 0000000..9c00776 --- /dev/null +++ b/demo/src/test/java/ch/dissem/bitmessage/TestListener.java @@ -0,0 +1,26 @@ +package ch.dissem.bitmessage; + +import ch.dissem.bitmessage.entity.Plaintext; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * Created by chrig on 02.02.2016. + */ +public class TestListener implements BitmessageContext.Listener { + private CompletableFuture