From 50ad1f486ebbc142478f0e27f8758047d10228dc Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Wed, 16 Jan 2013 22:56:03 +0000
Subject: [PATCH] Removed batches from BMP. Messages are now sent and acked
 individually.

---
 .../sf/briar/api/db/DatabaseComponent.java    |  31 +-
 .../api/db/event/BatchReceivedEvent.java      |   6 -
 ...AddedEvent.java => MessageAddedEvent.java} |   2 +-
 .../api/db/event/MessageReceivedEvent.java    |   6 +
 .../src/net/sf/briar/api/protocol/Ack.java    |   6 +-
 .../src/net/sf/briar/api/protocol/Batch.java  |  13 -
 .../net/sf/briar/api/protocol/BatchId.java    |  21 --
 .../briar/api/protocol/MessageVerifier.java   |   8 +
 .../sf/briar/api/protocol/PacketFactory.java  |   4 +-
 .../sf/briar/api/protocol/ProtocolReader.java |   4 +-
 .../sf/briar/api/protocol/ProtocolWriter.java |   6 +-
 .../net/sf/briar/api/protocol/RawBatch.java   |  13 -
 .../src/net/sf/briar/api/protocol/Types.java  |   1 -
 .../briar/api/protocol/UnverifiedBatch.java   |   8 -
 .../api}/protocol/UnverifiedMessage.java      |  10 +-
 briar-core/.classpath                         |   1 +
 briar-core/src/net/sf/briar/db/Database.java  |  71 ++--
 .../sf/briar/db/DatabaseComponentImpl.java    |  93 +++---
 .../net/sf/briar/db/DatabaseConstants.java    |   6 -
 .../src/net/sf/briar/db/JdbcDatabase.java     | 303 +++++-------------
 .../src/net/sf/briar/protocol/AckImpl.java    |   8 +-
 .../src/net/sf/briar/protocol/AckReader.java  |  10 +-
 .../src/net/sf/briar/protocol/BatchImpl.java  |  27 --
 .../net/sf/briar/protocol/BatchReader.java    |  41 ---
 .../net/sf/briar/protocol/MessageReader.java  |   3 +-
 ...atchImpl.java => MessageVerifierImpl.java} |  49 +--
 .../sf/briar/protocol/PacketFactoryImpl.java  |  21 +-
 .../net/sf/briar/protocol/ProtocolModule.java |  12 +-
 .../protocol/ProtocolReaderFactoryImpl.java   |  15 +-
 .../sf/briar/protocol/ProtocolReaderImpl.java |  16 +-
 .../sf/briar/protocol/ProtocolWriterImpl.java |  31 +-
 .../net/sf/briar/protocol/RawBatchImpl.java   |  25 --
 .../protocol/UnverifiedBatchFactory.java      |  11 -
 .../protocol/UnverifiedBatchFactoryImpl.java  |  23 --
 .../briar/protocol/UnverifiedMessageImpl.java |   3 +-
 .../protocol/duplex/DuplexConnection.java     |  66 ++--
 .../duplex/DuplexConnectionFactoryImpl.java   |  19 +-
 .../duplex/IncomingDuplexConnection.java      |  10 +-
 .../duplex/OutgoingDuplexConnection.java      |  10 +-
 .../simplex/IncomingSimplexConnection.java    |  38 ++-
 .../simplex/OutgoingSimplexConnection.java    |  24 +-
 .../simplex/SimplexConnectionFactoryImpl.java |  11 +-
 briar-tests/build.xml                         |   3 -
 .../net/sf/briar/ProtocolIntegrationTest.java |  80 +++--
 .../net/sf/briar/{db => }/TestMessage.java    |   4 +-
 .../sf/briar/db/DatabaseComponentTest.java    | 179 ++++-------
 .../src/net/sf/briar/db/H2DatabaseTest.java   | 237 ++++----------
 .../sf/briar/protocol/BatchReaderTest.java    | 137 --------
 .../net/sf/briar/protocol/ConstantsTest.java  |  52 ++-
 .../protocol/ProtocolIntegrationTest.java     | 134 --------
 .../protocol/UnverifiedBatchImplTest.java     | 242 --------------
 .../OutgoingSimplexConnectionTest.java        |  25 +-
 .../SimplexProtocolIntegrationTest.java       |  13 +-
 .../transport/ConnectionWriterImplTest.java   |   1 -
 .../transport/TransportIntegrationTest.java   |  11 +-
 55 files changed, 556 insertions(+), 1648 deletions(-)
 delete mode 100644 briar-api/src/net/sf/briar/api/db/event/BatchReceivedEvent.java
 rename briar-api/src/net/sf/briar/api/db/event/{MessagesAddedEvent.java => MessageAddedEvent.java} (70%)
 create mode 100644 briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
 delete mode 100644 briar-api/src/net/sf/briar/api/protocol/Batch.java
 delete mode 100644 briar-api/src/net/sf/briar/api/protocol/BatchId.java
 create mode 100644 briar-api/src/net/sf/briar/api/protocol/MessageVerifier.java
 delete mode 100644 briar-api/src/net/sf/briar/api/protocol/RawBatch.java
 delete mode 100644 briar-api/src/net/sf/briar/api/protocol/UnverifiedBatch.java
 rename {briar-core/src/net/sf/briar => briar-api/src/net/sf/briar/api}/protocol/UnverifiedMessage.java (58%)
 delete mode 100644 briar-core/src/net/sf/briar/protocol/BatchImpl.java
 delete mode 100644 briar-core/src/net/sf/briar/protocol/BatchReader.java
 rename briar-core/src/net/sf/briar/protocol/{UnverifiedBatchImpl.java => MessageVerifierImpl.java} (54%)
 delete mode 100644 briar-core/src/net/sf/briar/protocol/RawBatchImpl.java
 delete mode 100644 briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactory.java
 delete mode 100644 briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactoryImpl.java
 rename briar-tests/src/net/sf/briar/{db => }/TestMessage.java (96%)
 delete mode 100644 briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java
 delete mode 100644 briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java
 delete mode 100644 briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java

diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
index b584830233..37675ac399 100644
--- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -11,13 +11,11 @@ import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
@@ -70,25 +68,28 @@ public interface DatabaseComponent {
 
 	/**
 	 * Generates an acknowledgement for the given contact. Returns null if
-	 * there are no batches to acknowledge.
+	 * there are no messages to acknowledge.
 	 */
-	Ack generateAck(ContactId c, int maxBatches) throws DbException;
+	Ack generateAck(ContactId c, int maxMessages) throws DbException;
 
 	/**
-	 * Generates a batch of messages for the given contact. Returns null if
-	 * there are no sendable messages that fit in the given capacity.
+	 * Generates a batch of raw messages for the given contact, with a total
+	 * length less than or equal to the given length. Returns null if
+	 * there are no sendable messages that fit in the given length.
 	 */
-	RawBatch generateBatch(ContactId c, int capacity) throws DbException;
+	Collection<byte[]> generateBatch(ContactId c, int maxLength)
+			throws DbException;
 
 	/**
-	 * Generates a batch of messages for the given contact from the given
-	 * collection of requested messages. Any messages that were either added to
-	 * the batch, or were considered but are no longer sendable to the contact,
-	 * are removed from the collection of requested messages before returning.
+	 * Generates a batch of raw messages for the given contact from the given
+	 * collection of requested messages, with a total length less than or equal
+	 * to the given length. Any messages that were either added to the batch,
+	 * or were considered but are no longer sendable to the contact, are
+	 * removed from the collection of requested messages before returning.
 	 * Returns null if there are no sendable messages that fit in the given
-	 * capacity.
+	 * length.
 	 */
-	RawBatch generateBatch(ContactId c, int capacity,
+	Collection<byte[]> generateBatch(ContactId c, int maxLength,
 			Collection<MessageId> requested) throws DbException;
 
 	/**
@@ -170,8 +171,8 @@ public interface DatabaseComponent {
 	/** Processes an acknowledgement from the given contact. */
 	void receiveAck(ContactId c, Ack a) throws DbException;
 
-	/** Processes a batch of messages from the given contact. */
-	void receiveBatch(ContactId c, Batch b) throws DbException;
+	/** Processes a message from the given contact. */
+	void receiveMessage(ContactId c, Message m) throws DbException;
 
 	/**
 	 * Processes an offer from the given contact and generates a request for
diff --git a/briar-api/src/net/sf/briar/api/db/event/BatchReceivedEvent.java b/briar-api/src/net/sf/briar/api/db/event/BatchReceivedEvent.java
deleted file mode 100644
index a7ebcdef98..0000000000
--- a/briar-api/src/net/sf/briar/api/db/event/BatchReceivedEvent.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.sf.briar.api.db.event;
-
-/** An event that is broadcast when a batch of messages is received. */
-public class BatchReceivedEvent extends DatabaseEvent {
-
-}
diff --git a/briar-api/src/net/sf/briar/api/db/event/MessagesAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java
similarity index 70%
rename from briar-api/src/net/sf/briar/api/db/event/MessagesAddedEvent.java
rename to briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java
index c51515d528..d28dd1664d 100644
--- a/briar-api/src/net/sf/briar/api/db/event/MessagesAddedEvent.java
+++ b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java
@@ -4,6 +4,6 @@ package net.sf.briar.api.db.event;
  * An event that is broadcast when one or more messages are added to the
  * database.
  */
-public class MessagesAddedEvent extends DatabaseEvent {
+public class MessageAddedEvent extends DatabaseEvent {
 
 }
diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
new file mode 100644
index 0000000000..4a5ae2c688
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java
@@ -0,0 +1,6 @@
+package net.sf.briar.api.db.event;
+
+/** An event that is broadcast when a message is received. */
+public class MessageReceivedEvent extends DatabaseEvent {
+
+}
diff --git a/briar-api/src/net/sf/briar/api/protocol/Ack.java b/briar-api/src/net/sf/briar/api/protocol/Ack.java
index 8be9162b23..4608fee530 100644
--- a/briar-api/src/net/sf/briar/api/protocol/Ack.java
+++ b/briar-api/src/net/sf/briar/api/protocol/Ack.java
@@ -2,9 +2,9 @@ package net.sf.briar.api.protocol;
 
 import java.util.Collection;
 
-/** A packet acknowledging receipt of one or more batches. */
+/** A packet acknowledging receipt of one or more messages. */
 public interface Ack {
 
-	/** Returns the IDs of the acknowledged batches. */
-	Collection<BatchId> getBatchIds();
+	/** Returns the IDs of the acknowledged messages. */
+	Collection<MessageId> getMessageIds();
 }
diff --git a/briar-api/src/net/sf/briar/api/protocol/Batch.java b/briar-api/src/net/sf/briar/api/protocol/Batch.java
deleted file mode 100644
index eec1fef477..0000000000
--- a/briar-api/src/net/sf/briar/api/protocol/Batch.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.util.Collection;
-
-/** An incoming packet containing messages. */
-public interface Batch {
-
-	/** Returns the batch's unique identifier. */
-	BatchId getId();
-
-	/** Returns the messages contained in the batch. */
-	Collection<Message> getMessages();
-}
\ No newline at end of file
diff --git a/briar-api/src/net/sf/briar/api/protocol/BatchId.java b/briar-api/src/net/sf/briar/api/protocol/BatchId.java
deleted file mode 100644
index ab33800761..0000000000
--- a/briar-api/src/net/sf/briar/api/protocol/BatchId.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.util.Arrays;
-
-/**
- * Type-safe wrapper for a byte array that uniquely identifies a batch of
- * messages.
- */
-public class BatchId extends UniqueId {
-
-	public BatchId(byte[] id) {
-		super(id);
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if(o instanceof BatchId)
-			return Arrays.equals(id, ((BatchId) o).id);
-		return false;
-	}
-}
diff --git a/briar-api/src/net/sf/briar/api/protocol/MessageVerifier.java b/briar-api/src/net/sf/briar/api/protocol/MessageVerifier.java
new file mode 100644
index 0000000000..59313414fb
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/protocol/MessageVerifier.java
@@ -0,0 +1,8 @@
+package net.sf.briar.api.protocol;
+
+import java.security.GeneralSecurityException;
+
+public interface MessageVerifier {
+
+	Message verifyMessage(UnverifiedMessage m) throws GeneralSecurityException;
+}
diff --git a/briar-api/src/net/sf/briar/api/protocol/PacketFactory.java b/briar-api/src/net/sf/briar/api/protocol/PacketFactory.java
index 3f15e8bc8f..9c3c51116d 100644
--- a/briar-api/src/net/sf/briar/api/protocol/PacketFactory.java
+++ b/briar-api/src/net/sf/briar/api/protocol/PacketFactory.java
@@ -6,9 +6,7 @@ import java.util.Map;
 
 public interface PacketFactory {
 
-	Ack createAck(Collection<BatchId> acked);
-
-	RawBatch createBatch(Collection<byte[]> messages);
+	Ack createAck(Collection<MessageId> acked);
 
 	Offer createOffer(Collection<MessageId> offered);
 
diff --git a/briar-api/src/net/sf/briar/api/protocol/ProtocolReader.java b/briar-api/src/net/sf/briar/api/protocol/ProtocolReader.java
index 104b0d60a5..24f4c8aac8 100644
--- a/briar-api/src/net/sf/briar/api/protocol/ProtocolReader.java
+++ b/briar-api/src/net/sf/briar/api/protocol/ProtocolReader.java
@@ -9,8 +9,8 @@ public interface ProtocolReader {
 	boolean hasAck() throws IOException;
 	Ack readAck() throws IOException;
 
-	boolean hasBatch() throws IOException;
-	UnverifiedBatch readBatch() throws IOException;
+	boolean hasMessage() throws IOException;
+	UnverifiedMessage readMessage() throws IOException;
 
 	boolean hasOffer() throws IOException;
 	Offer readOffer() throws IOException;
diff --git a/briar-api/src/net/sf/briar/api/protocol/ProtocolWriter.java b/briar-api/src/net/sf/briar/api/protocol/ProtocolWriter.java
index fc30d803fd..4ddd4c4fb9 100644
--- a/briar-api/src/net/sf/briar/api/protocol/ProtocolWriter.java
+++ b/briar-api/src/net/sf/briar/api/protocol/ProtocolWriter.java
@@ -4,15 +4,13 @@ import java.io.IOException;
 
 public interface ProtocolWriter {
 
-	int getMaxBatchesForAck(long capacity);
+	int getMaxMessagesForAck(long capacity);
 
 	int getMaxMessagesForOffer(long capacity);
 
-	int getMessageCapacityForBatch(long capacity);
-
 	void writeAck(Ack a) throws IOException;
 
-	void writeBatch(RawBatch b) throws IOException;
+	void writeMessage(byte[] raw) throws IOException;
 
 	void writeOffer(Offer o) throws IOException;
 
diff --git a/briar-api/src/net/sf/briar/api/protocol/RawBatch.java b/briar-api/src/net/sf/briar/api/protocol/RawBatch.java
deleted file mode 100644
index f1337f6458..0000000000
--- a/briar-api/src/net/sf/briar/api/protocol/RawBatch.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.util.Collection;
-
-/** An outgoing packet containing messages. */
-public interface RawBatch {
-
-	/** Returns the batch's unique identifier. */
-	BatchId getId();
-
-	/** Returns the serialised messages contained in the batch. */
-	Collection<byte[]> getMessages();
-}
diff --git a/briar-api/src/net/sf/briar/api/protocol/Types.java b/briar-api/src/net/sf/briar/api/protocol/Types.java
index 2fd63f7eee..71ec94a397 100644
--- a/briar-api/src/net/sf/briar/api/protocol/Types.java
+++ b/briar-api/src/net/sf/briar/api/protocol/Types.java
@@ -5,7 +5,6 @@ public interface Types {
 
 	int ACK = 0;
 	int AUTHOR = 1;
-	int BATCH = 2;
 	int GROUP = 3;
 	int MESSAGE = 4;
 	int OFFER = 5;
diff --git a/briar-api/src/net/sf/briar/api/protocol/UnverifiedBatch.java b/briar-api/src/net/sf/briar/api/protocol/UnverifiedBatch.java
deleted file mode 100644
index abd1f15159..0000000000
--- a/briar-api/src/net/sf/briar/api/protocol/UnverifiedBatch.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.security.GeneralSecurityException;
-
-public interface UnverifiedBatch {
-
-	Batch verify() throws GeneralSecurityException;
-}
diff --git a/briar-core/src/net/sf/briar/protocol/UnverifiedMessage.java b/briar-api/src/net/sf/briar/api/protocol/UnverifiedMessage.java
similarity index 58%
rename from briar-core/src/net/sf/briar/protocol/UnverifiedMessage.java
rename to briar-api/src/net/sf/briar/api/protocol/UnverifiedMessage.java
index 64866d2a01..8fd391d2f8 100644
--- a/briar-core/src/net/sf/briar/protocol/UnverifiedMessage.java
+++ b/briar-api/src/net/sf/briar/api/protocol/UnverifiedMessage.java
@@ -1,10 +1,6 @@
-package net.sf.briar.protocol;
+package net.sf.briar.api.protocol;
 
-import net.sf.briar.api.protocol.Author;
-import net.sf.briar.api.protocol.Group;
-import net.sf.briar.api.protocol.MessageId;
-
-interface UnverifiedMessage {
+public interface UnverifiedMessage {
 
 	MessageId getParent();
 
@@ -16,7 +12,7 @@ interface UnverifiedMessage {
 
 	long getTimestamp();
 
-	byte[] getRaw();
+	byte[] getSerialised();
 
 	byte[] getAuthorSignature();
 
diff --git a/briar-core/.classpath b/briar-core/.classpath
index 4638c93899..f09021d12c 100644
--- a/briar-core/.classpath
+++ b/briar-core/.classpath
@@ -13,6 +13,7 @@
 	<classpathentry kind="lib" path="libs/weupnp-0.1.1.jar"/>
 	<classpathentry kind="lib" path="libs/bluecove-2.1.1-SNAPSHOT-briar.jar"/>
 	<classpathentry kind="lib" path="libs/bluecove-gpl-2.1.1-SNAPSHOT.jar"/>
+	<classpathentry kind="lib" path="libs/javax.inject.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/briar-api"/>
 	<classpathentry kind="lib" path="/briar-api/libs/android.jar"/>
 	<classpathentry kind="lib" path="/briar-api/libs/guice-3.0-no_aop.jar"/>
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 3959e735d1..08b420d59c 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -11,7 +11,6 @@ import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
@@ -71,13 +70,6 @@ interface Database<T> {
 	 */
 	void commitTransaction(T txn) throws DbException;
 
-	/**
-	 * Records a received batch as needing to be acknowledged.
-	 * <p>
-	 * Locking: contact read, messageStatus write.
-	 */
-	void addBatchToAck(T txn, ContactId c, BatchId b) throws DbException;
-
 	/**
 	 * Adds a new contact to the database and returns an ID for the contact.
 	 * <p>
@@ -101,12 +93,19 @@ interface Database<T> {
 	boolean addGroupMessage(T txn, Message m) throws DbException;
 
 	/**
-	 * Records a sent batch as needing to be acknowledged.
+	 * Records a received message as needing to be acknowledged.
+	 * <p>
+	 * Locking: contact read, messageStatus write.
+	 */
+	void addMessageToAck(T txn, ContactId c, MessageId m) throws DbException;
+
+	/**
+	 * Records a collection of sent messages as needing to be acknowledged.
 	 * <p>
 	 * Locking: contact read, message read, messageStatus write.
 	 */
-	void addOutstandingBatch(T txn, ContactId c, BatchId b,
-			Collection<MessageId> sent) throws DbException;
+	void addOutstandingMessages(T txn, ContactId c, Collection<MessageId> sent)
+			throws DbException;
 
 	/**
 	 * Returns false if the given message is already in the database. Otherwise
@@ -196,15 +195,6 @@ interface Database<T> {
 	boolean containsVisibleSubscription(T txn, GroupId g, ContactId c,
 			long time) throws DbException;
 
-	/**
-	 * Returns the IDs of any batches received from the given contact that need
-	 * to be acknowledged.
-	 * <p>
-	 * Locking: contact read, messageStatus read.
-	 */
-	Collection<BatchId> getBatchesToAck(T txn, ContactId c, int maxBatches)
-			throws DbException;
-
 	/**
 	 * Returns the configuration for the given transport.
 	 * <p>
@@ -267,12 +257,13 @@ interface Database<T> {
 	Collection<Transport> getLocalTransports(T txn) throws DbException;
 
 	/**
-	 * Returns the IDs of any batches sent to the given contact that should now
-	 * be considered lost.
+	 * Returns the IDs of any messages sent to the given contact that should
+	 * now be considered lost.
 	 * <p>
 	 * Locking: contact read, message read, messageStatus read.
 	 */
-	Collection<BatchId> getLostBatches(T txn, ContactId c) throws DbException;
+	Collection<MessageId> getLostMessages(T txn, ContactId c)
+			throws DbException;
 
 	/**
 	 * Returns the message identified by the given ID, in serialised form.
@@ -315,6 +306,15 @@ interface Database<T> {
 	Collection<MessageId> getMessagesByAuthor(T txn, AuthorId a)
 			throws DbException;
 
+	/**
+	 * Returns the IDs of any messages received from the given contact that
+	 * need to be acknowledged.
+	 * <p>
+	 * Locking: contact read, messageStatus read.
+	 */
+	Collection<MessageId> getMessagesToAck(T txn, ContactId c, int maxMessages)
+			throws DbException;
+
 	/**
 	 * Returns the number of children of the message identified by the given
 	 * ID that are present in the database and have sendability scores greater
@@ -380,12 +380,13 @@ interface Database<T> {
 
 	/**
 	 * Returns the IDs of some messages that are eligible to be sent to the
-	 * given contact, with a total size less than or equal to the given size.
+	 * given contact, with a total length less than or equal to the given
+	 * length.
 	 * <p>
 	 * Locking: contact read, message read, messageStatus read,
 	 * subscription read.
 	 */
-	Collection<MessageId> getSendableMessages(T txn, ContactId c, int capacity)
+	Collection<MessageId> getSendableMessages(T txn, ContactId c, int maxLength)
 			throws DbException;
 
 	/**
@@ -493,21 +494,22 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Removes an outstanding batch that has been acknowledged. Any messages in
-	 * the batch that are still considered outstanding (Status.SENT) with
+	 * Removes outstanding messages that have been acknowledged. Any of the
+	 * messages that are still considered outstanding (Status.SENT) with
 	 * respect to the given contact are now considered seen (Status.SEEN).
 	 * <p>
 	 * Locking: contact read, message read, messageStatus write.
 	 */
-	void removeAckedBatch(T txn, ContactId c, BatchId b) throws DbException;
+	void removeAckedMessages(T txn, ContactId c, Collection<MessageId> acked)
+			throws DbException;
 
 	/**
-	 * Marks the given batches received from the given contact as having been
+	 * Marks the given messages received from the given contact as having been
 	 * acknowledged.
 	 * <p>
 	 * Locking: contact read, messageStatus write.
 	 */
-	void removeBatchesToAck(T txn, ContactId c, Collection<BatchId> sent)
+	void removeMessagesToAck(T txn, ContactId c, Collection<MessageId> acked)
 			throws DbException;
 
 	/**
@@ -519,13 +521,14 @@ interface Database<T> {
 	void removeContact(T txn, ContactId c) throws DbException;
 
 	/**
-	 * Removes an outstanding batch that has been lost. Any messages in the
-	 * batch that are still considered outstanding (Status.SENT) with respect
-	 * to the given contact are now considered unsent (Status.NEW).
+	 * Removes outstanding messages that have been lost. Any messages that are
+	 * still considered outstanding (Status.SENT) with respect to the given
+	 * contact are now considered unsent (Status.NEW).
 	 * <p>
 	 * Locking: contact read, message read, messageStatus write.
 	 */
-	void removeLostBatch(T txn, ContactId c, BatchId b) throws DbException;
+	void removeLostMessages(T txn, ContactId c, Collection<MessageId> lost)
+			throws DbException;
 
 	/**
 	 * Removes a message (and all associated state) from the database.
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index 8c81c4ee9f..2fa615d601 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -32,28 +32,25 @@ import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
 import net.sf.briar.api.db.NoSuchContactException;
 import net.sf.briar.api.db.NoSuchContactTransportException;
-import net.sf.briar.api.db.event.BatchReceivedEvent;
 import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.MessagesAddedEvent;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageReceivedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
@@ -270,7 +267,7 @@ DatabaseCleaner.Callback {
 			contactLock.readLock().unlock();
 		}
 		// Call the listeners outside the lock
-		if(added) callListeners(new MessagesAddedEvent());
+		if(added) callListeners(new MessageAddedEvent());
 	}
 
 	/**
@@ -388,7 +385,7 @@ DatabaseCleaner.Callback {
 			contactLock.readLock().unlock();
 		}
 		// Call the listeners outside the lock
-		if(added) callListeners(new MessagesAddedEvent());
+		if(added) callListeners(new MessageAddedEvent());
 	}
 
 	public void addSecrets(Collection<TemporarySecret> secrets)
@@ -444,8 +441,8 @@ DatabaseCleaner.Callback {
 		return true;
 	}
 
-	public Ack generateAck(ContactId c, int maxBatches) throws DbException {
-		Collection<BatchId> acked;
+	public Ack generateAck(ContactId c, int maxMessages) throws DbException {
+		Collection<MessageId> acked;
 		contactLock.readLock().lock();
 		try {
 			messageStatusLock.readLock().lock();
@@ -454,7 +451,7 @@ DatabaseCleaner.Callback {
 				try {
 					if(!db.containsContact(txn, c))
 						throw new NoSuchContactException();
-					acked = db.getBatchesToAck(txn, c, maxBatches);
+					acked = db.getMessagesToAck(txn, c, maxMessages);
 					db.commitTransaction(txn);
 				} catch(DbException e) {
 					db.abortTransaction(txn);
@@ -469,7 +466,7 @@ DatabaseCleaner.Callback {
 			try {
 				T txn = db.startTransaction();
 				try {
-					db.removeBatchesToAck(txn, c, acked);
+					db.removeMessagesToAck(txn, c, acked);
 					db.commitTransaction(txn);
 				} catch(DbException e) {
 					db.abortTransaction(txn);
@@ -484,11 +481,10 @@ DatabaseCleaner.Callback {
 		return packetFactory.createAck(acked);
 	}
 
-	public RawBatch generateBatch(ContactId c, int capacity)
+	public Collection<byte[]> generateBatch(ContactId c, int maxLength)
 			throws DbException {
 		Collection<MessageId> ids;
 		List<byte[]> messages = new ArrayList<byte[]>();
-		RawBatch b;
 		// Get some sendable messages from the database
 		contactLock.readLock().lock();
 		try {
@@ -502,7 +498,7 @@ DatabaseCleaner.Callback {
 						try {
 							if(!db.containsContact(txn, c))
 								throw new NoSuchContactException();
-							ids = db.getSendableMessages(txn, c, capacity);
+							ids = db.getSendableMessages(txn, c, maxLength);
 							for(MessageId m : ids) {
 								messages.add(db.getMessage(txn, m));
 							}
@@ -518,13 +514,11 @@ DatabaseCleaner.Callback {
 					messageStatusLock.readLock().unlock();
 				}
 				if(messages.isEmpty()) return null;
-				messages = Collections.unmodifiableList(messages);
-				b = packetFactory.createBatch(messages);
 				messageStatusLock.writeLock().lock();
 				try {
 					T txn = db.startTransaction();
 					try {
-						db.addOutstandingBatch(txn, c, b.getId(), ids);
+						db.addOutstandingMessages(txn, c, ids);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -539,14 +533,13 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		return b;
+		return Collections.unmodifiableList(messages);
 	}
 
-	public RawBatch generateBatch(ContactId c, int capacity,
+	public Collection<byte[]> generateBatch(ContactId c, int maxLength,
 			Collection<MessageId> requested) throws DbException {
 		Collection<MessageId> ids = new ArrayList<MessageId>();
 		List<byte[]> messages = new ArrayList<byte[]>();
-		RawBatch b;
 		// Get some sendable messages from the database
 		contactLock.readLock().lock();
 		try {
@@ -565,10 +558,10 @@ DatabaseCleaner.Callback {
 								MessageId m = it.next();
 								byte[] raw = db.getMessageIfSendable(txn, c, m);
 								if(raw != null) {
-									if(raw.length > capacity) break;
+									if(raw.length > maxLength) break;
 									messages.add(raw);
 									ids.add(m);
-									capacity -= raw.length;
+									maxLength -= raw.length;
 								}
 								it.remove();
 							}
@@ -584,13 +577,11 @@ DatabaseCleaner.Callback {
 					messageStatusLock.readLock().unlock();
 				}
 				if(messages.isEmpty()) return null;
-				messages = Collections.unmodifiableList(messages);
-				b = packetFactory.createBatch(messages);
 				messageStatusLock.writeLock().lock();
 				try {
 					T txn = db.startTransaction();
 					try {
-						db.addOutstandingBatch(txn, c, b.getId(), ids);
+						db.addOutstandingMessages(txn, c, ids);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -605,7 +596,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		return b;
+		return Collections.unmodifiableList(messages);
 	}
 
 	public Offer generateOffer(ContactId c, int maxMessages)
@@ -1057,12 +1048,12 @@ DatabaseCleaner.Callback {
 					try {
 						if(!db.containsContact(txn, c))
 							throw new NoSuchContactException();
-						Collection<BatchId> acks = a.getBatchIds();
-						// Mark all messages in acked batches as seen
-						for(BatchId b : acks) db.removeAckedBatch(txn, c, b);
-						// Find any lost batches that need to be retransmitted
-						Collection<BatchId> lost = db.getLostBatches(txn, c);
-						for(BatchId b : lost) db.removeLostBatch(txn, c, b);
+						// Mark all acked messages as seen
+						db.removeAckedMessages(txn, c, a.getMessageIds());
+						// Find any lost messages that need to be retransmitted
+						// FIXME: Merge these methods
+						Collection<MessageId> lost = db.getLostMessages(txn, c);
+						if(!lost.isEmpty()) db.removeLostMessages(txn, c, lost);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -1079,8 +1070,8 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void receiveBatch(ContactId c, Batch b) throws DbException {
-		boolean anyAdded = false;
+	public void receiveMessage(ContactId c, Message m) throws DbException {
+		boolean added = false;
 		contactLock.readLock().lock();
 		try {
 			messageLock.writeLock().lock();
@@ -1093,8 +1084,8 @@ DatabaseCleaner.Callback {
 						try {
 							if(!db.containsContact(txn, c))
 								throw new NoSuchContactException();
-							anyAdded = storeMessages(txn, c, b.getMessages());
-							db.addBatchToAck(txn, c, b.getId());
+							added = storeMessage(txn, c, m);
+							db.addMessageToAck(txn, c, m.getId());
 							db.commitTransaction(txn);
 						} catch(DbException e) {
 							db.abortTransaction(txn);
@@ -1113,32 +1104,24 @@ DatabaseCleaner.Callback {
 			contactLock.readLock().unlock();
 		}
 		// Call the listeners outside the lock
-		callListeners(new BatchReceivedEvent());
-		if(anyAdded) callListeners(new MessagesAddedEvent());
+		callListeners(new MessageReceivedEvent());
+		if(added) callListeners(new MessageAddedEvent());
 	}
 
 	/**
-	 * Attempts to store a collection of messages received from the given
-	 * contact, and returns true if any were stored.
+	 * Attempts to store a message received from the given contact, and returns
+	 * true if it was stored.
 	 * <p>
 	 * Locking: contact read, message write, messageStatus write,
 	 * subscription read.
 	 */
-	private boolean storeMessages(T txn, ContactId c,
-			Collection<Message> messages) throws DbException {
-		boolean anyStored = false;
-		for(Message m : messages) {
-			GroupId g = m.getGroup();
-			if(g == null) {
-				if(storePrivateMessage(txn, m, c, true)) anyStored = true;
-			} else {
-				long timestamp = m.getTimestamp();
-				if(db.containsVisibleSubscription(txn, g, c, timestamp)) {
-					if(storeGroupMessage(txn, m, c)) anyStored = true;
-				}
-			}
-		}
-		return anyStored;
+	private boolean storeMessage(T txn, ContactId c, Message m)
+			throws DbException {
+		GroupId g = m.getGroup();
+		if(g == null) return storePrivateMessage(txn, m, c, true);
+		if(!db.containsVisibleSubscription(txn, g, c, m.getTimestamp()))
+			return false;
+		return storeGroupMessage(txn, m, c);
 	}
 
 	public Request receiveOffer(ContactId c, Offer o) throws DbException {
diff --git a/briar-core/src/net/sf/briar/db/DatabaseConstants.java b/briar-core/src/net/sf/briar/db/DatabaseConstants.java
index 27af159268..eb00f1290c 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseConstants.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseConstants.java
@@ -42,12 +42,6 @@ interface DatabaseConstants {
 	 */
 	long EXPIRY_MODULUS = 60L * 60L * 1000L; // 1 hour
 
-	/**
-	 * A batch sent to a contact is considered lost when this many more
-	 * recently sent batches have been acknowledged.
-	 */
-	int RETRANSMIT_THRESHOLD = 5;
-
 	/**
 	 * The time in milliseconds after which a subscription or transport update
 	 * should be sent to a contact even if no changes have occurred.
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index 15d89cd666..10e29501bd 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -3,7 +3,6 @@ package net.sf.briar.db;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static net.sf.briar.db.DatabaseConstants.EXPIRY_MODULUS;
-import static net.sf.briar.db.DatabaseConstants.RETRANSMIT_THRESHOLD;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -33,7 +32,6 @@ import net.sf.briar.api.db.DbClosedException;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
@@ -112,11 +110,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String INDEX_VISIBILITIES_BY_NEXT =
 			"CREATE INDEX visibilitiesByNext on visibilities (nextId)";
 
-	private static final String CREATE_BATCHES_TO_ACK =
-			"CREATE TABLE batchesToAck"
-					+ " (batchId HASH NOT NULL,"
+	private static final String CREATE_MESSAGES_TO_ACK =
+			"CREATE TABLE messagesToAck"
+					+ " (messageId HASH NOT NULL,"
 					+ " contactId INT NOT NULL,"
-					+ " PRIMARY KEY (batchId, contactId),"
+					+ " PRIMARY KEY (messageId, contactId),"
 					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE)";
 
@@ -131,32 +129,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE)";
 
-	private static final String CREATE_OUTSTANDING_BATCHES =
-			"CREATE TABLE outstandingBatches"
-					+ " (batchId HASH NOT NULL,"
-					+ " contactId INT NOT NULL,"
-					+ " timestamp BIGINT NOT NULL,"
-					+ " passover INT NOT NULL,"
-					+ " PRIMARY KEY (batchId, contactId),"
-					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-					+ " ON DELETE CASCADE)";
-
-	private static final String CREATE_OUTSTANDING_MESSAGES =
-			"CREATE TABLE outstandingMessages"
-					+ " (batchId HASH NOT NULL,"
-					+ " contactId INT NOT NULL,"
-					+ " messageId HASH NOT NULL,"
-					+ " PRIMARY KEY (batchId, contactId, messageId),"
-					+ " FOREIGN KEY (batchId, contactId)"
-					+ " REFERENCES outstandingBatches (batchId, contactId)"
-					+ " ON DELETE CASCADE,"
-					+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
-					+ " ON DELETE CASCADE)";
-
-	private static final String INDEX_OUTSTANDING_MESSAGES_BY_BATCH =
-			"CREATE INDEX outstandingMessagesByBatch"
-					+ " ON outstandingMessages (batchId)";
-
 	private static final String CREATE_RATINGS =
 			"CREATE TABLE ratings"
 					+ " (authorId HASH NOT NULL,"
@@ -322,11 +294,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_VISIBILITIES));
 			s.executeUpdate(INDEX_VISIBILITIES_BY_GROUP);
 			s.executeUpdate(INDEX_VISIBILITIES_BY_NEXT);
-			s.executeUpdate(insertTypeNames(CREATE_BATCHES_TO_ACK));
+			s.executeUpdate(insertTypeNames(CREATE_MESSAGES_TO_ACK));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_SUBSCRIPTIONS));
-			s.executeUpdate(insertTypeNames(CREATE_OUTSTANDING_BATCHES));
-			s.executeUpdate(insertTypeNames(CREATE_OUTSTANDING_MESSAGES));
-			s.executeUpdate(INDEX_OUTSTANDING_MESSAGES_BY_BATCH);
 			s.executeUpdate(insertTypeNames(CREATE_RATINGS));
 			s.executeUpdate(insertTypeNames(CREATE_STATUSES));
 			s.executeUpdate(INDEX_STATUSES_BY_MESSAGE);
@@ -452,37 +421,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		if(interrupted) Thread.currentThread().interrupt();
 	}
 
-	public void addBatchToAck(Connection txn, ContactId c, BatchId b)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT NULL FROM batchesToAck"
-					+ " WHERE batchId = ? AND contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, b.getBytes());
-			ps.setInt(2, c.getInt());
-			rs = ps.executeQuery();
-			boolean found = rs.next();
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if(found) return;
-			sql = "INSERT INTO batchesToAck (batchId, contactId)"
-					+ " VALUES (?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, b.getBytes());
-			ps.setInt(2, c.getInt());
-			int affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			ps.close();
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public ContactId addContact(Connection txn) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -596,42 +534,30 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addOutstandingBatch(Connection txn, ContactId c, BatchId b,
-			Collection<MessageId> sent) throws DbException {
+	public void addMessageToAck(Connection txn, ContactId c, MessageId m)
+			throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
-			// Create an outstanding batch row
-			String sql = "INSERT INTO outstandingBatches"
-					+ " (batchId, contactId, timestamp, passover)"
-					+ " VALUES (?, ?, ?, ZERO())";
+			String sql = "INSERT INTO messagesToAck (messageId, contactId)"
+					+ " VALUES (?, ?)";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, b.getBytes());
+			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
-			ps.setLong(3, clock.currentTimeMillis());
 			int affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			ps.close();
-			// Create an outstanding message row for each message in the batch
-			sql = "INSERT INTO outstandingMessages"
-					+ " (batchId, contactId, messageId)"
-					+ " VALUES (?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, b.getBytes());
-			ps.setInt(2, c.getInt());
-			for(MessageId m : sent) {
-				ps.setBytes(3, m.getBytes());
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != sent.size())
-				throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
+			if(affected > 1) throw new DbStateException();
 			ps.close();
-			// Set the status of each message in the batch to SENT
-			sql = "UPDATE statuses SET status = ?"
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public void addOutstandingMessages(Connection txn, ContactId c,
+			Collection<MessageId> sent) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			// Set the status of each message to SENT if it's currently NEW
+			String sql = "UPDATE statuses SET status = ?"
 					+ " WHERE messageId = ? AND contactId = ? AND status = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setShort(1, (short) Status.SENT.ordinal());
@@ -641,7 +567,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				ps.setBytes(2, m.getBytes());
 				ps.addBatch();
 			}
-			batchAffected = ps.executeBatch();
+			int[] batchAffected = ps.executeBatch();
 			if(batchAffected.length != sent.size())
 				throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
@@ -649,7 +575,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			ps.close();
 		} catch(SQLException e) {
-			tryToClose(rs);
 			tryToClose(ps);
 			throw new DbException(e);
 		}
@@ -1006,30 +931,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<BatchId> getBatchesToAck(Connection txn, ContactId c,
-			int maxBatches) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT batchId FROM batchesToAck"
-					+ " WHERE contactId = ?"
-					+ " LIMIT ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, maxBatches);
-			rs = ps.executeQuery();
-			List<BatchId> ids = new ArrayList<BatchId>();
-			while(rs.next()) ids.add(new BatchId(rs.getBytes(1)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(ids);
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public TransportConfig getConfig(Connection txn, TransportId t)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1216,27 +1117,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<BatchId> getLostBatches(Connection txn, ContactId c)
+	public Collection<MessageId> getLostMessages(Connection txn, ContactId c)
 			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT batchId FROM outstandingBatches"
-					+ " WHERE contactId = ? AND passover >= ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, RETRANSMIT_THRESHOLD);
-			rs = ps.executeQuery();
-			List<BatchId> ids = new ArrayList<BatchId>();
-			while(rs.next()) ids.add(new BatchId(rs.getBytes(1)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(ids);
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
+		// FIXME: Retransmission
+		return Collections.emptyList();
 	}
 
 	public byte[] getMessage(Connection txn, MessageId m) throws DbException {
@@ -1411,6 +1295,30 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c,
+			int maxMessages) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT messageId FROM messagesToAck"
+					+ " WHERE contactId = ?"
+					+ " LIMIT ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setInt(2, maxMessages);
+			rs = ps.executeQuery();
+			List<MessageId> ids = new ArrayList<MessageId>();
+			while(rs.next()) ids.add(new MessageId(rs.getBytes(1)));
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(ids);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public int getNumberOfSendableChildren(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1670,7 +1578,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<MessageId> getSendableMessages(Connection txn,
-			ContactId c, int capacity) throws DbException {
+			ContactId c, int maxLength) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1688,13 +1596,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int total = 0;
 			while(rs.next()) {
 				int length = rs.getInt(1);
-				if(total + length > capacity) break;
+				if(total + length > maxLength) break;
 				ids.add(new MessageId(rs.getBytes(2)));
 				total += length;
 			}
 			rs.close();
 			ps.close();
-			if(total == capacity) return Collections.unmodifiableList(ids);
+			if(total == maxLength) return Collections.unmodifiableList(ids);
 			// Do we have any sendable group messages?
 			sql = "SELECT length, m.messageId FROM messages AS m"
 					+ " JOIN contactSubscriptions AS cs"
@@ -1719,7 +1627,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs = ps.executeQuery();
 			while(rs.next()) {
 				int length = rs.getInt(1);
-				if(total + length > capacity) break;
+				if(total + length > maxLength) break;
 				ids.add(new MessageId(rs.getBytes(2)));
 				total += length;
 			}
@@ -2067,99 +1975,52 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void removeAckedBatch(Connection txn, ContactId c, BatchId b)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT timestamp FROM outstandingBatches"
-					+ " WHERE contactId = ? AND batchId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, b.getBytes());
-			rs = ps.executeQuery();
-			if(!rs.next()) throw new DbStateException();
-			long timestamp = rs.getLong(1);
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			// Increment the passover count of all older outstanding batches
-			sql = "UPDATE outstandingBatches SET passover = passover + ?"
-					+ " WHERE contactId = ? AND timestamp < ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, 1);
-			ps.setInt(2, c.getInt());
-			ps.setLong(3, timestamp);
-			ps.executeUpdate();
-			ps.close();
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-		removeBatch(txn, c, b, Status.SEEN);
+	public void removeAckedMessages(Connection txn, ContactId c,
+			Collection<MessageId> acked) throws DbException {
+		setStatus(txn, c, acked, Status.SEEN);
 	}
 
-	private void removeBatch(Connection txn, ContactId c, BatchId b,
-			Status newStatus) throws DbException {
-		PreparedStatement ps = null, ps1 = null;
-		ResultSet rs = null;
+	private void setStatus(Connection txn, ContactId c,
+			Collection<MessageId> ids, Status newStatus) throws DbException {
+		PreparedStatement ps = null;
 		try {
-			String sql = "SELECT messageId FROM outstandingMessages"
-					+ " WHERE contactId = ? AND batchId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, b.getBytes());
-			rs = ps.executeQuery();
-			sql = "UPDATE statuses SET status = ?"
+			// Set the status of each message if it's currently SENT
+			String sql = "UPDATE statuses SET status = ?"
 					+ " WHERE messageId = ? AND contactId = ? AND status = ?";
-			ps1 = txn.prepareStatement(sql);
-			ps1.setShort(1, (short) newStatus.ordinal());
-			ps1.setInt(3, c.getInt());
-			ps1.setShort(4, (short) Status.SENT.ordinal());
-			int messages = 0;
-			while(rs.next()) {
-				messages++;
-				ps1.setBytes(2, rs.getBytes(1));
-				ps1.addBatch();
+			ps = txn.prepareStatement(sql);
+			ps.setShort(1, (short) newStatus.ordinal());
+			ps.setInt(3, c.getInt());
+			ps.setShort(4, (short) Status.SENT.ordinal());
+			for(MessageId m : ids) {
+				ps.setBytes(2, m.getBytes());
+				ps.addBatch();
 			}
-			rs.close();
-			ps.close();
-			int[] batchAffected = ps1.executeBatch();
-			if(batchAffected.length != messages) throw new DbStateException();
+			int[] batchAffected = ps.executeBatch();
+			if(batchAffected.length != ids.size()) throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] > 1) throw new DbStateException();
 			}
-			ps1.close();
-			// Cascade on delete
-			sql = "DELETE FROM outstandingBatches WHERE batchId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, b.getBytes());
-			int affected = ps.executeUpdate();
-			if(affected > 1) throw new DbStateException();
 			ps.close();
 		} catch(SQLException e) {
-			tryToClose(rs);
 			tryToClose(ps);
-			tryToClose(ps1);
 			throw new DbException(e);
 		}
 	}
 
-	public void removeBatchesToAck(Connection txn, ContactId c,
-			Collection<BatchId> sent) throws DbException {
+	public void removeMessagesToAck(Connection txn, ContactId c,
+			Collection<MessageId> acked) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "DELETE FROM batchesToAck"
-					+ " WHERE contactId = ? and batchId = ?";
+			String sql = "DELETE FROM messagesToAck"
+					+ " WHERE contactId = ? AND messageId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			for(BatchId b : sent) {
-				ps.setBytes(2, b.getBytes());
+			for(MessageId m : acked) {
+				ps.setBytes(2, m.getBytes());
 				ps.addBatch();
 			}
 			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != sent.size())
+			if(batchAffected.length != acked.size())
 				throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] != 1) throw new DbStateException();
@@ -2187,9 +2048,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void removeLostBatch(Connection txn, ContactId c, BatchId b)
-			throws DbException {
-		removeBatch(txn, c, b, Status.NEW);
+	public void removeLostMessages(Connection txn, ContactId c,
+			Collection<MessageId> lost) throws DbException {
+		setStatus(txn, c, lost, Status.NEW);
 	}
 
 	public void removeMessage(Connection txn, MessageId m) throws DbException {
diff --git a/briar-core/src/net/sf/briar/protocol/AckImpl.java b/briar-core/src/net/sf/briar/protocol/AckImpl.java
index 9b956ff462..ea8e924284 100644
--- a/briar-core/src/net/sf/briar/protocol/AckImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/AckImpl.java
@@ -3,17 +3,17 @@ package net.sf.briar.protocol;
 import java.util.Collection;
 
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.MessageId;
 
 class AckImpl implements Ack {
 
-	private final Collection<BatchId> acked;
+	private final Collection<MessageId> acked;
 
-	AckImpl(Collection<BatchId> acked) {
+	AckImpl(Collection<MessageId> acked) {
 		this.acked = acked;
 	}
 
-	public Collection<BatchId> getBatchIds() {
+	public Collection<MessageId> getMessageIds() {
 		return acked;
 	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/AckReader.java b/briar-core/src/net/sf/briar/protocol/AckReader.java
index 0b614d7c1b..397a0b66e4 100644
--- a/briar-core/src/net/sf/briar/protocol/AckReader.java
+++ b/briar-core/src/net/sf/briar/protocol/AckReader.java
@@ -10,7 +10,7 @@ import java.util.List;
 import net.sf.briar.api.Bytes;
 import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.PacketFactory;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.UniqueId;
@@ -38,14 +38,14 @@ class AckReader implements StructReader<Ack> {
 		r.resetMaxBytesLength();
 		r.removeConsumer(counting);
 		if(raw.isEmpty()) throw new FormatException();
-		// Convert the byte arrays to batch IDs
-		List<BatchId> batches = new ArrayList<BatchId>();
+		// Convert the byte arrays to message IDs
+		List<MessageId> acked = new ArrayList<MessageId>();
 		for(Bytes b : raw) {
 			if(b.getBytes().length != UniqueId.LENGTH)
 				throw new FormatException();
-			batches.add(new BatchId(b.getBytes()));
+			acked.add(new MessageId(b.getBytes()));
 		}
 		// Build and return the ack
-		return packetFactory.createAck(Collections.unmodifiableList(batches));
+		return packetFactory.createAck(Collections.unmodifiableList(acked));
 	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/BatchImpl.java b/briar-core/src/net/sf/briar/protocol/BatchImpl.java
deleted file mode 100644
index 86f54548ea..0000000000
--- a/briar-core/src/net/sf/briar/protocol/BatchImpl.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Collection;
-
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.Message;
-
-/** A simple in-memory implementation of a batch. */
-class BatchImpl implements Batch {
-
-	private final BatchId id;
-	private final Collection<Message> messages;
-
-	BatchImpl(BatchId id, Collection<Message> messages) {
-		this.id = id;
-		this.messages = messages;
-	}
-
-	public BatchId getId() {
-		return id;
-	}
-
-	public Collection<Message> getMessages() {
-		return messages;
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/BatchReader.java b/briar-core/src/net/sf/briar/protocol/BatchReader.java
deleted file mode 100644
index e650ac9bb3..0000000000
--- a/briar-core/src/net/sf/briar/protocol/BatchReader.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-
-import java.io.IOException;
-import java.util.List;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.StructReader;
-import net.sf.briar.api.serial.Reader;
-
-class BatchReader implements StructReader<UnverifiedBatch> {
-
-	private final StructReader<UnverifiedMessage> messageReader;
-	private final UnverifiedBatchFactory batchFactory;
-
-	BatchReader(StructReader<UnverifiedMessage> messageReader,
-			UnverifiedBatchFactory batchFactory) {
-		this.messageReader = messageReader;
-		this.batchFactory = batchFactory;
-	}
-
-	public UnverifiedBatch readStruct(Reader r) throws IOException {
-		// Initialise the consumer
-		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
-		// Read the data
-		r.addConsumer(counting);
-		r.readStructId(Types.BATCH);
-		r.addStructReader(Types.MESSAGE, messageReader);
-		List<UnverifiedMessage> messages = r.readList(UnverifiedMessage.class);
-		r.removeStructReader(Types.MESSAGE);
-		r.removeConsumer(counting);
-		if(messages.isEmpty()) throw new FormatException();
-		// Build and return the batch
-		return batchFactory.createUnverifiedBatch( messages);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/MessageReader.java b/briar-core/src/net/sf/briar/protocol/MessageReader.java
index cd62801904..97e9871e40 100644
--- a/briar-core/src/net/sf/briar/protocol/MessageReader.java
+++ b/briar-core/src/net/sf/briar/protocol/MessageReader.java
@@ -14,10 +14,11 @@ import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.UniqueId;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.serial.CopyingConsumer;
 import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.StructReader;
 import net.sf.briar.api.serial.Reader;
+import net.sf.briar.api.serial.StructReader;
 
 class MessageReader implements StructReader<UnverifiedMessage> {
 
diff --git a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchImpl.java b/briar-core/src/net/sf/briar/protocol/MessageVerifierImpl.java
similarity index 54%
rename from briar-core/src/net/sf/briar/protocol/UnverifiedBatchImpl.java
rename to briar-core/src/net/sf/briar/protocol/MessageVerifierImpl.java
index 8f1e22c582..cb445a8c9b 100644
--- a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/MessageVerifierImpl.java
@@ -3,63 +3,44 @@ package net.sf.briar.protocol;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
 import java.security.Signature;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
 
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.crypto.MessageDigest;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.UnverifiedBatch;
+import net.sf.briar.api.protocol.MessageVerifier;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 
-class UnverifiedBatchImpl implements UnverifiedBatch {
+import com.google.inject.Inject;
 
-	private final CryptoComponent crypto;
-	private final Collection<UnverifiedMessage> messages;
-	private final MessageDigest batchDigest, messageDigest;
+class MessageVerifierImpl implements MessageVerifier {
 
-	// Initialise lazily - the batch may contain unsigned messages
-	private KeyParser keyParser = null;
-	private Signature signature = null;
+	private final CryptoComponent crypto;
+	private final KeyParser keyParser;
 
-	UnverifiedBatchImpl(CryptoComponent crypto,
-			Collection<UnverifiedMessage> messages) {
+	@Inject
+	MessageVerifierImpl(CryptoComponent crypto) {
 		this.crypto = crypto;
-		this.messages = messages;
-		batchDigest = crypto.getMessageDigest();
-		messageDigest = crypto.getMessageDigest();
-	}
-
-	public Batch verify() throws GeneralSecurityException {
-		List<Message> verified = new ArrayList<Message>();
-		for(UnverifiedMessage m : messages) verified.add(verify(m));
-		BatchId id = new BatchId(batchDigest.digest());
-		return new BatchImpl(id, Collections.unmodifiableList(verified));
+		keyParser = crypto.getSignatureKeyParser();
 	}
 
-	private Message verify(UnverifiedMessage m)
-	throws GeneralSecurityException {
-		// The batch ID is the hash of the concatenated messages
-		byte[] raw = m.getRaw();
-		batchDigest.update(raw);
+	public Message verifyMessage(UnverifiedMessage m)
+			throws GeneralSecurityException {
+		MessageDigest messageDigest = crypto.getMessageDigest();
+		Signature signature = crypto.getSignature();
 		// Hash the message, including the signatures, to get the message ID
+		byte[] raw = m.getSerialised();
 		messageDigest.update(raw);
 		MessageId id = new MessageId(messageDigest.digest());
 		// Verify the author's signature, if there is one
 		Author author = m.getAuthor();
 		if(author != null) {
-			if(keyParser == null) keyParser = crypto.getSignatureKeyParser();
 			PublicKey k = keyParser.parsePublicKey(author.getPublicKey());
-			if(signature == null) signature = crypto.getSignature();
 			signature.initVerify(k);
 			signature.update(raw, 0, m.getLengthSignedByAuthor());
 			if(!signature.verify(m.getAuthorSignature()))
@@ -68,9 +49,7 @@ class UnverifiedBatchImpl implements UnverifiedBatch {
 		// Verify the group's signature, if there is one
 		Group group = m.getGroup();
 		if(group != null && group.getPublicKey() != null) {
-			if(keyParser == null) keyParser = crypto.getSignatureKeyParser();
 			PublicKey k = keyParser.parsePublicKey(group.getPublicKey());
-			if(signature == null) signature = crypto.getSignature();
 			signature.initVerify(k);
 			signature.update(raw, 0, m.getLengthSignedByGroup());
 			if(!signature.verify(m.getGroupSignature()))
diff --git a/briar-core/src/net/sf/briar/protocol/PacketFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/PacketFactoryImpl.java
index 75885951d9..a8e9e32c20 100644
--- a/briar-core/src/net/sf/briar/protocol/PacketFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/PacketFactoryImpl.java
@@ -4,42 +4,23 @@ import java.util.BitSet;
 import java.util.Collection;
 import java.util.Map;
 
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.crypto.MessageDigest;
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportUpdate;
 
-import com.google.inject.Inject;
-
 class PacketFactoryImpl implements PacketFactory {
 
-	private final CryptoComponent crypto;
-
-	@Inject
-	PacketFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public Ack createAck(Collection<BatchId> acked) {
+	public Ack createAck(Collection<MessageId> acked) {
 		return new AckImpl(acked);
 	}
 
-	public RawBatch createBatch(Collection<byte[]> messages) {
-		MessageDigest messageDigest = crypto.getMessageDigest();
-		for(byte[] raw : messages) messageDigest.update(raw);
-		return new RawBatchImpl(new BatchId(messageDigest.digest()), messages);
-	}
-
 	public Offer createOffer(Collection<MessageId> offered) {
 		return new OfferImpl(offered);
 	}
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolModule.java b/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
index 49b0bd55bc..075f4158b9 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
@@ -9,6 +9,7 @@ import net.sf.briar.api.protocol.AuthorFactory;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.MessageFactory;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
@@ -16,7 +17,7 @@ import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.UnverifiedBatch;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.serial.StructReader;
 import net.sf.briar.util.BoundedExecutor;
@@ -46,10 +47,10 @@ public class ProtocolModule extends AbstractModule {
 		bind(AuthorFactory.class).to(AuthorFactoryImpl.class);
 		bind(GroupFactory.class).to(GroupFactoryImpl.class);
 		bind(MessageFactory.class).to(MessageFactoryImpl.class);
+		bind(MessageVerifier.class).to(MessageVerifierImpl.class);
 		bind(PacketFactory.class).to(PacketFactoryImpl.class);
 		bind(ProtocolReaderFactory.class).to(ProtocolReaderFactoryImpl.class);
 		bind(ProtocolWriterFactory.class).to(ProtocolWriterFactoryImpl.class);
-		bind(UnverifiedBatchFactory.class).to(UnverifiedBatchFactoryImpl.class);
 		// The executor is bounded, so tasks must be independent and short-lived
 		bind(Executor.class).annotatedWith(
 				VerificationExecutor.class).toInstance(
@@ -68,13 +69,6 @@ public class ProtocolModule extends AbstractModule {
 		return new AuthorReader(crypto, authorFactory);
 	}
 
-	@Provides
-	StructReader<UnverifiedBatch> getBatchReader(
-			StructReader<UnverifiedMessage> messageReader,
-			UnverifiedBatchFactory batchFactory) {
-		return new BatchReader(messageReader, batchFactory);
-	}
-
 	@Provides
 	StructReader<Group> getGroupReader(CryptoComponent crypto) {
 		return new GroupReader(crypto);
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
index 0a2ff03307..3ac7649d52 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
@@ -9,9 +9,9 @@ import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-import net.sf.briar.api.serial.StructReader;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.serial.ReaderFactory;
+import net.sf.briar.api.serial.StructReader;
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -20,7 +20,7 @@ class ProtocolReaderFactoryImpl implements ProtocolReaderFactory {
 
 	private final ReaderFactory readerFactory;
 	private final Provider<StructReader<Ack>> ackProvider;
-	private final Provider<StructReader<UnverifiedBatch>> batchProvider;
+	private final Provider<StructReader<UnverifiedMessage>> messageProvider;
 	private final Provider<StructReader<Offer>> offerProvider;
 	private final Provider<StructReader<Request>> requestProvider;
 	private final Provider<StructReader<SubscriptionUpdate>> subscriptionProvider;
@@ -29,14 +29,14 @@ class ProtocolReaderFactoryImpl implements ProtocolReaderFactory {
 	@Inject
 	ProtocolReaderFactoryImpl(ReaderFactory readerFactory,
 			Provider<StructReader<Ack>> ackProvider,
-			Provider<StructReader<UnverifiedBatch>> batchProvider,
+			Provider<StructReader<UnverifiedMessage>> messageProvider,
 			Provider<StructReader<Offer>> offerProvider,
 			Provider<StructReader<Request>> requestProvider,
 			Provider<StructReader<SubscriptionUpdate>> subscriptionProvider,
 			Provider<StructReader<TransportUpdate>> transportProvider) {
 		this.readerFactory = readerFactory;
 		this.ackProvider = ackProvider;
-		this.batchProvider = batchProvider;
+		this.messageProvider = messageProvider;
 		this.offerProvider = offerProvider;
 		this.requestProvider = requestProvider;
 		this.subscriptionProvider = subscriptionProvider;
@@ -45,7 +45,8 @@ class ProtocolReaderFactoryImpl implements ProtocolReaderFactory {
 
 	public ProtocolReader createProtocolReader(InputStream in) {
 		return new ProtocolReaderImpl(in, readerFactory, ackProvider.get(),
-				batchProvider.get(), offerProvider.get(), requestProvider.get(),
-				subscriptionProvider.get(), transportProvider.get());
+				messageProvider.get(), offerProvider.get(),
+				requestProvider.get(), subscriptionProvider.get(),
+				transportProvider.get());
 	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java b/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
index cf6ff211f4..05c2562964 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
@@ -10,10 +10,10 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-import net.sf.briar.api.serial.StructReader;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
+import net.sf.briar.api.serial.StructReader;
 
 class ProtocolReaderImpl implements ProtocolReader {
 
@@ -21,14 +21,14 @@ class ProtocolReaderImpl implements ProtocolReader {
 
 	ProtocolReaderImpl(InputStream in, ReaderFactory readerFactory,
 			StructReader<Ack> ackReader,
-			StructReader<UnverifiedBatch> batchReader,
+			StructReader<UnverifiedMessage> messageReader,
 			StructReader<Offer> offerReader,
 			StructReader<Request> requestReader,
 			StructReader<SubscriptionUpdate> subscriptionReader,
 			StructReader<TransportUpdate> transportReader) {
 		reader = readerFactory.createReader(in);
 		reader.addStructReader(Types.ACK, ackReader);
-		reader.addStructReader(Types.BATCH, batchReader);
+		reader.addStructReader(Types.MESSAGE, messageReader);
 		reader.addStructReader(Types.OFFER, offerReader);
 		reader.addStructReader(Types.REQUEST, requestReader);
 		reader.addStructReader(Types.SUBSCRIPTION_UPDATE, subscriptionReader);
@@ -47,12 +47,12 @@ class ProtocolReaderImpl implements ProtocolReader {
 		return reader.readStruct(Types.ACK, Ack.class);
 	}
 
-	public boolean hasBatch() throws IOException {
-		return reader.hasStruct(Types.BATCH);
+	public boolean hasMessage() throws IOException {
+		return reader.hasStruct(Types.MESSAGE);
 	}
 
-	public UnverifiedBatch readBatch() throws IOException {
-		return reader.readStruct(Types.BATCH, UnverifiedBatch.class);
+	public UnverifiedMessage readMessage() throws IOException {
+		return reader.readStruct(Types.MESSAGE, UnverifiedMessage.class);
 	}
 
 	public boolean hasOffer() throws IOException {
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolWriterImpl.java b/briar-core/src/net/sf/briar/protocol/ProtocolWriterImpl.java
index fd100d561e..0fda305552 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolWriterImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolWriterImpl.java
@@ -8,13 +8,11 @@ import java.util.BitSet;
 import java.util.Map.Entry;
 
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolWriter;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
@@ -40,11 +38,11 @@ class ProtocolWriterImpl implements ProtocolWriter {
 		w = writerFactory.createWriter(out);
 	}
 
-	public int getMaxBatchesForAck(long capacity) {
+	public int getMaxMessagesForAck(long capacity) {
 		int packet = (int) Math.min(capacity, MAX_PACKET_LENGTH);
 		int overhead = serial.getSerialisedStructIdLength(Types.ACK)
-		+ serial.getSerialisedListStartLength()
-		+ serial.getSerialisedListEndLength();
+				+ serial.getSerialisedListStartLength()
+				+ serial.getSerialisedListEndLength();
 		int idLength = serial.getSerialisedUniqueIdLength();
 		return (packet - overhead) / idLength;
 	}
@@ -52,33 +50,22 @@ class ProtocolWriterImpl implements ProtocolWriter {
 	public int getMaxMessagesForOffer(long capacity) {
 		int packet = (int) Math.min(capacity, MAX_PACKET_LENGTH);
 		int overhead = serial.getSerialisedStructIdLength(Types.OFFER)
-		+ serial.getSerialisedListStartLength()
-		+ serial.getSerialisedListEndLength();
+				+ serial.getSerialisedListStartLength()
+				+ serial.getSerialisedListEndLength();
 		int idLength = serial.getSerialisedUniqueIdLength();
 		return (packet - overhead) / idLength;
 	}
 
-	public int getMessageCapacityForBatch(long capacity) {
-		int packet = (int) Math.min(capacity, MAX_PACKET_LENGTH);
-		int overhead = serial.getSerialisedStructIdLength(Types.BATCH)
-		+ serial.getSerialisedListStartLength()
-		+ serial.getSerialisedListEndLength();
-		return packet - overhead;
-	}
-
 	public void writeAck(Ack a) throws IOException {
 		w.writeStructId(Types.ACK);
 		w.writeListStart();
-		for(BatchId b : a.getBatchIds()) w.writeBytes(b.getBytes());
+		for(MessageId m : a.getMessageIds()) w.writeBytes(m.getBytes());
 		w.writeListEnd();
 		if(flush) out.flush();
 	}
 
-	public void writeBatch(RawBatch b) throws IOException {
-		w.writeStructId(Types.BATCH);
-		w.writeListStart();
-		for(byte[] raw : b.getMessages()) out.write(raw);
-		w.writeListEnd();
+	public void writeMessage(byte[] raw) throws IOException {
+		out.write(raw);
 		if(flush) out.flush();
 	}
 
@@ -111,7 +98,7 @@ class ProtocolWriterImpl implements ProtocolWriter {
 	}
 
 	public void writeSubscriptionUpdate(SubscriptionUpdate s)
-	throws IOException {
+			throws IOException {
 		w.writeStructId(Types.SUBSCRIPTION_UPDATE);
 		// Holes
 		w.writeMapStart();
diff --git a/briar-core/src/net/sf/briar/protocol/RawBatchImpl.java b/briar-core/src/net/sf/briar/protocol/RawBatchImpl.java
deleted file mode 100644
index 06da27023c..0000000000
--- a/briar-core/src/net/sf/briar/protocol/RawBatchImpl.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Collection;
-
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.RawBatch;
-
-class RawBatchImpl implements RawBatch {
-
-	private final BatchId id;
-	private final Collection<byte[]> messages;
-
-	RawBatchImpl(BatchId id, Collection<byte[]> messages) {
-		this.id = id;
-		this.messages = messages;
-	}
-
-	public BatchId getId() {
-		return id;
-	}
-
-	public Collection<byte[]> getMessages() {
-		return messages;
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactory.java b/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactory.java
deleted file mode 100644
index 93e20ac8aa..0000000000
--- a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactory.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Collection;
-
-import net.sf.briar.api.protocol.UnverifiedBatch;
-
-interface UnverifiedBatchFactory {
-
-	UnverifiedBatch createUnverifiedBatch(
-			Collection<UnverifiedMessage> messages);
-}
diff --git a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactoryImpl.java
deleted file mode 100644
index 980c09db57..0000000000
--- a/briar-core/src/net/sf/briar/protocol/UnverifiedBatchFactoryImpl.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Collection;
-
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-
-import com.google.inject.Inject;
-
-class UnverifiedBatchFactoryImpl implements UnverifiedBatchFactory {
-
-	private final CryptoComponent crypto;
-
-	@Inject
-	UnverifiedBatchFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public UnverifiedBatch createUnverifiedBatch(
-			Collection<UnverifiedMessage> messages) {
-		return new UnverifiedBatchImpl(crypto, messages);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/UnverifiedMessageImpl.java b/briar-core/src/net/sf/briar/protocol/UnverifiedMessageImpl.java
index dd3f941c62..57cdb0fc69 100644
--- a/briar-core/src/net/sf/briar/protocol/UnverifiedMessageImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/UnverifiedMessageImpl.java
@@ -3,6 +3,7 @@ package net.sf.briar.protocol;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 
 class UnverifiedMessageImpl implements UnverifiedMessage {
 
@@ -52,7 +53,7 @@ class UnverifiedMessageImpl implements UnverifiedMessage {
 		return timestamp;
 	}
 
-	public byte[] getRaw() {
+	public byte[] getSerialised() {
 		return raw;
 	}
 
diff --git a/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnection.java b/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnection.java
index 4eb35d8b70..24d084bc85 100644
--- a/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnection.java
+++ b/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnection.java
@@ -24,28 +24,28 @@ import net.sf.briar.api.FormatException;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.db.DbException;
-import net.sf.briar.api.db.event.BatchReceivedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.MessagesAddedEvent;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageReceivedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriter;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.UnverifiedBatch;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
@@ -76,6 +76,7 @@ abstract class DuplexConnection implements DatabaseListener {
 	protected final TransportId transportId;
 
 	private final Executor dbExecutor, verificationExecutor;
+	private final MessageVerifier messageVerifier;
 	private final AtomicBoolean canSendOffer, disposed;
 	private final BlockingQueue<Runnable> writerTasks;
 
@@ -85,7 +86,8 @@ abstract class DuplexConnection implements DatabaseListener {
 
 	DuplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
@@ -93,6 +95,7 @@ abstract class DuplexConnection implements DatabaseListener {
 			DuplexTransportConnection transport) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
+		this.messageVerifier = messageVerifier;
 		this.db = db;
 		this.connRegistry = connRegistry;
 		this.connReaderFactory = connReaderFactory;
@@ -115,12 +118,12 @@ abstract class DuplexConnection implements DatabaseListener {
 			throws IOException;
 
 	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof BatchReceivedEvent) {
+		if(e instanceof MessageReceivedEvent) {
 			dbExecutor.execute(new GenerateAcks());
 		} else if(e instanceof ContactRemovedEvent) {
 			ContactId c = ((ContactRemovedEvent) e).getContactId();
 			if(contactId.equals(c)) dispose(false, true);
-		} else if(e instanceof MessagesAddedEvent) {
+		} else if(e instanceof MessageAddedEvent) {
 			if(canSendOffer.getAndSet(false))
 				dbExecutor.execute(new GenerateOffer());
 		} else if(e instanceof SubscriptionsUpdatedEvent) {
@@ -142,9 +145,9 @@ abstract class DuplexConnection implements DatabaseListener {
 				if(reader.hasAck()) {
 					Ack a = reader.readAck();
 					dbExecutor.execute(new ReceiveAck(a));
-				} else if(reader.hasBatch()) {
-					UnverifiedBatch b = reader.readBatch();
-					verificationExecutor.execute(new VerifyBatch(b));
+				} else if(reader.hasMessage()) {
+					UnverifiedMessage m = reader.readMessage();
+					verificationExecutor.execute(new VerifyMessage(m));
 				} else if(reader.hasOffer()) {
 					Offer o = reader.readOffer();
 					dbExecutor.execute(new ReceiveOffer(o));
@@ -260,18 +263,18 @@ abstract class DuplexConnection implements DatabaseListener {
 	}
 
 	// This task runs on a verification thread
-	private class VerifyBatch implements Runnable {
+	private class VerifyMessage implements Runnable {
 
-		private final UnverifiedBatch batch;
+		private final UnverifiedMessage message;
 
-		private VerifyBatch(UnverifiedBatch batch) {
-			this.batch = batch;
+		private VerifyMessage(UnverifiedMessage message) {
+			this.message = message;
 		}
 
 		public void run() {
 			try {
-				Batch b = batch.verify();
-				dbExecutor.execute(new ReceiveBatch(b));
+				Message m = messageVerifier.verifyMessage(message);
+				dbExecutor.execute(new ReceiveMessage(m));
 			} catch(GeneralSecurityException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
@@ -279,17 +282,17 @@ abstract class DuplexConnection implements DatabaseListener {
 	}
 
 	// This task runs on a database thread
-	private class ReceiveBatch implements Runnable {
+	private class ReceiveMessage implements Runnable {
 
-		private final Batch batch;
+		private final Message message;
 
-		private ReceiveBatch(Batch batch) {
-			this.batch = batch;
+		private ReceiveMessage(Message message) {
+			this.message = message;
 		}
 
 		public void run() {
 			try {
-				db.receiveBatch(contactId, batch);
+				db.receiveMessage(contactId, message);
 			} catch(DbException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
@@ -394,9 +397,9 @@ abstract class DuplexConnection implements DatabaseListener {
 
 		public void run() {
 			assert writer != null;
-			int maxBatches = writer.getMaxBatchesForAck(Long.MAX_VALUE);
+			int maxMessages = writer.getMaxMessagesForAck(Long.MAX_VALUE);
 			try {
-				Ack a = db.generateAck(contactId, maxBatches);
+				Ack a = db.generateAck(contactId, maxMessages);
 				if(a != null) writerTasks.add(new WriteAck(a));
 			} catch(DbException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -436,11 +439,11 @@ abstract class DuplexConnection implements DatabaseListener {
 
 		public void run() {
 			assert writer != null;
-			int capacity = writer.getMessageCapacityForBatch(Long.MAX_VALUE);
 			try {
-				RawBatch b = db.generateBatch(contactId, capacity, requested);
-				if(b == null) new GenerateOffer().run();
-				else writerTasks.add(new WriteBatch(b, requested));
+				Collection<byte[]> batch = db.generateBatch(contactId,
+						Integer.MAX_VALUE, requested);
+				if(batch == null) new GenerateOffer().run();
+				else writerTasks.add(new WriteBatch(batch, requested));
 			} catch(DbException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
@@ -450,10 +453,11 @@ abstract class DuplexConnection implements DatabaseListener {
 	// This task runs on the writer thread
 	private class WriteBatch implements Runnable {
 
-		private final RawBatch batch;
+		private final Collection<byte[]> batch;
 		private final Collection<MessageId> requested;
 
-		private WriteBatch(RawBatch batch, Collection<MessageId> requested) {
+		private WriteBatch(Collection<byte[]> batch,
+				Collection<MessageId> requested) {
 			this.batch = batch;
 			this.requested = requested;
 		}
@@ -461,7 +465,7 @@ abstract class DuplexConnection implements DatabaseListener {
 		public void run() {
 			assert writer != null;
 			try {
-				writer.writeBatch(batch);
+				for(byte[] raw : batch) writer.writeMessage(raw);
 				if(requested.isEmpty()) dbExecutor.execute(new GenerateOffer());
 				else dbExecutor.execute(new GenerateBatches(requested));
 			} catch(IOException e) {
diff --git a/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
index c005162d39..e60f9c0be7 100644
--- a/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
@@ -10,6 +10,7 @@ import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.TransportId;
@@ -28,6 +29,7 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 			Logger.getLogger(DuplexConnectionFactoryImpl.class.getName());
 
 	private final Executor dbExecutor, verificationExecutor;
+	private final MessageVerifier messageVerifier;
 	private final DatabaseComponent db;
 	private final KeyManager keyManager;
 	private final ConnectionRegistry connRegistry;
@@ -39,13 +41,14 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 	@Inject
 	DuplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, KeyManager keyManager,
-			ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			KeyManager keyManager, ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory, ProtocolWriterFactory protoWriterFactory) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
+		this.messageVerifier = messageVerifier;
 		this.db = db;
 		this.keyManager = keyManager;
 		this.connRegistry = connRegistry;
@@ -58,9 +61,9 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 	public void createIncomingConnection(ConnectionContext ctx,
 			DuplexTransportConnection transport) {
 		final DuplexConnection conn = new IncomingDuplexConnection(dbExecutor,
-				verificationExecutor, db, connRegistry, connReaderFactory,
-				connWriterFactory, protoReaderFactory, protoWriterFactory, ctx,
-				transport);
+				verificationExecutor, messageVerifier, db, connRegistry,
+				connReaderFactory, connWriterFactory, protoReaderFactory,
+				protoWriterFactory, ctx, transport);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
@@ -84,9 +87,9 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 			return;
 		}
 		final DuplexConnection conn = new OutgoingDuplexConnection(dbExecutor,
-				verificationExecutor, db, connRegistry, connReaderFactory,
-				connWriterFactory, protoReaderFactory, protoWriterFactory, ctx,
-				transport);
+				verificationExecutor, messageVerifier, db, connRegistry,
+				connReaderFactory, connWriterFactory, protoReaderFactory,
+				protoWriterFactory, ctx, transport);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
diff --git a/briar-core/src/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java b/briar-core/src/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
index 3ec37b99cf..2f5b006eca 100644
--- a/briar-core/src/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
+++ b/briar-core/src/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
@@ -6,6 +6,7 @@ import java.util.concurrent.Executor;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.VerificationExecutor;
@@ -20,15 +21,16 @@ class IncomingDuplexConnection extends DuplexConnection {
 
 	IncomingDuplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
 			ProtocolWriterFactory protoWriterFactory,
 			ConnectionContext ctx, DuplexTransportConnection transport) {
-		super(dbExecutor, verificationExecutor, db, connRegistry,
-				connReaderFactory, connWriterFactory, protoReaderFactory,
-				protoWriterFactory, ctx, transport);
+		super(dbExecutor, verificationExecutor, messageVerifier, db,
+				connRegistry, connReaderFactory, connWriterFactory,
+				protoReaderFactory, protoWriterFactory, ctx, transport);
 	}
 
 	@Override
diff --git a/briar-core/src/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java b/briar-core/src/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
index 8be0202ff6..4b67f433fb 100644
--- a/briar-core/src/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
+++ b/briar-core/src/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
@@ -6,6 +6,7 @@ import java.util.concurrent.Executor;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.VerificationExecutor;
@@ -20,15 +21,16 @@ class OutgoingDuplexConnection extends DuplexConnection {
 
 	OutgoingDuplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
 			ProtocolWriterFactory protoWriterFactory, ConnectionContext ctx,
 			DuplexTransportConnection transport) {
-		super(dbExecutor, verificationExecutor, db, connRegistry,
-				connReaderFactory, connWriterFactory, protoReaderFactory,
-				protoWriterFactory, ctx, transport);
+		super(dbExecutor, verificationExecutor, messageVerifier, db,
+				connRegistry, connReaderFactory, connWriterFactory,
+				protoReaderFactory, protoWriterFactory, ctx, transport);
 	}
 
 	@Override
diff --git a/briar-core/src/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java b/briar-core/src/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
index 24ae0701b8..ffc6b8479d 100644
--- a/briar-core/src/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
+++ b/briar-core/src/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
@@ -15,13 +15,14 @@ import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.UnverifiedBatch;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
@@ -35,6 +36,7 @@ class IncomingSimplexConnection {
 			Logger.getLogger(IncomingSimplexConnection.class.getName());
 
 	private final Executor dbExecutor, verificationExecutor;
+	private final MessageVerifier messageVerifier;
 	private final DatabaseComponent db;
 	private final ConnectionRegistry connRegistry;
 	private final ConnectionReaderFactory connFactory;
@@ -46,12 +48,14 @@ class IncomingSimplexConnection {
 
 	IncomingSimplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connFactory,
 			ProtocolReaderFactory protoFactory, ConnectionContext ctx,
 			SimplexTransportReader transport) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
+		this.messageVerifier = messageVerifier;
 		this.db = db;
 		this.connRegistry = connRegistry;
 		this.connFactory = connFactory;
@@ -74,9 +78,9 @@ class IncomingSimplexConnection {
 				if(reader.hasAck()) {
 					Ack a = reader.readAck();
 					dbExecutor.execute(new ReceiveAck(a));
-				} else if(reader.hasBatch()) {
-					UnverifiedBatch b = reader.readBatch();
-					verificationExecutor.execute(new VerifyBatch(b));
+				} else if(reader.hasMessage()) {
+					UnverifiedMessage m = reader.readMessage();
+					verificationExecutor.execute(new VerifyMessage(m));
 				} else if(reader.hasSubscriptionUpdate()) {
 					SubscriptionUpdate s = reader.readSubscriptionUpdate();
 					dbExecutor.execute(new ReceiveSubscriptionUpdate(s));
@@ -122,35 +126,35 @@ class IncomingSimplexConnection {
 		}
 	}
 
-	private class VerifyBatch implements Runnable {
+	private class VerifyMessage implements Runnable {
 
-		private final UnverifiedBatch batch;
+		private final UnverifiedMessage message;
 
-		private VerifyBatch(UnverifiedBatch batch) {
-			this.batch = batch;
+		private VerifyMessage(UnverifiedMessage message) {
+			this.message = message;
 		}
 
 		public void run() {
 			try {
-				Batch b = batch.verify();
-				dbExecutor.execute(new ReceiveBatch(b));
+				Message m = messageVerifier.verifyMessage(message);
+				dbExecutor.execute(new ReceiveMessage(m));
 			} catch(GeneralSecurityException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
 		}
 	}
 
-	private class ReceiveBatch implements Runnable {
+	private class ReceiveMessage implements Runnable {
 
-		private final Batch batch;
+		private final Message message;
 
-		private ReceiveBatch(Batch batch) {
-			this.batch = batch;
+		private ReceiveMessage(Message message) {
+			this.message = message;
 		}
 
 		public void run() {
 			try {
-				db.receiveBatch(contactId, batch);
+				db.receiveMessage(contactId, message);
 			} catch(DbException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
diff --git a/briar-core/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java b/briar-core/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
index eea83eeddf..e53facc463 100644
--- a/briar-core/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
+++ b/briar-core/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
@@ -6,6 +6,7 @@ import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Collection;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
@@ -15,7 +16,6 @@ import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.ProtocolWriter;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
@@ -77,23 +77,23 @@ class OutgoingSimplexConnection {
 			}
 			// Write acks until you can't write acks no more
 			capacity = conn.getRemainingCapacity();
-			int maxBatches = writer.getMaxBatchesForAck(capacity);
-			Ack a = db.generateAck(contactId, maxBatches);
+			int maxMessages = writer.getMaxMessagesForAck(capacity);
+			Ack a = db.generateAck(contactId, maxMessages);
 			while(a != null) {
 				writer.writeAck(a);
 				capacity = conn.getRemainingCapacity();
-				maxBatches = writer.getMaxBatchesForAck(capacity);
-				a = db.generateAck(contactId, maxBatches);
+				maxMessages = writer.getMaxMessagesForAck(capacity);
+				a = db.generateAck(contactId, maxMessages);
 			}
-			// Write batches until you can't write batches no more
+			// Write messages until you can't write messages no more
 			capacity = conn.getRemainingCapacity();
-			capacity = writer.getMessageCapacityForBatch(capacity);
-			RawBatch b = db.generateBatch(contactId, (int) capacity);
-			while(b != null) {
-				writer.writeBatch(b);
+			int maxLength = (int) Math.min(capacity, MAX_PACKET_LENGTH);
+			Collection<byte[]> batch = db.generateBatch(contactId, maxLength);
+			while(batch != null) {
+				for(byte[] raw : batch) writer.writeMessage(raw);
 				capacity = conn.getRemainingCapacity();
-				capacity = writer.getMessageCapacityForBatch(capacity);
-				b = db.generateBatch(contactId, (int) capacity);
+				maxLength = (int) Math.min(capacity, MAX_PACKET_LENGTH);
+				batch = db.generateBatch(contactId, maxLength);
 			}
 			writer.flush();
 			writer.close();
diff --git a/briar-core/src/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
index e8cb98768d..6f6736ca3c 100644
--- a/briar-core/src/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
@@ -11,6 +11,7 @@ import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.TransportId;
@@ -29,6 +30,7 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 			Logger.getLogger(SimplexConnectionFactoryImpl.class.getName());
 
 	private final Executor dbExecutor, verificationExecutor;
+	private final MessageVerifier messageVerifier;
 	private final DatabaseComponent db;
 	private final KeyManager keyManager;
 	private final ConnectionRegistry connRegistry;
@@ -40,14 +42,15 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 	@Inject
 	SimplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, KeyManager keyManager,
-			ConnectionRegistry connRegistry,
+			MessageVerifier messageVerifier, DatabaseComponent db,
+			KeyManager keyManager, ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
 			ProtocolWriterFactory protoWriterFactory) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
+		this.messageVerifier = messageVerifier;
 		this.db = db;
 		this.keyManager = keyManager;
 		this.connRegistry = connRegistry;
@@ -59,8 +62,8 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 
 	public void createIncomingConnection(ConnectionContext ctx, SimplexTransportReader r) {
 		final IncomingSimplexConnection conn = new IncomingSimplexConnection(
-				dbExecutor, verificationExecutor, db, connRegistry,
-				connReaderFactory, protoReaderFactory, ctx, r);
+				dbExecutor, verificationExecutor, messageVerifier, db,
+				connRegistry, connReaderFactory, protoReaderFactory, ctx, r);
 		Runnable read = new Runnable() {
 			public void run() {
 				conn.read();
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index f1171d9052..5211bfe17a 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -89,14 +89,11 @@
 			<test name='net.sf.briar.plugins.modem.ModemPluginTest'/>
 			<test name='net.sf.briar.plugins.tcp.LanTcpPluginTest'/>
 			<test name='net.sf.briar.protocol.AckReaderTest'/>
-			<test name='net.sf.briar.protocol.BatchReaderTest'/>
 			<test name='net.sf.briar.protocol.ConstantsTest'/>
 			<test name='net.sf.briar.protocol.ConsumersTest'/>
 			<test name='net.sf.briar.protocol.OfferReaderTest'/>
-			<test name='net.sf.briar.protocol.ProtocolIntegrationTest'/>
 			<test name='net.sf.briar.protocol.ProtocolWriterImplTest'/>
 			<test name='net.sf.briar.protocol.RequestReaderTest'/>
-			<test name='net.sf.briar.protocol.UnverifiedBatchImplTest'/>
 			<test name='net.sf.briar.protocol.simplex.OutgoingSimplexConnectionTest'/>
 			<test name='net.sf.briar.protocol.simplex.SimplexProtocolIntegrationTest'/>
 			<test name='net.sf.briar.serial.ReaderImplTest'/>
diff --git a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java
index bbd7af2f9a..c0be507489 100644
--- a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java
+++ b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java
@@ -12,7 +12,6 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Random;
@@ -22,26 +21,25 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageFactory;
 import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriter;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
@@ -64,15 +62,13 @@ import com.google.inject.Injector;
 
 public class ProtocolIntegrationTest extends BriarTestCase {
 
-	private final BatchId ack = new BatchId(TestUtils.getRandomId());
-	private final long timestamp = System.currentTimeMillis();
-
 	private final ConnectionReaderFactory connectionReaderFactory;
 	private final ConnectionWriterFactory connectionWriterFactory;
 	private final ProtocolReaderFactory protocolReaderFactory;
 	private final ProtocolWriterFactory protocolWriterFactory;
 	private final PacketFactory packetFactory;
-	private final CryptoComponent crypto;
+	private final MessageVerifier messageVerifier;
+
 	private final ContactId contactId;
 	private final TransportId transportId;
 	private final byte[] secret;
@@ -82,30 +78,32 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 	private final String authorName = "Alice";
 	private final String subject = "Hello";
 	private final String messageBody = "Hello world";
+	private final Collection<MessageId> messageIds;
 	private final Collection<Transport> transports;
+	private final long timestamp = System.currentTimeMillis();
 
 	public ProtocolIntegrationTest() throws Exception {
 		super();
-		Injector i = Guice.createInjector(new ClockModule(), new CryptoModule(),
-				new DatabaseModule(), new LifecycleModule(),
-				new ProtocolModule(), new SerialModule(),
-				new TestDatabaseModule(), new SimplexProtocolModule(),
-				new TransportModule(), new DuplexProtocolModule());
+		Injector i = Guice.createInjector(new TestDatabaseModule(),
+				new ClockModule(), new CryptoModule(), new DatabaseModule(),
+				new LifecycleModule(), new ProtocolModule(),
+				new DuplexProtocolModule(), new SimplexProtocolModule(),
+				new SerialModule(), new TransportModule());
 		connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class);
 		connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
 		protocolReaderFactory = i.getInstance(ProtocolReaderFactory.class);
 		protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class);
 		packetFactory = i.getInstance(PacketFactory.class);
-		crypto = i.getInstance(CryptoComponent.class);
+		messageVerifier = i.getInstance(MessageVerifier.class);
 		contactId = new ContactId(234);
 		transportId = new TransportId(TestUtils.getRandomId());
 		// Create a shared secret
-		Random r = new Random();
 		secret = new byte[32];
-		r.nextBytes(secret);
+		new Random().nextBytes(secret);
 		// Create two groups: one restricted, one unrestricted
 		GroupFactory groupFactory = i.getInstance(GroupFactory.class);
 		group = groupFactory.createGroup("Unrestricted group", null);
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
 		KeyPair groupKeyPair = crypto.generateSignatureKeyPair();
 		group1 = groupFactory.createGroup("Restricted group",
 				groupKeyPair.getPublic().getEncoded());
@@ -127,6 +125,8 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		message3 = messageFactory.createMessage(null, group1,
 				groupKeyPair.getPrivate(), author, authorKeyPair.getPrivate(),
 				subject, messageBody.getBytes("UTF-8"));
+		messageIds = Arrays.asList(message.getId(),
+				message1.getId(), message2.getId(), message3.getId());
 		// Create some transports
 		TransportId transportId = new TransportId(TestUtils.getRandomId());
 		Transport transport = new Transport(transportId,
@@ -149,18 +149,15 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out1,
 				false);
 
-		Ack a = packetFactory.createAck(Collections.singletonList(ack));
+		Ack a = packetFactory.createAck(messageIds);
 		writer.writeAck(a);
 
-		Collection<byte[]> batch = Arrays.asList(message.getSerialised(),
-				message1.getSerialised(), message2.getSerialised(),
-				message3.getSerialised());
-		RawBatch b = packetFactory.createBatch(batch);
-		writer.writeBatch(b);
+		writer.writeMessage(message.getSerialised());
+		writer.writeMessage(message1.getSerialised());
+		writer.writeMessage(message2.getSerialised());
+		writer.writeMessage(message3.getSerialised());
 
-		Collection<MessageId> offer = Arrays.asList(message.getId(),
-				message1.getId(), message2.getId(), message3.getId());
-		Offer o = packetFactory.createOffer(offer);
+		Offer o = packetFactory.createOffer(messageIds);
 		writer.writeOffer(o);
 
 		BitSet requested = new BitSet(4);
@@ -200,29 +197,26 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		// Read the ack
 		assertTrue(reader.hasAck());
 		Ack a = reader.readAck();
-		assertEquals(Collections.singletonList(ack), a.getBatchIds());
+		assertEquals(messageIds, a.getMessageIds());
 
-		// Read and verify the batch
-		assertTrue(reader.hasBatch());
-		Batch b = reader.readBatch().verify();
-		Collection<Message> messages = b.getMessages();
-		assertEquals(4, messages.size());
-		Iterator<Message> it = messages.iterator();
-		checkMessageEquality(message, it.next());
-		checkMessageEquality(message1, it.next());
-		checkMessageEquality(message2, it.next());
-		checkMessageEquality(message3, it.next());
+		// Read and verify the messages
+		assertTrue(reader.hasMessage());
+		UnverifiedMessage m = reader.readMessage();
+		checkMessageEquality(message, messageVerifier.verifyMessage(m));
+		assertTrue(reader.hasMessage());
+		m = reader.readMessage();
+		checkMessageEquality(message1, messageVerifier.verifyMessage(m));
+		assertTrue(reader.hasMessage());
+		m = reader.readMessage();
+		checkMessageEquality(message2, messageVerifier.verifyMessage(m));
+		assertTrue(reader.hasMessage());
+		m = reader.readMessage();
+		checkMessageEquality(message3, messageVerifier.verifyMessage(m));
 
 		// Read the offer
 		assertTrue(reader.hasOffer());
 		Offer o = reader.readOffer();
-		Collection<MessageId> offered = o.getMessageIds();
-		assertEquals(4, offered.size());
-		Iterator<MessageId> it1 = offered.iterator();
-		assertEquals(message.getId(), it1.next());
-		assertEquals(message1.getId(), it1.next());
-		assertEquals(message2.getId(), it1.next());
-		assertEquals(message3.getId(), it1.next());
+		assertEquals(messageIds, o.getMessageIds());
 
 		// Read the request
 		assertTrue(reader.hasRequest());
diff --git a/briar-tests/src/net/sf/briar/db/TestMessage.java b/briar-tests/src/net/sf/briar/TestMessage.java
similarity index 96%
rename from briar-tests/src/net/sf/briar/db/TestMessage.java
rename to briar-tests/src/net/sf/briar/TestMessage.java
index bf25200e04..c66abc8bb0 100644
--- a/briar-tests/src/net/sf/briar/db/TestMessage.java
+++ b/briar-tests/src/net/sf/briar/TestMessage.java
@@ -1,4 +1,4 @@
-package net.sf.briar.db;
+package net.sf.briar;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
@@ -8,7 +8,7 @@ import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
-class TestMessage implements Message {
+public class TestMessage implements Message {
 
 	private final MessageId id, parent;
 	private final GroupId group;
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index aa598ac702..5a2fabba23 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -7,6 +7,7 @@ import java.util.Collection;
 import java.util.Collections;
 
 import net.sf.briar.BriarTestCase;
+import net.sf.briar.TestMessage;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
@@ -17,21 +18,18 @@ import net.sf.briar.api.db.NoSuchContactTransportException;
 import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessagesAddedEvent;
+import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
@@ -48,10 +46,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	protected final Object txn = new Object();
 	protected final AuthorId authorId;
-	protected final BatchId batchId;
 	protected final ContactId contactId;
 	protected final GroupId groupId;
-	protected final MessageId messageId, parentId;
+	protected final MessageId messageId, messageId1;
 	private final String subject;
 	private final long timestamp;
 	private final int size;
@@ -66,11 +63,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	public DatabaseComponentTest() {
 		super();
 		authorId = new AuthorId(TestUtils.getRandomId());
-		batchId = new BatchId(TestUtils.getRandomId());
 		contactId = new ContactId(234);
 		groupId = new GroupId(TestUtils.getRandomId());
 		messageId = new MessageId(TestUtils.getRandomId());
-		parentId = new MessageId(TestUtils.getRandomId());
+		messageId1 = new MessageId(TestUtils.getRandomId());
 		subject = "Foo";
 		timestamp = System.currentTimeMillis();
 		size = 1234;
@@ -250,11 +246,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setSendability(txn, messageId, 1);
 			// The parent exists, is in the DB, and is in the same group
 			oneOf(database).getGroupMessageParent(txn, messageId);
-			will(returnValue(parentId));
+			will(returnValue(messageId1));
 			// The parent is already sendable
-			oneOf(database).getSendability(txn, parentId);
+			oneOf(database).getSendability(txn, messageId1);
 			will(returnValue(1));
-			oneOf(database).setSendability(txn, parentId, 2);
+			oneOf(database).setSendability(txn, messageId1, 2);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
@@ -288,13 +284,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setSendability(txn, messageId, 1);
 			// The parent exists, is in the DB, and is in the same group
 			oneOf(database).getGroupMessageParent(txn, messageId);
-			will(returnValue(parentId));
+			will(returnValue(messageId1));
 			// The parent is not already sendable
-			oneOf(database).getSendability(txn, parentId);
+			oneOf(database).getSendability(txn, messageId1);
 			will(returnValue(0));
-			oneOf(database).setSendability(txn, parentId, 1);
+			oneOf(database).setSendability(txn, messageId1, 1);
 			// The parent has no parent
-			oneOf(database).getGroupMessageParent(txn, parentId);
+			oneOf(database).getGroupMessageParent(txn, messageId1);
 			will(returnValue(null));
 			oneOf(database).commitTransaction(txn);
 		}});
@@ -494,7 +490,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
 		final Ack ack = context.mock(Ack.class);
-		final Batch batch = context.mock(Batch.class);
 		final Offer offer = context.mock(Offer.class);
 		final SubscriptionUpdate subscriptionUpdate =
 				context.mock(SubscriptionUpdate.class);
@@ -563,7 +558,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		} catch(NoSuchContactException expected) {}
 
 		try {
-			db.receiveBatch(contactId, batch);
+			db.receiveMessage(contactId, message);
 			fail();
 		} catch(NoSuchContactException expected) {}
 
@@ -631,10 +626,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testGenerateAck() throws Exception {
-		final BatchId batchId1 = new BatchId(TestUtils.getRandomId());
-		final Collection<BatchId> batchesToAck = new ArrayList<BatchId>();
-		batchesToAck.add(batchId);
-		batchesToAck.add(batchId1);
+		final Collection<MessageId> messagesToAck = Arrays.asList(messageId,
+				messageId1);
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -648,14 +641,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).commitTransaction(txn);
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			// Get the batches to ack
-			oneOf(database).getBatchesToAck(txn, contactId, 123);
-			will(returnValue(batchesToAck));
-			// Create the packet
-			oneOf(packetFactory).createAck(batchesToAck);
+			// Get the messages to ack
+			oneOf(database).getMessagesToAck(txn, contactId, 123);
+			will(returnValue(messagesToAck));
+			// Create the ack packet
+			oneOf(packetFactory).createAck(messagesToAck);
 			will(returnValue(ack));
-			// Record the batches that were acked
-			oneOf(database).removeBatchesToAck(txn, contactId, batchesToAck);
+			// Record the messages that were acked
+			oneOf(database).removeMessagesToAck(txn, contactId, messagesToAck);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
@@ -667,7 +660,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testGenerateBatch() throws Exception {
-		final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		final byte[] raw1 = new byte[size];
 		final Collection<MessageId> sendable = Arrays.asList(messageId,
 				messageId1);
@@ -678,7 +670,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final RawBatch batch = context.mock(RawBatch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -692,40 +683,32 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(raw));
 			oneOf(database).getMessage(txn, messageId1);
 			will(returnValue(raw1));
-			// Create the packet
-			oneOf(packetFactory).createBatch(messages);
-			will(returnValue(batch));
-			// Record the outstanding batch
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addOutstandingBatch(txn, contactId, batchId,
-					sendable);
+			// Record the outstanding messages
+			oneOf(database).addOutstandingMessages(txn, contactId, sendable);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		assertEquals(batch, db.generateBatch(contactId, size * 2));
+		assertEquals(messages, db.generateBatch(contactId, size * 2));
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testGenerateBatchFromRequest() throws Exception {
-		final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		final MessageId messageId2 = new MessageId(TestUtils.getRandomId());
 		final byte[] raw1 = new byte[size];
 		final Collection<MessageId> requested = new ArrayList<MessageId>();
 		requested.add(messageId);
 		requested.add(messageId1);
 		requested.add(messageId2);
-		final Collection<byte[]> msgs = Arrays.asList(raw1);
+		final Collection<byte[]> messages = Arrays.asList(raw1);
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final RawBatch batch = context.mock(RawBatch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -739,19 +722,15 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(raw1)); // Message is sendable
 			oneOf(database).getMessageIfSendable(txn, contactId, messageId2);
 			will(returnValue(null)); // Message is not sendable
-			// Create the packet
-			oneOf(packetFactory).createBatch(msgs);
-			will(returnValue(batch));
-			// Record the outstanding batch
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addOutstandingBatch(txn, contactId, batchId,
+			// Record the outstanding messages
+			oneOf(database).addOutstandingMessages(txn, contactId,
 					Collections.singletonList(messageId1));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		assertEquals(batch, db.generateBatch(contactId, size * 3, requested));
+		assertEquals(messages, db.generateBatch(contactId, size * 3,
+				requested));
 
 		context.assertIsSatisfied();
 	}
@@ -903,7 +882,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testReceiveAck() throws Exception {
-		final BatchId batchId1 = new BatchId(TestUtils.getRandomId());
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -917,14 +895,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).commitTransaction(txn);
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			// Get the acked batches
-			oneOf(ack).getBatchIds();
-			will(returnValue(Collections.singletonList(batchId)));
-			oneOf(database).removeAckedBatch(txn, contactId, batchId);
-			// Find lost batches
-			oneOf(database).getLostBatches(txn, contactId);
-			will(returnValue(Collections.singletonList(batchId1)));
-			oneOf(database).removeLostBatch(txn, contactId, batchId1);
+			// Get the acked messages
+			oneOf(ack).getMessageIds();
+			will(returnValue(Collections.singletonList(messageId)));
+			oneOf(database).removeAckedMessages(txn, contactId,
+					Collections.singletonList(messageId));
+			// Find lost messages
+			oneOf(database).getLostMessages(txn, contactId);
+			will(returnValue(Collections.emptyList()));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
@@ -935,74 +913,64 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testReceiveBatchStoresPrivateMessage() throws Exception {
+	public void testReceivePrivateMessage() throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
 			allowing(database).commitTransaction(txn);
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(privateMessage)));
 			// The message is stored
 			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
 			will(returnValue(true));
 			oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN);
-			// The batch must be acked
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message must be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, privateMessage);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testReceiveBatchWithDuplicatePrivateMessage() throws Exception {
+	public void testReceiveDuplicatePrivateMessage() throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
 			allowing(database).commitTransaction(txn);
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(privateMessage)));
 			// The message is stored, but it's a duplicate
 			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
 			will(returnValue(false));
-			// The batch must still be acked
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message must still be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, privateMessage);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testReceiveBatchDoesNotStoreGroupMessageUnlessSubscribed()
+	public void testReceiveMessageDoesNotStoreGroupMessageUnlessSubscribed()
 			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
@@ -1010,7 +978,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1018,26 +985,22 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Only store messages belonging to visible, subscribed groups
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(message)));
 			oneOf(database).containsVisibleSubscription(txn, groupId,
 					contactId, timestamp);
 			will(returnValue(false));
-			// The message is not stored but the batch must still be acked
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message is not stored but it must still be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, message);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testReceiveBatchDoesNotCalculateSendabilityForDuplicates()
+	public void testReceiveMessageDoesNotCalculateSendabilityForDuplicates()
 			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
@@ -1045,7 +1008,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1053,8 +1015,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Only store messages belonging to visible, subscribed groups
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(message)));
 			oneOf(database).containsVisibleSubscription(txn, groupId,
 					contactId, timestamp);
 			will(returnValue(true));
@@ -1062,28 +1022,25 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).addGroupMessage(txn, message);
 			will(returnValue(false));
 			oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN);
-			// The batch needs to be acknowledged
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message must be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, message);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testReceiveBatchCalculatesSendability() throws Exception {
+	public void testReceiveMessageCalculatesSendability() throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1091,8 +1048,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Only store messages belonging to visible, subscribed groups
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(message)));
 			oneOf(database).containsVisibleSubscription(txn, groupId,
 					contactId, timestamp);
 			will(returnValue(true));
@@ -1109,28 +1064,26 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).getNumberOfSendableChildren(txn, messageId);
 			will(returnValue(0));
 			oneOf(database).setSendability(txn, messageId, 0);
-			// The batch needs to be acknowledged
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message must be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, message);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testReceiveBatchUpdatesAncestorSendability() throws Exception {
+	public void testReceiveMessageUpdatesAncestorSendability()
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final Batch batch = context.mock(Batch.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1138,8 +1091,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Only store messages belonging to visible, subscribed groups
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(message)));
 			oneOf(database).containsVisibleSubscription(txn, groupId,
 					contactId, timestamp);
 			will(returnValue(true));
@@ -1158,15 +1109,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setSendability(txn, messageId, 2);
 			oneOf(database).getGroupMessageParent(txn, messageId);
 			will(returnValue(null));
-			// The batch needs to be acknowledged
-			oneOf(batch).getId();
-			will(returnValue(batchId));
-			oneOf(database).addBatchToAck(txn, contactId, batchId);
+			// The message must be acked
+			oneOf(database).addMessageToAck(txn, contactId, messageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.receiveBatch(contactId, batch);
+		db.receiveMessage(contactId, message);
 
 		context.assertIsSatisfied();
 	}
@@ -1319,7 +1268,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setSendability(txn, messageId, 0);
 			oneOf(database).commitTransaction(txn);
 			// The message was added, so the listener should be called
-			oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class)));
+			oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
@@ -1350,7 +1299,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).setStatus(txn, contactId, messageId, Status.NEW);
 			// The message was added, so the listener should be called
-			oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class)));
+			oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index d31ac8a8fd..e9ba2d0b74 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -1,7 +1,6 @@
 package net.sf.briar.db;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static net.sf.briar.db.DatabaseConstants.RETRANSMIT_THRESHOLD;
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.File;
@@ -20,6 +19,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.TestDatabaseConfig;
+import net.sf.briar.TestMessage;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
@@ -29,7 +29,6 @@ import net.sf.briar.api.clock.SystemClock;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
@@ -53,10 +52,9 @@ public class H2DatabaseTest extends BriarTestCase {
 	private final Random random = new Random();
 	private final Group group;
 	private final AuthorId authorId;
-	private final BatchId batchId;
 	private final ContactId contactId;
 	private final GroupId groupId;
-	private final MessageId messageId, privateMessageId;
+	private final MessageId messageId, messageId1;
 	private final String subject;
 	private final long timestamp;
 	private final int size;
@@ -67,11 +65,10 @@ public class H2DatabaseTest extends BriarTestCase {
 	public H2DatabaseTest() throws Exception {
 		super();
 		authorId = new AuthorId(TestUtils.getRandomId());
-		batchId = new BatchId(TestUtils.getRandomId());
 		contactId = new ContactId(1);
 		groupId = new GroupId(TestUtils.getRandomId());
 		messageId = new MessageId(TestUtils.getRandomId());
-		privateMessageId = new MessageId(TestUtils.getRandomId());
+		messageId1 = new MessageId(TestUtils.getRandomId());
 		group = new Group(groupId, "Foo", null);
 		subject = "Foo";
 		timestamp = System.currentTimeMillis();
@@ -80,7 +77,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		random.nextBytes(raw);
 		message = new TestMessage(messageId, null, groupId, authorId, subject,
 				timestamp, raw);
-		privateMessage = new TestMessage(privateMessageId, null, null, null,
+		privateMessage = new TestMessage(messageId1, null, null, null,
 				subject, timestamp, raw);
 		transportId = new TransportId(TestUtils.getRandomId());
 	}
@@ -104,9 +101,9 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertFalse(db.containsMessage(txn, messageId));
 		db.addGroupMessage(txn, message);
 		assertTrue(db.containsMessage(txn, messageId));
-		assertFalse(db.containsMessage(txn, privateMessageId));
+		assertFalse(db.containsMessage(txn, messageId1));
 		db.addPrivateMessage(txn, privateMessage, contactId);
-		assertTrue(db.containsMessage(txn, privateMessageId));
+		assertTrue(db.containsMessage(txn, messageId1));
 		db.commitTransaction(txn);
 		db.close();
 
@@ -118,12 +115,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertTrue(db.containsMessage(txn, messageId));
 		byte[] raw1 = db.getMessage(txn, messageId);
 		assertArrayEquals(raw, raw1);
-		assertTrue(db.containsMessage(txn, privateMessageId));
-		raw1 = db.getMessage(txn, privateMessageId);
+		assertTrue(db.containsMessage(txn, messageId1));
+		raw1 = db.getMessage(txn, messageId1);
 		assertArrayEquals(raw, raw1);
 		// Delete the records
 		db.removeMessage(txn, messageId);
-		db.removeMessage(txn, privateMessageId);
+		db.removeMessage(txn, messageId1);
 		db.removeContact(txn, contactId);
 		db.removeSubscription(txn, groupId);
 		db.commitTransaction(txn);
@@ -137,7 +134,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				db.getRemoteProperties(txn, transportId));
 		assertFalse(db.containsSubscription(txn, groupId));
 		assertFalse(db.containsMessage(txn, messageId));
-		assertFalse(db.containsMessage(txn, privateMessageId));
+		assertFalse(db.containsMessage(txn, messageId1));
 		db.commitTransaction(txn);
 		db.close();
 	}
@@ -216,9 +213,9 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addPrivateMessage(txn, privateMessage, contactId);
 
 		// Removing the contact should remove the message
-		assertTrue(db.containsMessage(txn, privateMessageId));
+		assertTrue(db.containsMessage(txn, messageId1));
 		db.removeContact(txn, contactId);
-		assertFalse(db.containsMessage(txn, privateMessageId));
+		assertFalse(db.containsMessage(txn, messageId1));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -241,21 +238,21 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertFalse(it.hasNext());
 
 		// Changing the status to NEW should make the message sendable
-		db.setStatus(txn, contactId, privateMessageId, Status.NEW);
+		db.setStatus(txn, contactId, messageId1, Status.NEW);
 		assertTrue(db.hasSendableMessages(txn, contactId));
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertTrue(it.hasNext());
-		assertEquals(privateMessageId, it.next());
+		assertEquals(messageId1, it.next());
 		assertFalse(it.hasNext());
 
 		// Changing the status to SENT should make the message unsendable
-		db.setStatus(txn, contactId, privateMessageId, Status.SENT);
+		db.setStatus(txn, contactId, messageId1, Status.SENT);
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Changing the status to SEEN should also make the message unsendable
-		db.setStatus(txn, contactId, privateMessageId, Status.SEEN);
+		db.setStatus(txn, contactId, messageId1, Status.SEEN);
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
@@ -272,7 +269,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact and store a private message
 		assertEquals(contactId, db.addContact(txn));
 		db.addPrivateMessage(txn, privateMessage, contactId);
-		db.setStatus(txn, contactId, privateMessageId, Status.NEW);
+		db.setStatus(txn, contactId, messageId1, Status.NEW);
 
 		// The message is sendable, but too large to send
 		assertTrue(db.hasSendableMessages(txn, contactId));
@@ -284,7 +281,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertTrue(db.hasSendableMessages(txn, contactId));
 		it = db.getSendableMessages(txn, contactId, size).iterator();
 		assertTrue(it.hasNext());
-		assertEquals(privateMessageId, it.next());
+		assertEquals(messageId1, it.next());
 		assertFalse(it.hasNext());
 
 		db.commitTransaction(txn);
@@ -508,109 +505,58 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testBatchesToAck() throws Exception {
-		BatchId batchId1 = new BatchId(TestUtils.getRandomId());
+	public void testMessagesToAck() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and some batches to ack
+		// Add a contact and some messages to ack
 		assertEquals(contactId, db.addContact(txn));
-		db.addBatchToAck(txn, contactId, batchId);
-		db.addBatchToAck(txn, contactId, batchId1);
+		db.addMessageToAck(txn, contactId, messageId);
+		db.addMessageToAck(txn, contactId, messageId1);
 
-		// Both batch IDs should be returned
-		Collection<BatchId> acks = db.getBatchesToAck(txn, contactId, 1234);
-		assertEquals(2, acks.size());
-		assertTrue(acks.contains(batchId));
-		assertTrue(acks.contains(batchId1));
+		// Both message IDs should be returned
+		Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
+		assertEquals(ids, db.getMessagesToAck(txn, contactId, 1234));
 
-		// Remove the batch IDs
-		db.removeBatchesToAck(txn, contactId, acks);
+		// Remove both message IDs
+		db.removeMessagesToAck(txn, contactId, ids);
 
-		// Both batch IDs should have been removed
-		acks = db.getBatchesToAck(txn, contactId, 1234);
-		assertEquals(0, acks.size());
+		// Both message IDs should have been removed
+		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
+				contactId, 1234));
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
 	@Test
-	public void testDuplicateBatchesReceived() throws Exception {
+	public void testDuplicateMessageReceived() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and receive the same batch twice
+		// Add a contact and receive the same message twice
 		assertEquals(contactId, db.addContact(txn));
-		db.addBatchToAck(txn, contactId, batchId);
-		db.addBatchToAck(txn, contactId, batchId);
-
-		// The batch ID should only be returned once
-		Collection<BatchId> acks = db.getBatchesToAck(txn, contactId, 1234);
-		assertEquals(1, acks.size());
-		assertTrue(acks.contains(batchId));
-
-		// Remove the batch ID
-		db.removeBatchesToAck(txn, contactId, acks);
-
-		// The batch ID should have been removed
-		acks = db.getBatchesToAck(txn, contactId, 1234);
-		assertEquals(0, acks.size());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
+		db.addMessageToAck(txn, contactId, messageId);
+		db.addMessageToAck(txn, contactId, messageId);
 
-	@Test
-	public void testSameBatchCannotBeSentTwice() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
+		// The message ID should only be returned once
+		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
+		assertEquals(Collections.singletonList(messageId), ids);
 
-		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn));
-		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
-
-		// Add an outstanding batch
-		db.addOutstandingBatch(txn, contactId, batchId,
+		// Remove the message ID
+		db.removeMessagesToAck(txn, contactId,
 				Collections.singletonList(messageId));
 
-		// It should not be possible to add the same outstanding batch again
-		try {
-			db.addOutstandingBatch(txn, contactId, batchId,
-					Collections.singletonList(messageId));
-			fail();
-		} catch(DbException expected) {}
-
-		db.abortTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSameBatchCanBeSentToDifferentContacts() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add two contacts, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn));
-		ContactId contactId1 = db.addContact(txn);
-		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
-
-		// Add an outstanding batch for the first contact
-		db.addOutstandingBatch(txn, contactId, batchId,
-				Collections.singletonList(messageId));
-
-		// Add the same outstanding batch for the second contact
-		db.addOutstandingBatch(txn, contactId1, batchId,
-				Collections.singletonList(messageId));
+		// The message ID should have been removed
+		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
+				contactId, 1234));
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
 	@Test
-	public void testRemoveAckedBatch() throws Exception {
+	public void testRemoveAckedMessage() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -630,15 +576,16 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
 		db.setStatus(txn, contactId, messageId, Status.SENT);
-		db.addOutstandingBatch(txn, contactId, batchId,
+		db.addOutstandingMessages(txn, contactId,
 				Collections.singletonList(messageId));
 
 		// The message should no longer be sendable
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
-		// Pretend that the batch was acked
-		db.removeAckedBatch(txn, contactId, batchId);
+		// Pretend that the message was acked
+		db.removeAckedMessages(txn, contactId,
+				Collections.singletonList(messageId));
 
 		// The message still should not be sendable
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
@@ -649,7 +596,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testRemoveLostBatch() throws Exception {
+	public void testRemoveLostMessage() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -669,15 +616,16 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
 		db.setStatus(txn, contactId, messageId, Status.SENT);
-		db.addOutstandingBatch(txn, contactId, batchId,
+		db.addOutstandingMessages(txn, contactId,
 				Collections.singletonList(messageId));
 
 		// The message should no longer be sendable
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
-		// Pretend that the batch was lost
-		db.removeLostBatch(txn, contactId, batchId);
+		// Pretend that the message was lost
+		db.removeLostMessages(txn, contactId,
+				Collections.singletonList(messageId));
 
 		// The message should be sendable again
 		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
@@ -689,77 +637,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testRetransmission() throws Exception {
-		BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD + 5];
-		for(int i = 0; i < ids.length; i++) {
-			ids[i] = new BatchId(TestUtils.getRandomId());
-		}
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn));
-
-		// Add some outstanding batches, a few ms apart
-		for(int i = 0; i < ids.length; i++) {
-			db.addOutstandingBatch(txn, contactId, ids[i],
-					Collections.<MessageId>emptyList());
-			Thread.sleep(5);
-		}
-
-		// The contact acks the batches in reverse order. The first
-		// RETRANSMIT_THRESHOLD - 1 acks should not trigger any retransmissions
-		for(int i = 0; i < RETRANSMIT_THRESHOLD - 1; i++) {
-			db.removeAckedBatch(txn, contactId, ids[ids.length - i - 1]);
-			Collection<BatchId> lost = db.getLostBatches(txn, contactId);
-			assertEquals(Collections.emptyList(), lost);
-		}
-
-		// The next ack should trigger the retransmission of the remaining
-		// five outstanding batches
-		int index = ids.length - RETRANSMIT_THRESHOLD;
-		db.removeAckedBatch(txn, contactId, ids[index]);
-		Collection<BatchId> lost = db.getLostBatches(txn, contactId);
-		for(int i = 0; i < index; i++) {
-			assertTrue(lost.contains(ids[i]));
-		}
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testNoRetransmission() throws Exception {
-		BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD * 2];
-		for(int i = 0; i < ids.length; i++) {
-			ids[i] = new BatchId(TestUtils.getRandomId());
-		}
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn));
-
-		// Add some outstanding batches, a few ms apart
-		for(int i = 0; i < ids.length; i++) {
-			db.addOutstandingBatch(txn, contactId, ids[i],
-					Collections.<MessageId>emptyList());
-			Thread.sleep(5);
-		}
-
-		// The contact acks the batches in the order they were sent - nothing
-		// should be retransmitted
-		for(int i = 0; i < ids.length; i++) {
-			db.removeAckedBatch(txn, contactId, ids[i]);
-			Collection<BatchId> lost = db.getLostBatches(txn, contactId);
-			assertEquals(Collections.emptyList(), lost);
-		}
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testGetMessagesByAuthor() throws Exception {
 		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
@@ -1426,12 +1303,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// A message with a private parent should return null
 		MessageId childId = new MessageId(TestUtils.getRandomId());
-		Message child = new TestMessage(childId, privateMessageId, groupId,
+		Message child = new TestMessage(childId, messageId1, groupId,
 				null, subject, timestamp, raw);
 		db.addGroupMessage(txn, child);
 		db.addPrivateMessage(txn, privateMessage, contactId);
 		assertTrue(db.containsMessage(txn, childId));
-		assertTrue(db.containsMessage(txn, privateMessageId));
+		assertTrue(db.containsMessage(txn, messageId1));
 		assertNull(db.getGroupMessageParent(txn, childId));
 
 		db.commitTransaction(txn);
@@ -1477,7 +1354,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		int bodyLength = raw.length - 20;
 		Message message1 = new TestMessage(messageId, null, groupId, null,
 				subject, timestamp, raw, 5, bodyLength);
-		Message privateMessage1 = new TestMessage(privateMessageId, null, null,
+		Message privateMessage1 = new TestMessage(messageId1, null, null,
 				null, subject, timestamp, raw, 10, bodyLength);
 		db.addGroupMessage(txn, message1);
 		db.addPrivateMessage(txn, privateMessage1, contactId);
@@ -1492,12 +1369,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Retrieve the raw messages
 		assertArrayEquals(raw, db.getMessage(txn, messageId));
-		assertArrayEquals(raw, db.getMessage(txn, privateMessageId));
+		assertArrayEquals(raw, db.getMessage(txn, messageId1));
 
 		// Retrieve the message bodies
 		byte[] body = db.getMessageBody(txn, messageId);
 		assertArrayEquals(expectedBody, body);
-		byte[] body1 = db.getMessageBody(txn, privateMessageId);
+		byte[] body1 = db.getMessageBody(txn, messageId1);
 		assertArrayEquals(expectedBody1, body1);
 
 		db.commitTransaction(txn);
diff --git a/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java b/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java
deleted file mode 100644
index 6323638b01..0000000000
--- a/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.Collections;
-
-import net.sf.briar.BriarTestCase;
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.Types;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.ReaderFactory;
-import net.sf.briar.api.serial.StructReader;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-import net.sf.briar.serial.SerialModule;
-
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class BatchReaderTest extends BriarTestCase {
-
-	// FIXME: This is an integration test, not a unit test
-
-	private final ReaderFactory readerFactory;
-	private final WriterFactory writerFactory;
-	private final Mockery context;
-	private final UnverifiedMessage message;
-	private final StructReader<UnverifiedMessage> messageReader;
-
-	public BatchReaderTest() throws Exception {
-		super();
-		Injector i = Guice.createInjector(new SerialModule());
-		readerFactory = i.getInstance(ReaderFactory.class);
-		writerFactory = i.getInstance(WriterFactory.class);
-		context = new Mockery();
-		message = context.mock(UnverifiedMessage.class);
-		messageReader = new TestMessageReader();
-	}
-
-	@Test
-	public void testFormatExceptionIfBatchIsTooLarge() throws Exception {
-		UnverifiedBatchFactory batchFactory =
-			context.mock(UnverifiedBatchFactory.class);
-		BatchReader batchReader = new BatchReader(messageReader, batchFactory);
-
-		byte[] b = createBatch(MAX_PACKET_LENGTH + 1);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		Reader reader = readerFactory.createReader(in);
-		reader.addStructReader(Types.BATCH, batchReader);
-
-		try {
-			reader.readStruct(Types.BATCH, UnverifiedBatch.class);
-			fail();
-		} catch(FormatException expected) {}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testNoFormatExceptionIfBatchIsMaximumSize() throws Exception {
-		final UnverifiedBatchFactory batchFactory =
-			context.mock(UnverifiedBatchFactory.class);
-		BatchReader batchReader = new BatchReader(messageReader, batchFactory);
-		final UnverifiedBatch batch = context.mock(UnverifiedBatch.class);
-		context.checking(new Expectations() {{
-			oneOf(batchFactory).createUnverifiedBatch(
-					Collections.singletonList(message));
-			will(returnValue(batch));
-		}});
-
-		byte[] b = createBatch(MAX_PACKET_LENGTH);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		Reader reader = readerFactory.createReader(in);
-		reader.addStructReader(Types.BATCH, batchReader);
-
-		assertEquals(batch, reader.readStruct(Types.BATCH,
-				UnverifiedBatch.class));
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEmptyBatch() throws Exception {
-		final UnverifiedBatchFactory batchFactory =
-			context.mock(UnverifiedBatchFactory.class);
-		BatchReader batchReader = new BatchReader(messageReader, batchFactory);
-
-		byte[] b = createEmptyBatch();
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		Reader reader = readerFactory.createReader(in);
-		reader.addStructReader(Types.BATCH, batchReader);
-
-		try {
-			reader.readStruct(Types.BATCH, UnverifiedBatch.class);
-			fail();
-		} catch(FormatException expected) {}
-		context.assertIsSatisfied();
-	}
-
-	private byte[] createBatch(int size) throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream(size);
-		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(Types.BATCH);
-		w.writeListStart();
-		// We're using a fake message reader, so it's OK to use a fake message
-		w.writeStructId(Types.MESSAGE);
-		w.writeBytes(new byte[size - 10]);
-		w.writeListEnd();
-		byte[] b = out.toByteArray();
-		assertEquals(size, b.length);
-		return b;
-	}
-
-	private byte[] createEmptyBatch() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		Writer w = writerFactory.createWriter(out);
-		w.writeStructId(Types.BATCH);
-		w.writeListStart();
-		w.writeListEnd();
-		return out.toByteArray();
-	}
-
-	private class TestMessageReader implements StructReader<UnverifiedMessage> {
-
-		public UnverifiedMessage readStruct(Reader r) throws IOException {
-			r.readStructId(Types.MESSAGE);
-			r.readBytes();
-			return message;
-		}
-	}
-}
diff --git a/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java
index 5f3fee3fa4..94543fb5f7 100644
--- a/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java
+++ b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java
@@ -14,7 +14,6 @@ import java.io.ByteArrayOutputStream;
 import java.security.PrivateKey;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 
 import net.sf.briar.BriarTestCase;
@@ -23,7 +22,6 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
-import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.Message;
@@ -33,7 +31,6 @@ import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.PacketFactory;
 import net.sf.briar.api.protocol.ProtocolWriter;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
@@ -69,24 +66,24 @@ public class ConstantsTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testBatchesFitIntoLargeAck() throws Exception {
-		testBatchesFitIntoAck(MAX_PACKET_LENGTH);
+	public void testMessageIdsFitIntoLargeAck() throws Exception {
+		testMessageIdsFitIntoAck(MAX_PACKET_LENGTH);
 	}
 
 	@Test
-	public void testBatchesFitIntoSmallAck() throws Exception {
-		testBatchesFitIntoAck(1000);
+	public void testMessageIdsFitIntoSmallAck() throws Exception {
+		testMessageIdsFitIntoAck(1000);
 	}
 
-	private void testBatchesFitIntoAck(int length) throws Exception {
-		// Create an ack with as many batch IDs as possible
+	private void testMessageIdsFitIntoAck(int length) throws Exception {
+		// Create an ack with as many message IDs as possible
 		ByteArrayOutputStream out = new ByteArrayOutputStream(length);
 		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out,
 				true);
-		int maxBatches = writer.getMaxBatchesForAck(length);
-		Collection<BatchId> acked = new ArrayList<BatchId>();
-		for(int i = 0; i < maxBatches; i++) {
-			acked.add(new BatchId(TestUtils.getRandomId()));
+		int maxMessages = writer.getMaxMessagesForAck(length);
+		Collection<MessageId> acked = new ArrayList<MessageId>();
+		for(int i = 0; i < maxMessages; i++) {
+			acked.add(new MessageId(TestUtils.getRandomId()));
 		}
 		Ack a = packetFactory.createAck(acked);
 		writer.writeAck(a);
@@ -95,7 +92,7 @@ public class ConstantsTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testMessageFitsIntoBatch() throws Exception {
+	public void testMessageFitsIntoPacket() throws Exception {
 		// Create a maximum-length group
 		String groupName = createRandomString(MAX_GROUP_NAME_LENGTH);
 		byte[] groupPublic = new byte[MAX_PUBLIC_KEY_LENGTH];
@@ -111,32 +108,25 @@ public class ConstantsTest extends BriarTestCase {
 		byte[] body = new byte[MAX_BODY_LENGTH];
 		Message message = messageFactory.createMessage(null, group,
 				groupPrivate, author, authorPrivate, subject, body);
-		// Add the message to a batch
-		ByteArrayOutputStream out =
-			new ByteArrayOutputStream(MAX_PACKET_LENGTH);
-		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out,
-				true);
-		RawBatch b = packetFactory.createBatch(Collections.singletonList(
-				message.getSerialised()));
-		writer.writeBatch(b);
-		// Check the size of the serialised batch
-		assertTrue(out.size() > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH
+		// Check the size of the serialised message
+		int length = message.getSerialised().length;
+		assertTrue(length > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH
 				+ MAX_PUBLIC_KEY_LENGTH + MAX_AUTHOR_NAME_LENGTH
 				+ MAX_PUBLIC_KEY_LENGTH + MAX_BODY_LENGTH);
-		assertTrue(out.size() <= MAX_PACKET_LENGTH);
+		assertTrue(length <= MAX_PACKET_LENGTH);
 	}
 
 	@Test
-	public void testMessagesFitIntoLargeOffer() throws Exception {
-		testMessagesFitIntoOffer(MAX_PACKET_LENGTH);
+	public void testMessageIdsFitIntoLargeOffer() throws Exception {
+		testMessageIdsFitIntoOffer(MAX_PACKET_LENGTH);
 	}
 
 	@Test
-	public void testMessagesFitIntoSmallOffer() throws Exception {
-		testMessagesFitIntoOffer(1000);
+	public void testMessageIdsFitIntoSmallOffer() throws Exception {
+		testMessageIdsFitIntoOffer(1000);
 	}
 
-	private void testMessagesFitIntoOffer(int length) throws Exception {
+	private void testMessageIdsFitIntoOffer(int length) throws Exception {
 		// Create an offer with as many message IDs as possible
 		ByteArrayOutputStream out = new ByteArrayOutputStream(length);
 		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out,
@@ -170,7 +160,7 @@ public class ConstantsTest extends BriarTestCase {
 		}
 		// Add the transports to an update
 		ByteArrayOutputStream out =
-			new ByteArrayOutputStream(MAX_PACKET_LENGTH);
+				new ByteArrayOutputStream(MAX_PACKET_LENGTH);
 		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out,
 				true);
 		TransportUpdate t = packetFactory.createTransportUpdate(transports,
diff --git a/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java
deleted file mode 100644
index 6328c65358..0000000000
--- a/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.util.BitSet;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-
-import net.sf.briar.BriarTestCase;
-import net.sf.briar.TestUtils;
-import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.Group;
-import net.sf.briar.api.protocol.GroupFactory;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageFactory;
-import net.sf.briar.api.protocol.Offer;
-import net.sf.briar.api.protocol.PacketFactory;
-import net.sf.briar.api.protocol.ProtocolReader;
-import net.sf.briar.api.protocol.ProtocolReaderFactory;
-import net.sf.briar.api.protocol.ProtocolWriter;
-import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
-import net.sf.briar.api.protocol.Request;
-import net.sf.briar.api.protocol.SubscriptionUpdate;
-import net.sf.briar.api.protocol.Transport;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.clock.ClockModule;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.serial.SerialModule;
-
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class ProtocolIntegrationTest extends BriarTestCase {
-
-	private final ProtocolReaderFactory readerFactory;
-	private final ProtocolWriterFactory writerFactory;
-	private final PacketFactory packetFactory;
-	private final BatchId batchId;
-	private final Group group;
-	private final Message message;
-	private final String subject = "Hello";
-	private final String messageBody = "Hello world";
-	private final BitSet bitSet;
-	private final Map<Group, Long> subscriptions;
-	private final Collection<Transport> transports;
-	private final long timestamp = System.currentTimeMillis();
-
-	public ProtocolIntegrationTest() throws Exception {
-		super();
-		Injector i = Guice.createInjector(new ClockModule(), new CryptoModule(),
-				new ProtocolModule(), new SerialModule());
-		readerFactory = i.getInstance(ProtocolReaderFactory.class);
-		writerFactory = i.getInstance(ProtocolWriterFactory.class);
-		packetFactory = i.getInstance(PacketFactory.class);
-		batchId = new BatchId(TestUtils.getRandomId());
-		GroupFactory groupFactory = i.getInstance(GroupFactory.class);
-		group = groupFactory.createGroup("Unrestricted group", null);
-		MessageFactory messageFactory = i.getInstance(MessageFactory.class);
-		message = messageFactory.createMessage(null, group, subject,
-				messageBody.getBytes("UTF-8"));
-		bitSet = new BitSet();
-		bitSet.set(3);
-		bitSet.set(7);
-		subscriptions = Collections.singletonMap(group, 123L);
-		TransportId transportId = new TransportId(TestUtils.getRandomId());
-		Transport transport = new Transport(transportId,
-				Collections.singletonMap("bar", "baz"));
-		transports = Collections.singletonList(transport);
-	}
-
-	@Test
-	public void testWriteAndRead() throws Exception {
-		// Write
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		ProtocolWriter writer = writerFactory.createProtocolWriter(out, true);
-
-		Ack a = packetFactory.createAck(Collections.singletonList(batchId));
-		writer.writeAck(a);
-
-		RawBatch b = packetFactory.createBatch(Collections.singletonList(
-				message.getSerialised()));
-		writer.writeBatch(b);
-
-		Offer o = packetFactory.createOffer(Collections.singletonList(
-				message.getId()));
-		writer.writeOffer(o);
-
-		Request r = packetFactory.createRequest(bitSet, 10);
-		writer.writeRequest(r);
-
-		SubscriptionUpdate s = packetFactory.createSubscriptionUpdate(
-				Collections.<GroupId, GroupId>emptyMap(), subscriptions, 0L,
-				timestamp);
-		writer.writeSubscriptionUpdate(s);
-
-		TransportUpdate t = packetFactory.createTransportUpdate(transports,
-				timestamp);
-		writer.writeTransportUpdate(t);
-
-		// Read
-		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
-		ProtocolReader reader = readerFactory.createProtocolReader(in);
-
-		a = reader.readAck();
-		assertEquals(Collections.singletonList(batchId), a.getBatchIds());
-
-		Batch b1 = reader.readBatch().verify();
-		assertEquals(Collections.singletonList(message), b1.getMessages());
-
-		o = reader.readOffer();
-		assertEquals(Collections.singletonList(message.getId()),
-				o.getMessageIds());
-
-		r = reader.readRequest();
-		assertEquals(bitSet, r.getBitmap());
-		assertEquals(10, r.getLength());
-
-		s = reader.readSubscriptionUpdate();
-		assertEquals(subscriptions, s.getSubscriptions());
-		assertEquals(timestamp, s.getTimestamp());
-
-		t = reader.readTransportUpdate();
-		assertEquals(transports, t.getTransports());
-		assertEquals(timestamp, t.getTimestamp());
-	}
-}
diff --git a/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java b/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java
deleted file mode 100644
index 08fdb3adbb..0000000000
--- a/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java
+++ /dev/null
@@ -1,242 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.Signature;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Random;
-
-import net.sf.briar.BriarTestCase;
-import net.sf.briar.TestUtils;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.crypto.MessageDigest;
-import net.sf.briar.api.protocol.Author;
-import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.Group;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.UnverifiedBatch;
-import net.sf.briar.crypto.CryptoModule;
-
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class UnverifiedBatchImplTest extends BriarTestCase {
-
-	// FIXME: This is an integration test, not a unit test
-
-	private final CryptoComponent crypto;
-	private final byte[] raw, raw1;
-	private final String subject;
-	private final long timestamp;
-
-	public UnverifiedBatchImplTest() {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		Random r = new Random();
-		raw = new byte[123];
-		r.nextBytes(raw);
-		raw1 = new byte[1234];
-		r.nextBytes(raw1);
-		subject = "Unit tests are exciting";
-		timestamp = System.currentTimeMillis();
-	}
-
-	@Test
-	public void testIds() throws Exception {
-		// Calculate the expected batch and message IDs
-		MessageDigest messageDigest = crypto.getMessageDigest();
-		messageDigest.update(raw);
-		messageDigest.update(raw1);
-		BatchId batchId = new BatchId(messageDigest.digest());
-		messageDigest.update(raw);
-		MessageId messageId = new MessageId(messageDigest.digest());
-		messageDigest.update(raw1);
-		MessageId messageId1 = new MessageId(messageDigest.digest());
-		// Verify the batch
-		Mockery context = new Mockery();
-		final UnverifiedMessage message =
-				context.mock(UnverifiedMessage.class, "message");
-		final UnverifiedMessage message1 =
-				context.mock(UnverifiedMessage.class, "message1");
-		context.checking(new Expectations() {{
-			// First message
-			oneOf(message).getRaw();
-			will(returnValue(raw));
-			oneOf(message).getAuthor();
-			will(returnValue(null));
-			oneOf(message).getGroup();
-			will(returnValue(null));
-			oneOf(message).getParent();
-			will(returnValue(null));
-			oneOf(message).getSubject();
-			will(returnValue(subject));
-			oneOf(message).getTimestamp();
-			will(returnValue(timestamp));
-			oneOf(message).getBodyStart();
-			will(returnValue(10));
-			oneOf(message).getBodyLength();
-			will(returnValue(100));
-			// Second message
-			oneOf(message1).getRaw();
-			will(returnValue(raw1));
-			oneOf(message1).getAuthor();
-			will(returnValue(null));
-			oneOf(message1).getGroup();
-			will(returnValue(null));
-			oneOf(message1).getParent();
-			will(returnValue(null));
-			oneOf(message1).getSubject();
-			will(returnValue(subject));
-			oneOf(message1).getTimestamp();
-			will(returnValue(timestamp));
-			oneOf(message1).getBodyStart();
-			will(returnValue(10));
-			oneOf(message1).getBodyLength();
-			will(returnValue(1000));
-		}});
-		Collection<UnverifiedMessage> messages = Arrays.asList(message,
-				message1);
-		UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages);
-		Batch verifiedBatch = batch.verify();
-		// Check that the batch and message IDs match
-		assertEquals(batchId, verifiedBatch.getId());
-		Collection<Message> verifiedMessages = verifiedBatch.getMessages();
-		assertEquals(2, verifiedMessages.size());
-		Iterator<Message> it = verifiedMessages.iterator();
-		Message verifiedMessage = it.next();
-		assertEquals(messageId, verifiedMessage.getId());
-		Message verifiedMessage1 = it.next();
-		assertEquals(messageId1, verifiedMessage1.getId());
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testSignatures() throws Exception {
-		final int signedByAuthor = 100, signedByGroup = 110;
-		final KeyPair authorKeyPair = crypto.generateSignatureKeyPair();
-		final KeyPair groupKeyPair = crypto.generateSignatureKeyPair();
-		GroupId groupId = new GroupId(TestUtils.getRandomId());
-		final Group group = new Group(groupId, "Group name",
-				groupKeyPair.getPublic().getEncoded());
-		Signature signature = crypto.getSignature();
-		// Calculate the expected author and group signatures
-		signature.initSign(authorKeyPair.getPrivate());
-		signature.update(raw, 0, signedByAuthor);
-		final byte[] authorSignature = signature.sign();
-		signature.initSign(groupKeyPair.getPrivate());
-		signature.update(raw, 0, signedByGroup);
-		final byte[] groupSignature = signature.sign();
-		// Verify the batch
-		Mockery context = new Mockery();
-		final UnverifiedMessage message =
-				context.mock(UnverifiedMessage.class, "message");
-		final Author author = context.mock(Author.class);
-		final UnverifiedMessage message1 =
-				context.mock(UnverifiedMessage.class, "message1");
-		context.checking(new Expectations() {{
-			// First message
-			oneOf(message).getRaw();
-			will(returnValue(raw));
-			oneOf(message).getAuthor();
-			will(returnValue(author));
-			oneOf(author).getPublicKey();
-			will(returnValue(authorKeyPair.getPublic().getEncoded()));
-			oneOf(message).getLengthSignedByAuthor();
-			will(returnValue(signedByAuthor));
-			oneOf(message).getAuthorSignature();
-			will(returnValue(authorSignature));
-			oneOf(message).getGroup();
-			will(returnValue(group));
-			oneOf(message).getLengthSignedByGroup();
-			will(returnValue(signedByGroup));
-			oneOf(message).getGroupSignature();
-			will(returnValue(groupSignature));
-			oneOf(author).getId();
-			will(returnValue(new AuthorId(TestUtils.getRandomId())));
-			oneOf(message).getParent();
-			will(returnValue(null));
-			oneOf(message).getSubject();
-			will(returnValue(subject));
-			oneOf(message).getTimestamp();
-			will(returnValue(timestamp));
-			oneOf(message).getBodyStart();
-			will(returnValue(10));
-			oneOf(message).getBodyLength();
-			will(returnValue(100));
-			// Second message
-			oneOf(message1).getRaw();
-			will(returnValue(raw1));
-			oneOf(message1).getAuthor();
-			will(returnValue(null));
-			oneOf(message1).getGroup();
-			will(returnValue(null));
-			oneOf(message1).getParent();
-			will(returnValue(null));
-			oneOf(message1).getSubject();
-			will(returnValue(subject));
-			oneOf(message1).getTimestamp();
-			will(returnValue(timestamp));
-			oneOf(message1).getBodyStart();
-			will(returnValue(10));
-			oneOf(message1).getBodyLength();
-			will(returnValue(1000));
-		}});
-		Collection<UnverifiedMessage> messages = Arrays.asList(message,
-				message1);
-		UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages);
-		batch.verify();
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testExceptionThrownIfMessageIsModified() throws Exception {
-		final int signedByAuthor = 100;
-		final KeyPair authorKeyPair = crypto.generateSignatureKeyPair();
-		Signature signature = crypto.getSignature();
-		// Calculate the expected author signature
-		signature.initSign(authorKeyPair.getPrivate());
-		signature.update(raw, 0, signedByAuthor);
-		final byte[] authorSignature = signature.sign();
-		// Modify the message
-		raw[signedByAuthor / 2] ^= 0xff;
-		// Verify the batch
-		Mockery context = new Mockery();
-		final UnverifiedMessage message =
-				context.mock(UnverifiedMessage.class, "message");
-		final Author author = context.mock(Author.class);
-		final UnverifiedMessage message1 =
-				context.mock(UnverifiedMessage.class, "message1");
-		context.checking(new Expectations() {{
-			// First message - verification will fail at the author's signature
-			oneOf(message).getRaw();
-			will(returnValue(raw));
-			oneOf(message).getAuthor();
-			will(returnValue(author));
-			oneOf(author).getPublicKey();
-			will(returnValue(authorKeyPair.getPublic().getEncoded()));
-			oneOf(message).getLengthSignedByAuthor();
-			will(returnValue(signedByAuthor));
-			oneOf(message).getAuthorSignature();
-			will(returnValue(authorSignature));
-		}});
-		Collection<UnverifiedMessage> messages = Arrays.asList(message,
-				message1);
-		UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages);
-		try {
-			batch.verify();
-			fail();
-		} catch(GeneralSecurityException expected) {}
-		context.assertIsSatisfied();
-	}
-}
diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
index bc0491b2e0..fef1015b04 100644
--- a/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
+++ b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
@@ -17,9 +17,8 @@ import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.transport.ConnectionContext;
@@ -51,6 +50,7 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 	private final ConnectionWriterFactory connFactory;
 	private final ProtocolWriterFactory protoFactory;
 	private final ContactId contactId;
+	private final MessageId messageId;
 	private final TransportId transportId;
 	private final byte[] secret;
 
@@ -75,6 +75,7 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 		connFactory = i.getInstance(ConnectionWriterFactory.class);
 		protoFactory = i.getInstance(ProtocolWriterFactory.class);
 		contactId = new ContactId(234);
+		messageId = new MessageId(TestUtils.getRandomId());
 		transportId = new TransportId(TestUtils.getRandomId());
 		secret = new byte[32];
 	}
@@ -115,7 +116,7 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 			// No acks to send
 			oneOf(db).generateAck(with(contactId), with(any(int.class)));
 			will(returnValue(null));
-			// No batches to send
+			// No messages to send
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)));
 			will(returnValue(null));
 		}});
@@ -138,9 +139,7 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
 				connRegistry, connFactory, protoFactory, ctx, transport);
 		final Ack ack = context.mock(Ack.class);
-		final BatchId batchId = new BatchId(TestUtils.getRandomId());
-		final RawBatch batch = context.mock(RawBatch.class);
-		final byte[] message = new byte[1234];
+		final byte[] raw = new byte[1234];
 		context.checking(new Expectations() {{
 			// No transports to send
 			oneOf(db).generateTransportUpdate(contactId);
@@ -151,24 +150,22 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 			// One ack to send
 			oneOf(db).generateAck(with(contactId), with(any(int.class)));
 			will(returnValue(ack));
-			oneOf(ack).getBatchIds();
-			will(returnValue(Collections.singletonList(batchId)));
+			oneOf(ack).getMessageIds();
+			will(returnValue(Collections.singletonList(messageId)));
 			// No more acks
 			oneOf(db).generateAck(with(contactId), with(any(int.class)));
 			will(returnValue(null));
-			// One batch to send
+			// One message to send
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)));
-			will(returnValue(batch));
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singletonList(message)));
-			// No more batches
+			will(returnValue(Collections.singletonList(raw)));
+			// No more messages
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)));
 			will(returnValue(null));
 		}});
 		connection.write();
 		// Something should have been written
 		int overhead = TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH;
-		assertTrue(out.size() > overhead + UniqueId.LENGTH + message.length);
+		assertTrue(out.size() > overhead + UniqueId.LENGTH + raw.length);
 		// The transport should have been disposed with exception == false
 		assertTrue(transport.getDisposed());
 		assertFalse(transport.getException());
diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java
index 302b37e3d0..cffd54b1e9 100644
--- a/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java
+++ b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java
@@ -17,9 +17,10 @@ import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.MessagesAddedEvent;
+import net.sf.briar.api.db.event.MessageAddedEvent;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageFactory;
+import net.sf.briar.api.protocol.MessageVerifier;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.Transport;
@@ -181,6 +182,8 @@ public class SimplexProtocolIntegrationTest extends BriarTestCase {
 		ConnectionContext ctx = rec.acceptConnection(transportId, tag);
 		assertNotNull(ctx);
 		// Create an incoming simplex connection
+		MessageVerifier messageVerifier =
+				bob.getInstance(MessageVerifier.class);
 		ConnectionRegistry connRegistry =
 				bob.getInstance(ConnectionRegistry.class);
 		ConnectionReaderFactory connFactory =
@@ -190,8 +193,9 @@ public class SimplexProtocolIntegrationTest extends BriarTestCase {
 		TestSimplexTransportReader transport =
 				new TestSimplexTransportReader(in);
 		IncomingSimplexConnection simplex = new IncomingSimplexConnection(
-				new ImmediateExecutor(), new ImmediateExecutor(), db,
-				connRegistry, connFactory, protoFactory, ctx, transport);
+				new ImmediateExecutor(), new ImmediateExecutor(),
+				messageVerifier, db, connRegistry, connFactory, protoFactory,
+				ctx, transport);
 		// No messages should have been added yet
 		assertFalse(listener.messagesAdded);
 		// Read whatever needs to be read
@@ -216,8 +220,7 @@ public class SimplexProtocolIntegrationTest extends BriarTestCase {
 		private boolean messagesAdded = false;
 
 		public void eventOccurred(DatabaseEvent e) {
-			if(e instanceof MessagesAddedEvent)
-				messagesAdded = true;
+			if(e instanceof MessageAddedEvent) messagesAdded = true;
 		}
 	}
 }
diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java
index 663700326d..c0c7437816 100644
--- a/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java
+++ b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java
@@ -8,7 +8,6 @@ import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
-
 public class ConnectionWriterImplTest extends BriarTestCase {
 
 	private static final int FRAME_LENGTH = 1024;
diff --git a/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java
index daa99b5b02..e2ec072e5e 100644
--- a/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java
+++ b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java
@@ -18,19 +18,12 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.transport.ConnectionReaderImpl;
-import net.sf.briar.transport.ConnectionWriterFactoryImpl;
-import net.sf.briar.transport.ConnectionWriterImpl;
-import net.sf.briar.transport.IncomingEncryptionLayer;
-import net.sf.briar.transport.OutgoingEncryptionLayer;
 
 import org.junit.Test;
 
-
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -93,7 +86,7 @@ public class TransportIntegrationTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		FrameWriter encryptionOut = new OutgoingEncryptionLayer(out,
 				Long.MAX_VALUE, frameCipher, frameCopy, FRAME_LENGTH);
-		ConnectionWriter writer = new ConnectionWriterImpl(encryptionOut,
+		ConnectionWriterImpl writer = new ConnectionWriterImpl(encryptionOut,
 				FRAME_LENGTH);
 		OutputStream out1 = writer.getOutputStream();
 		out1.write(frame);
@@ -106,7 +99,7 @@ public class TransportIntegrationTest extends BriarTestCase {
 		ByteArrayInputStream in = new ByteArrayInputStream(output);
 		FrameReader encryptionIn = new IncomingEncryptionLayer(in, frameCipher,
 				frameKey, FRAME_LENGTH);
-		ConnectionReader reader = new ConnectionReaderImpl(encryptionIn,
+		ConnectionReaderImpl reader = new ConnectionReaderImpl(encryptionIn,
 				FRAME_LENGTH);
 		InputStream in1 = reader.getInputStream();
 		byte[] recovered = new byte[frame.length];
-- 
GitLab