diff --git a/api/net/sf/briar/api/protocol/Batch.java b/api/net/sf/briar/api/protocol/Batch.java
index 05dc321a295bc3bc004124a4fd9188951a99cbb7..e8534a72f4a08bb7a8e7bda460dabafabdc9a772 100644
--- a/api/net/sf/briar/api/protocol/Batch.java
+++ b/api/net/sf/briar/api/protocol/Batch.java
@@ -8,12 +8,6 @@ public interface Batch {
 	/** Returns the batch's unique identifier. */
 	BatchId getId();
 
-	/** Returns the size of the serialised batch in bytes. */
-	long getSize();
-
 	/** Returns the messages contained in the batch. */
 	Iterable<Message> getMessages();
-
-	/** Returns the sender's signature over the contents of the batch. */
-	byte[] getSignature();
 }
\ No newline at end of file
diff --git a/api/net/sf/briar/api/protocol/BatchBuilder.java b/api/net/sf/briar/api/protocol/BatchBuilder.java
deleted file mode 100644
index ca7f867940d83563636cafd4e2b7c972102d2a4a..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/protocol/BatchBuilder.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
-public interface BatchBuilder {
-
-	/** Adds a message to the batch. */
-	void addMessage(Message m);
-
-	/** Sets the sender's signature over the contents of the batch. */
-	void setSignature(byte[] sig);
-
-	/** Builds and returns the batch. */
-	Batch build() throws IOException, GeneralSecurityException;
-}
diff --git a/api/net/sf/briar/api/protocol/BundleReader.java b/api/net/sf/briar/api/protocol/BundleReader.java
index b5e6390437751d2513287b148abb8e3d5f0f7721..ce32210902fb032191ccbb9c6a5752dd8991f039 100644
--- a/api/net/sf/briar/api/protocol/BundleReader.java
+++ b/api/net/sf/briar/api/protocol/BundleReader.java
@@ -9,9 +9,6 @@ import java.security.GeneralSecurityException;
  */
 public interface BundleReader {
 
-	/** Returns the size of the serialised bundle in bytes. */
-	long getSize() throws IOException;
-
 	/** Returns the bundle's header. */
 	Header getHeader() throws IOException, GeneralSecurityException;
 
@@ -21,5 +18,5 @@ public interface BundleReader {
 	Batch getNextBatch() throws IOException, GeneralSecurityException;
 
 	/** Finishes reading the bundle. */
-	void close() throws IOException;
+	void finish() throws IOException;
 }
diff --git a/api/net/sf/briar/api/protocol/BundleWriter.java b/api/net/sf/briar/api/protocol/BundleWriter.java
index 730a133ae6ce147ae9fd57aae7f5a6696b951408..5bb570a3ca84ac34c549990ab0cd8a1063947c0f 100644
--- a/api/net/sf/briar/api/protocol/BundleWriter.java
+++ b/api/net/sf/briar/api/protocol/BundleWriter.java
@@ -1,6 +1,8 @@
 package net.sf.briar.api.protocol;
 
 import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Map;
 
 /**
  * An interface for writing a bundle of acknowledgements, subscriptions,
@@ -8,15 +10,18 @@ import java.io.IOException;
  */
 public interface BundleWriter {
 
-	/** Returns the bundle's capacity in bytes. */
-	long getCapacity() throws IOException;
+	/** Returns the bundle's remaining capacity in bytes. */
+	long getRemainingCapacity() throws IOException;
 
-	/** Adds a header to the bundle. */
-	void addHeader(Header h) throws IOException;
+	/** Adds a header to the bundle and returns its identifier. */
+	BundleId addHeader(Iterable<BatchId> acks, Iterable<GroupId> subs,
+			Map<String, String> transports) throws IOException,
+			GeneralSecurityException;
 
-	/** Adds a batch of messages to the bundle. */
-	void addBatch(Batch b) throws IOException;
+	/** Adds a batch to the bundle and returns its identifier. */
+	BatchId addBatch(Iterable<Message> messages) throws IOException,
+	GeneralSecurityException;
 
 	/** Finishes writing the bundle. */
-	void close() throws IOException;
+	void finish() throws IOException;
 }
diff --git a/api/net/sf/briar/api/protocol/Header.java b/api/net/sf/briar/api/protocol/Header.java
index ce3c581a0740198eed30475c0bf47ece526ee6a0..09d762989b41abe25dc71df08127e2caa1fd3aff 100644
--- a/api/net/sf/briar/api/protocol/Header.java
+++ b/api/net/sf/briar/api/protocol/Header.java
@@ -11,9 +11,6 @@ public interface Header {
 	// FIXME: Remove BundleId when refactoring is complete
 	BundleId getId();
 
-	/** Returns the size of the serialised header in bytes. */
-	long getSize();
-
 	/** Returns the acknowledgements contained in the header. */
 	Set<BatchId> getAcks();
 
@@ -22,7 +19,4 @@ public interface Header {
 
 	/** Returns the transport details contained in the header. */
 	Map<String, String> getTransports();
-
-	/** Returns the sender's signature over the contents of the header. */
-	byte[] getSignature();
 }
diff --git a/api/net/sf/briar/api/protocol/HeaderBuilder.java b/api/net/sf/briar/api/protocol/HeaderBuilder.java
deleted file mode 100644
index c22ed3a361e2a355e7f2f75065a50d921c95f3f6..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/protocol/HeaderBuilder.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.sf.briar.api.protocol;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.Map;
-
-public interface HeaderBuilder {
-
-	/** Adds acknowledgements to the header. */
-	void addAcks(Iterable<BatchId> acks);
-
-	/** Adds subscriptions to the header. */
-	void addSubscriptions(Iterable<GroupId> subs);
-
-	/** Adds transport details to the header. */
-	void addTransports(Map<String, String> transports);
-
-	/** Sets the sender's signature over the contents of the header. */
-	void setSignature(byte[] sig);
-
-	/** Builds and returns the header. */
-	Header build() throws IOException, GeneralSecurityException;
-}
diff --git a/api/net/sf/briar/api/protocol/MessageFactory.java b/api/net/sf/briar/api/protocol/MessageFactory.java
index 33f0fdc4393d4b969e771f22a238561696eb76b5..ca3af74a996df2cb2009251cd72ec5df3f6d48de 100644
--- a/api/net/sf/briar/api/protocol/MessageFactory.java
+++ b/api/net/sf/briar/api/protocol/MessageFactory.java
@@ -3,5 +3,5 @@ package net.sf.briar.api.protocol;
 public interface MessageFactory {
 
 	Message createMessage(MessageId id, MessageId parent, GroupId group,
-			AuthorId author, long timestamp, byte[] body);
+			AuthorId author, long timestamp, byte[] raw);
 }
diff --git a/api/net/sf/briar/api/serial/Raw.java b/api/net/sf/briar/api/serial/Raw.java
index 3196dcc46417bb23417de5a3dbce84ee0f638b54..5a40b2f8c5549e00436028a0996619652159fc8d 100644
--- a/api/net/sf/briar/api/serial/Raw.java
+++ b/api/net/sf/briar/api/serial/Raw.java
@@ -1,5 +1,9 @@
 package net.sf.briar.api.serial;
 
+/**
+ * Generic interface for any object that knows how to serialise itself as a
+ * raw byte array.
+ */
 public interface Raw {
 
 	byte[] getBytes();
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 4fc737634d40e8abcced69301854d4d57e8663e0..45c006ecc4ffb22c884733fc70705c9dc2a00b70 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -9,13 +9,9 @@ import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.Status;
 import net.sf.briar.api.protocol.AuthorId;
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
-import com.google.inject.Provider;
-
 /**
  * Abstract superclass containing code shared by ReadWriteLockDatabaseComponent
  * and SynchronizedDatabaseComponent.
@@ -28,8 +24,6 @@ DatabaseCleaner.Callback {
 
 	protected final Database<Txn> db;
 	protected final DatabaseCleaner cleaner;
-	protected final Provider<HeaderBuilder> headerBuilderProvider;
-	protected final Provider<BatchBuilder> batchBuilderProvider;
 
 	private final Object spaceLock = new Object();
 	private final Object writeLock = new Object();
@@ -37,13 +31,9 @@ DatabaseCleaner.Callback {
 	private long timeOfLastCheck = 0L; // Locking: spaceLock
 	private volatile boolean writesAllowed = true;
 
-	DatabaseComponentImpl(Database<Txn> db, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
+	DatabaseComponentImpl(Database<Txn> db, DatabaseCleaner cleaner) {
 		this.db = db;
 		this.cleaner = cleaner;
-		this.headerBuilderProvider = headerBuilderProvider;
-		this.batchBuilderProvider = batchBuilderProvider;
 	}
 
 	public void open(boolean resume) throws DbException {
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index 2c6c5552633b8b85e15a63a56970679780ce19eb..cabebcc17543d2a815f53106729d251fb8dd3caf 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -52,7 +52,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		+ " authorId XXXX NOT NULL,"
 		+ " timestamp BIGINT NOT NULL,"
 		+ " size INT NOT NULL,"
-		+ " body BLOB NOT NULL,"
+		+ " raw BLOB NOT NULL,"
 		+ " sendability INT NOT NULL,"
 		+ " PRIMARY KEY (messageId),"
 		+ " FOREIGN KEY (groupId) REFERENCES localSubscriptions (groupId)"
@@ -458,7 +458,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			String sql = "INSERT INTO messages"
 				+ " (messageId, parentId, groupId, authorId, timestamp, size,"
-				+ " body, sendability)"
+				+ " raw, sendability)"
 				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
@@ -834,7 +834,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql =
-				"SELECT parentId, groupId, authorId, timestamp, size, body"
+				"SELECT parentId, groupId, authorId, timestamp, size, raw"
 				+ " FROM messages WHERE messageId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
@@ -847,14 +847,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			long timestamp = rs.getLong(4);
 			int size = rs.getInt(5);
 			Blob b = rs.getBlob(6);
-			byte[] body = b.getBytes(1, size);
-			assert body.length == size;
+			byte[] raw = b.getBytes(1, size);
+			assert raw.length == size;
 			boolean more = rs.next();
 			assert !more;
 			rs.close();
 			ps.close();
 			return messageFactory.createMessage(m, parent, group, author,
-					timestamp, body);
+					timestamp, raw);
 		} catch(SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
index f9971cfc0664d98785e8edefa7b5377ccb2dfe34..5342218efc426980158a65bc6a0627835b765ba0 100644
--- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
+++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
@@ -2,9 +2,10 @@ package net.sf.briar.db;
 
 import java.io.IOException;
 import java.security.GeneralSecurityException;
-import java.security.SignatureException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -17,19 +18,16 @@ import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.NoSuchContactException;
 import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.BundleId;
 import net.sf.briar.api.protocol.BundleReader;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 /**
  * An implementation of DatabaseComponent using reentrant read-write locks.
@@ -59,10 +57,8 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		new ReentrantReadWriteLock(true);
 
 	@Inject
-	ReadWriteLockDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		super(db, cleaner, headerBuilderProvider, batchBuilderProvider);
+	ReadWriteLockDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner) {
+		super(db, cleaner);
 	}
 
 	public void close() throws DbException {
@@ -195,18 +191,18 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 	public void generateBundle(ContactId c, BundleWriter b) throws DbException,
 	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c);
-		HeaderBuilder h;
+		Set<BatchId> acks;
+		Set<GroupId> subs;
+		Map<String, String> transports;
 		// Add acks
 		contactLock.readLock().lock();
 		try {
 			if(!containsContact(c)) throw new NoSuchContactException();
-			h = headerBuilderProvider.get();
 			messageStatusLock.writeLock().lock();
 			try {
 				Txn txn = db.startTransaction();
 				try {
-					Set<BatchId> acks = db.removeBatchesToAck(txn, c);
-					h.addAcks(acks);
+					acks = db.removeBatchesToAck(txn, c);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + acks.size() + " acks");
 					db.commitTransaction(txn);
@@ -228,8 +224,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			try {
 				Txn txn = db.startTransaction();
 				try {
-					Set<GroupId> subs = db.getSubscriptions(txn);
-					h.addSubscriptions(subs);
+					subs = db.getSubscriptions(txn);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + subs.size() + " subscriptions");
 					db.commitTransaction(txn);
@@ -251,8 +246,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			try {
 				Txn txn = db.startTransaction();
 				try {
-					Map<String, String> transports = db.getTransports(txn);
-					h.addTransports(transports);
+					transports = db.getTransports(txn);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + transports.size() + " transports");
 					db.commitTransaction(txn);
@@ -266,28 +260,16 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		} finally {
 			contactLock.readLock().unlock();
 		}
-		// Sign the header and add it to the bundle
-		Header header = h.build();
-		long capacity = b.getCapacity();
-		capacity -= header.getSize();
-		b.addHeader(header);
+		// Add the header to the bundle
+		b.addHeader(acks, subs, transports);
 		// Add as many messages as possible to the bundle
-		while(true) {
-			Batch batch = fillBatch(c, capacity);
-			if(batch == null) break; // No more messages to send
-			b.addBatch(batch);
-			long size = batch.getSize();
-			capacity -= size;
-			// If the batch is less than half full, stop trying - there may be
-			// more messages trickling in but we can't wait forever
-			if(size * 2 < Batch.MAX_SIZE) break;
-		}
-		b.close();
+		while(fillBatch(c, b));
+		b.finish();
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Bundle generated");
 		System.gc();
 	}
 
-	private Batch fillBatch(ContactId c, long capacity) throws DbException,
+	private boolean fillBatch(ContactId c, BundleWriter b) throws DbException,
 	IOException, GeneralSecurityException {
 		contactLock.readLock().lock();
 		try {
@@ -295,31 +277,38 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			messageLock.readLock().lock();
 			try {
 				Set<MessageId> sent;
-				Batch batch;
+				int bytesSent = 0;
+				BatchId batchId;
 				messageStatusLock.readLock().lock();
 				try {
 					Txn txn = db.startTransaction();
 					try {
-						capacity = Math.min(capacity, Batch.MAX_SIZE);
+						long capacity = Math.min(b.getRemainingCapacity(),
+								Batch.MAX_SIZE);
 						Iterator<MessageId> it =
 							db.getSendableMessages(txn, c, capacity).iterator();
 						if(!it.hasNext()) {
 							db.commitTransaction(txn);
-							return null; // No more messages to send
+							return false; // No more messages to send
 						}
 						sent = new HashSet<MessageId>();
-						BatchBuilder b = batchBuilderProvider.get();
+						List<Message> messages = new ArrayList<Message>();
 						while(it.hasNext()) {
 							MessageId m = it.next();
-							b.addMessage(db.getMessage(txn, m));
+							Message message = db.getMessage(txn, m);
+							bytesSent += message.getSize();
+							messages.add(message);
 							sent.add(m);
 						}
-						batch = b.build();
+						batchId = b.addBatch(messages);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
 						throw e;
-					} catch(SignatureException e) {
+					} catch(IOException e) {
+						db.abortTransaction(txn);
+						throw e;
+					} catch(GeneralSecurityException e) {
 						db.abortTransaction(txn);
 						throw e;
 					}
@@ -332,9 +321,10 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 					Txn txn = db.startTransaction();
 					try {
 						assert !sent.isEmpty();
-						db.addOutstandingBatch(txn, c, batch.getId(), sent);
+						db.addOutstandingBatch(txn, c, batchId, sent);
 						db.commitTransaction(txn);
-						return batch;
+						// Don't create another batch if this one was half-empty
+						return bytesSent > Batch.MAX_SIZE / 2;
 					} catch(DbException e) {
 						db.abortTransaction(txn);
 						throw e;
@@ -443,9 +433,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 
 	public void receiveBundle(ContactId c, BundleReader b) throws DbException,
 	IOException, GeneralSecurityException {
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Received bundle from " + c + ", "
-					+ b.getSize() + " bytes");
+		if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c);
 		Header h;
 		// Mark all messages in acked batches as seen
 		contactLock.readLock().lock();
@@ -536,7 +524,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 		if(LOG.isLoggable(Level.FINE))
 			LOG.fine("Received " + batches + " batches");
-		b.close();
+		b.finish();
 		retransmitLostBatches(c, h.getId());
 		System.gc();
 	}
diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
index f7caf0910741e1ed6cb11e7958dd330e64535ac6..9916738ea62fe2d99ed9028ae2cf05a760b70586 100644
--- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
+++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
@@ -3,8 +3,10 @@ package net.sf.briar.db;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.SignatureException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.logging.Level;
@@ -16,19 +18,16 @@ import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.NoSuchContactException;
 import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.BundleId;
 import net.sf.briar.api.protocol.BundleReader;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 /**
  * An implementation of DatabaseComponent using Java synchronization. This
@@ -52,10 +51,8 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 	private final Object transportLock = new Object();
 
 	@Inject
-	SynchronizedDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		super(db, cleaner, headerBuilderProvider, batchBuilderProvider);
+	SynchronizedDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner) {
+		super(db, cleaner);
 	}
 
 	public void close() throws DbException {
@@ -148,16 +145,16 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 	public void generateBundle(ContactId c, BundleWriter b) throws DbException,
 	IOException, GeneralSecurityException {
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Generating bundle for " + c);
-		HeaderBuilder h;
+		Set<BatchId> acks;
+		Set<GroupId> subs;
+		Map<String, String> transports;
 		// Add acks
 		synchronized(contactLock) {
 			if(!containsContact(c)) throw new NoSuchContactException();
-			h = headerBuilderProvider.get();
 			synchronized(messageStatusLock) {
 				Txn txn = db.startTransaction();
 				try {
-					Set<BatchId> acks = db.removeBatchesToAck(txn, c);
-					h.addAcks(acks);
+					acks = db.removeBatchesToAck(txn, c);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + acks.size() + " acks");
 					db.commitTransaction(txn);
@@ -173,8 +170,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			synchronized(subscriptionLock) {
 				Txn txn = db.startTransaction();
 				try {
-					Set<GroupId> subs = db.getSubscriptions(txn);
-					h.addSubscriptions(subs);
+					subs = db.getSubscriptions(txn);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + subs.size() + " subscriptions");
 					db.commitTransaction(txn);
@@ -190,8 +186,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 			synchronized(transportLock) {
 				Txn txn = db.startTransaction();
 				try {
-					Map<String, String> transports = db.getTransports(txn);
-					h.addTransports(transports);
+					transports = db.getTransports(txn);
 					if(LOG.isLoggable(Level.FINE))
 						LOG.fine("Added " + transports.size() + " transports");
 					db.commitTransaction(txn);
@@ -201,28 +196,16 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				}
 			}
 		}
-		// Sign the header and add it to the bundle
-		Header header = h.build();
-		long capacity = b.getCapacity();
-		capacity -= header.getSize();
-		b.addHeader(header);
+		// Add the header to the bundle
+		b.addHeader(acks, subs, transports);
 		// Add as many messages as possible to the bundle
-		while(true) {
-			Batch batch = fillBatch(c, capacity);
-			if(batch == null) break; // No more messages to send
-			b.addBatch(batch);
-			long size = batch.getSize();
-			capacity -= size;
-			// If the batch is less than half full, stop trying - there may be
-			// more messages trickling in but we can't wait forever
-			if(size * 2 < Batch.MAX_SIZE) break;
-		}
-		b.close();
+		while(fillBatch(c, b));
+		b.finish();
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Bundle generated");
 		System.gc();
 	}
 
-	private Batch fillBatch(ContactId c, long capacity) throws DbException,
+	private boolean fillBatch(ContactId c, BundleWriter b) throws DbException,
 	IOException, GeneralSecurityException {
 		synchronized(contactLock) {
 			if(!containsContact(c)) throw new NoSuchContactException();
@@ -230,26 +213,31 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 				synchronized(messageStatusLock) {
 					Txn txn = db.startTransaction();
 					try {
-						capacity = Math.min(capacity, Batch.MAX_SIZE);
+						long capacity = Math.min(b.getRemainingCapacity(),
+								Batch.MAX_SIZE);
 						Iterator<MessageId> it =
 							db.getSendableMessages(txn, c, capacity).iterator();
 						if(!it.hasNext()) {
 							db.commitTransaction(txn);
-							return null; // No more messages to send
+							return false; // No more messages to send
 						}
-						BatchBuilder b = batchBuilderProvider.get();
 						Set<MessageId> sent = new HashSet<MessageId>();
+						List<Message> messages = new ArrayList<Message>();
+						int bytesSent = 0;
 						while(it.hasNext()) {
 							MessageId m = it.next();
-							b.addMessage(db.getMessage(txn, m));
+							Message message = db.getMessage(txn, m);
+							bytesSent += message.getSize();
+							messages.add(message);
 							sent.add(m);
 						}
-						Batch batch = b.build();
+						BatchId batchId = b.addBatch(messages);
 						// Record the contents of the batch
 						assert !sent.isEmpty();
-						db.addOutstandingBatch(txn, c, batch.getId(), sent);
+						db.addOutstandingBatch(txn, c, batchId, sent);
 						db.commitTransaction(txn);
-						return batch;
+						// Don't create another batch if this one was half-empty
+						return bytesSent > Batch.MAX_SIZE / 2;
 					} catch(DbException e) {
 						db.abortTransaction(txn);
 						throw e;
@@ -337,9 +325,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 
 	public void receiveBundle(ContactId c, BundleReader b) throws DbException,
 	IOException, GeneralSecurityException {
-		if(LOG.isLoggable(Level.FINE))
-			LOG.fine("Received bundle from " + c + ", "
-					+ b.getSize() + " bytes");
+		if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c);
 		Header h;
 		// Mark all messages in acked batches as seen
 		synchronized(contactLock) {
@@ -409,7 +395,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 		}
 		if(LOG.isLoggable(Level.FINE))
 			LOG.fine("Received " + batches + " batches");
-		b.close();
+		b.finish();
 		retransmitLostBatches(c, h.getId());
 		System.gc();
 	}
diff --git a/components/net/sf/briar/protocol/BatchBuilderImpl.java b/components/net/sf/briar/protocol/BatchBuilderImpl.java
deleted file mode 100644
index 824ac38c56708f4bd082a5356a88695c7f331a15..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/BatchBuilderImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-import java.util.ArrayList;
-import java.util.List;
-
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.serial.WriterFactory;
-
-abstract class BatchBuilderImpl implements BatchBuilder {
-
-	protected final List<Message> messages = new ArrayList<Message>();
-	protected final KeyPair keyPair;
-	protected final Signature signature;
-	protected final MessageDigest messageDigest;
-	protected final WriterFactory writerFactory;
-
-	protected BatchBuilderImpl(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		this.keyPair = keyPair;
-		this.signature = signature;
-		this.messageDigest = messageDigest;
-		this.writerFactory = writerFactory;
-	}
-
-	public void addMessage(Message m) {
-		messages.add(m);
-	}
-}
diff --git a/components/net/sf/briar/protocol/BatchFactory.java b/components/net/sf/briar/protocol/BatchFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..8efe7fc0693f6a2dd96b361bbc868d36dd72cff0
--- /dev/null
+++ b/components/net/sf/briar/protocol/BatchFactory.java
@@ -0,0 +1,12 @@
+package net.sf.briar.protocol;
+
+import java.util.List;
+
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.Message;
+
+interface BatchFactory {
+
+	Batch createBatch(BatchId id, List<Message> messages);
+}
diff --git a/components/net/sf/briar/protocol/BatchFactoryImpl.java b/components/net/sf/briar/protocol/BatchFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..5254190d760984391c217a49a0c252bc7335c4d7
--- /dev/null
+++ b/components/net/sf/briar/protocol/BatchFactoryImpl.java
@@ -0,0 +1,14 @@
+package net.sf.briar.protocol;
+
+import java.util.List;
+
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.Message;
+
+public class BatchFactoryImpl implements BatchFactory {
+
+	public Batch createBatch(BatchId id, List<Message> messages) {
+		return new BatchImpl(id, messages);
+	}
+}
diff --git a/components/net/sf/briar/protocol/BatchImpl.java b/components/net/sf/briar/protocol/BatchImpl.java
index 97ccd5a71c8c0d70ffd3dbbd310cecddbf8cb86f..9f7244ff00432fcb78358068e6dae801a63f6f36 100644
--- a/components/net/sf/briar/protocol/BatchImpl.java
+++ b/components/net/sf/briar/protocol/BatchImpl.java
@@ -10,30 +10,18 @@ import net.sf.briar.api.protocol.Message;
 class BatchImpl implements Batch {
 
 	private final BatchId id;
-	private final long size;
 	private final List<Message> messages;
-	private final byte[] signature;
 
-	BatchImpl(BatchId id, long size, List<Message> messages, byte[] signature) {
+	BatchImpl(BatchId id, List<Message> messages) {
 		this.id = id;
-		this.size = size;
 		this.messages = messages;
-		this.signature = signature;
 	}
 
 	public BatchId getId() {
 		return id;
 	}
 
-	public long getSize() {
-		return size;
-	}
-
 	public Iterable<Message> getMessages() {
 		return messages;
 	}
-
-	public byte[] getSignature() {
-		return signature;
-	}
 }
diff --git a/components/net/sf/briar/protocol/BundleReaderImpl.java b/components/net/sf/briar/protocol/BundleReaderImpl.java
index ea397abdf279b2275e62f7b0d5d61f99c827367e..6461071db1ae3ce985f2573fd3e532c7c6407138 100644
--- a/components/net/sf/briar/protocol/BundleReaderImpl.java
+++ b/components/net/sf/briar/protocol/BundleReaderImpl.java
@@ -1,56 +1,70 @@
 package net.sf.briar.protocol;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.BundleId;
 import net.sf.briar.api.protocol.BundleReader;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageParser;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.Raw;
 import net.sf.briar.api.serial.Reader;
-
-import com.google.inject.Provider;
+import net.sf.briar.api.serial.ReaderFactory;
 
 /** A bundle that deserialises its contents on demand using a reader. */
 class BundleReaderImpl implements BundleReader {
 
 	private static enum State { START, FIRST_BATCH, MORE_BATCHES, END };
 
+	private final SigningDigestingInputStream in;
 	private final Reader r;
-	private final long size;
+	private final PublicKey publicKey;
+	private final Signature signature;
+	private final MessageDigest messageDigest;
 	private final MessageParser messageParser;
-	private final Provider<HeaderBuilder> headerBuilderProvider;
-	private final Provider<BatchBuilder> batchBuilderProvider;
+	private final HeaderFactory headerFactory;
+	private final BatchFactory batchFactory;
 	private State state = State.START;
 
-	BundleReaderImpl(Reader r, long size, MessageParser messageParser,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		this.r = r;
-		this.size = size;
+	BundleReaderImpl(InputStream in, ReaderFactory readerFactory,
+			PublicKey publicKey, Signature signature,
+			MessageDigest messageDigest, MessageParser messageParser,
+			HeaderFactory headerFactory, BatchFactory batchFactory) {
+		this.in = new SigningDigestingInputStream(in, signature, messageDigest);
+		r = readerFactory.createReader(this.in);
+		this.publicKey = publicKey;
+		this.signature = signature;
+		this.messageDigest = messageDigest;
 		this.messageParser = messageParser;
-		this.headerBuilderProvider = headerBuilderProvider;
-		this.batchBuilderProvider = batchBuilderProvider;
-	}
-
-	public long getSize() {
-		return size;
+		this.headerFactory = headerFactory;
+		this.batchFactory = batchFactory;
 	}
 
 	public Header getHeader() throws IOException, GeneralSecurityException {
 		if(state != State.START) throw new IllegalStateException();
+		state = State.FIRST_BATCH;
+		// Initialise the input stream
+		signature.initVerify(publicKey);
+		messageDigest.reset();
+		// Read the signed data
+		in.setDigesting(true);
+		in.setSigning(true);
 		r.setReadLimit(Header.MAX_SIZE);
 		Set<BatchId> acks = new HashSet<BatchId>();
 		for(Raw raw : r.readList(Raw.class)) {
@@ -64,15 +78,16 @@ class BundleReaderImpl implements BundleReader {
 			if(b.length != UniqueId.LENGTH) throw new FormatException();
 			subs.add(new GroupId(b));
 		}
-		Map<String, String> transports = r.readMap(String.class, String.class);
+		Map<String, String> transports =
+			r.readMap(String.class, String.class);
+		in.setSigning(false);
+		// Read and verify the signature
 		byte[] sig = r.readRaw();
-		state = State.FIRST_BATCH;
-		HeaderBuilder h = headerBuilderProvider.get();
-		h.addAcks(acks);
-		h.addSubscriptions(subs);
-		h.addTransports(transports);
-		h.setSignature(sig);
-		return h.build();
+		in.setDigesting(false);
+		if(!signature.verify(sig)) throw new SignatureException();
+		// Build and return the header
+		BundleId id = new BundleId(messageDigest.digest());
+		return headerFactory.createHeader(id, acks, subs, transports);
 	}
 
 	public Batch getNextBatch() throws IOException, GeneralSecurityException {
@@ -86,19 +101,31 @@ class BundleReaderImpl implements BundleReader {
 			state = State.END;
 			return null;
 		}
+		// Initialise the input stream
+		signature.initVerify(publicKey);
+		messageDigest.reset();
+		// Read the signed data
+		in.setDigesting(true);
+		in.setSigning(true);
 		r.setReadLimit(Batch.MAX_SIZE);
-		List<Raw> messages = r.readList(Raw.class);
-		BatchBuilder b = batchBuilderProvider.get();
-		for(Raw r : messages) {
+		List<Raw> rawMessages = r.readList(Raw.class);
+		in.setSigning(false);
+		// Read and verify the signature
+		byte[] sig = r.readRaw();
+		in.setDigesting(false);
+		if(!signature.verify(sig)) throw new SignatureException();
+		// Parse the messages
+		List<Message> messages = new ArrayList<Message>(rawMessages.size());
+		for(Raw r : rawMessages) {
 			Message m = messageParser.parseMessage(r.getBytes());
-			b.addMessage(m);
+			messages.add(m);
 		}
-		byte[] sig = r.readRaw();
-		b.setSignature(sig);
-		return b.build();
+		// Build and return the batch
+		BatchId id = new BatchId(messageDigest.digest());
+		return batchFactory.createBatch(id, messages);
 	}
 
-	public void close() throws IOException {
+	public void finish() throws IOException {
 		r.close();
 	}
 }
diff --git a/components/net/sf/briar/protocol/BundleWriterImpl.java b/components/net/sf/briar/protocol/BundleWriterImpl.java
index ba3f3f830691dc06e9ccb0ef0f7a9aa1b3ec2630..6dcdb65a15a2135e3d561441036f36faa723a956 100644
--- a/components/net/sf/briar/protocol/BundleWriterImpl.java
+++ b/components/net/sf/briar/protocol/BundleWriterImpl.java
@@ -1,59 +1,100 @@
 package net.sf.briar.protocol;
 
 import java.io.IOException;
+import java.io.OutputStream;
+import java.security.DigestOutputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Map;
 
-import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.BundleId;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
 
 /** A bundle builder that serialises its contents using a writer. */
 class BundleWriterImpl implements BundleWriter {
 
 	private static enum State { START, FIRST_BATCH, MORE_BATCHES, END };
 
+	private final SigningOutputStream out;
 	private final Writer w;
+	private final PrivateKey privateKey;
+	private final Signature signature;
+	private final MessageDigest messageDigest;
 	private final long capacity;
 	private State state = State.START;
 
-	BundleWriterImpl(Writer w, long capacity) {
-		this.w = w;
+	BundleWriterImpl(OutputStream out, WriterFactory writerFactory,
+			PrivateKey privateKey, Signature signature,
+			MessageDigest messageDigest, long capacity) {
+		OutputStream out1 = new DigestOutputStream(out, messageDigest);
+		this.out = new SigningOutputStream(out1, signature);
+		w = writerFactory.createWriter(this.out);
+		this.privateKey = privateKey;
+		this.signature = signature;
+		this.messageDigest = messageDigest;
 		this.capacity = capacity;
 	}
 
-	public long getCapacity() {
-		return capacity;
+	public long getRemainingCapacity() {
+		return capacity - w.getRawBytesWritten();
 	}
 
-	public void addHeader(Header h) throws IOException {
+	public BundleId addHeader(Iterable<BatchId> acks, Iterable<GroupId> subs,
+			Map<String, String> transports) throws IOException,
+			GeneralSecurityException {
 		if(state != State.START) throw new IllegalStateException();
+		// Initialise the output stream
+		signature.initSign(privateKey);
+		messageDigest.reset();
+		// Write the data to be signed
+		out.setSigning(true);
 		w.writeListStart();
-		for(BatchId ack : h.getAcks()) w.writeRaw(ack);
+		for(BatchId ack : acks) w.writeRaw(ack);
 		w.writeListEnd();
 		w.writeListStart();
-		for(GroupId sub : h.getSubscriptions()) w.writeRaw(sub);
+		for(GroupId sub : subs) w.writeRaw(sub);
 		w.writeListEnd();
-		w.writeMap(h.getTransports());
-		w.writeRaw(h.getSignature());
+		w.writeMap(transports);
+		out.setSigning(false);
+		// Create and write the signature
+		byte[] sig = signature.sign();
+		w.writeRaw(sig);
+		// Calculate and return the ID
 		state = State.FIRST_BATCH;
+		return new BundleId(messageDigest.digest());
 	}
 
-	public void addBatch(Batch b) throws IOException {
+	public BatchId addBatch(Iterable<Message> messages) throws IOException,
+	GeneralSecurityException {
 		if(state == State.FIRST_BATCH) {
 			w.writeListStart();
 			state = State.MORE_BATCHES;
 		}
 		if(state != State.MORE_BATCHES) throw new IllegalStateException();
+		// Initialise the output stream
+		signature.initSign(privateKey);
+		messageDigest.reset();
+		// Write the data to be signed
+		out.setSigning(true);
 		w.writeListStart();
-		for(Message m : b.getMessages()) w.writeRaw(m.getBytes());
+		for(Message m : messages) w.writeRaw(m);
 		w.writeListEnd();
-		w.writeRaw(b.getSignature());
+		out.setSigning(false);
+		// Create and write the signature
+		byte[] sig = signature.sign();
+		w.writeRaw(sig);
+		// Calculate and return the ID
+		return new BatchId(messageDigest.digest());
 	}
 
-	public void close() throws IOException {
+	public void finish() throws IOException {
 		if(state == State.FIRST_BATCH) {
 			w.writeListStart();
 			state = State.MORE_BATCHES;
diff --git a/components/net/sf/briar/protocol/HeaderBuilderImpl.java b/components/net/sf/briar/protocol/HeaderBuilderImpl.java
deleted file mode 100644
index d40debd72c46457c59bddf9b5c4ecb2057248023..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/HeaderBuilderImpl.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.HeaderBuilder;
-import net.sf.briar.api.serial.WriterFactory;
-
-abstract class HeaderBuilderImpl implements HeaderBuilder {
-
-	protected final List<BatchId> acks = new ArrayList<BatchId>();
-	protected final List<GroupId> subs = new ArrayList<GroupId>();
-	protected final Map<String, String> transports =
-		new LinkedHashMap<String, String>();
-
-	protected final KeyPair keyPair;
-	protected final Signature signature;
-	protected final MessageDigest messageDigest;
-	protected final WriterFactory writerFactory;
-
-	protected HeaderBuilderImpl(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		this.keyPair = keyPair;
-		this.signature = signature;
-		this.messageDigest = messageDigest;
-		this.writerFactory = writerFactory;
-	}
-
-	public void addAcks(Iterable<BatchId> acks) {
-		for(BatchId ack : acks) this.acks.add(ack);
-	}
-
-	public void addSubscriptions(Iterable<GroupId> subs) {
-		for(GroupId sub : subs) this.subs.add(sub);
-	}
-
-	public void addTransports(Map<String, String> transports) {
-		for(String key : transports.keySet()) {
-			this.transports.put(key, transports.get(key));
-		}
-	}
-}
diff --git a/components/net/sf/briar/protocol/HeaderFactory.java b/components/net/sf/briar/protocol/HeaderFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..06c47eac93c26ff79268837ce4fe483be9e731f4
--- /dev/null
+++ b/components/net/sf/briar/protocol/HeaderFactory.java
@@ -0,0 +1,15 @@
+package net.sf.briar.protocol;
+
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.BundleId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Header;
+
+interface HeaderFactory {
+
+	Header createHeader(BundleId id, Set<BatchId> acks, Set<GroupId> subs,
+			Map<String, String> transports);
+}
diff --git a/components/net/sf/briar/protocol/HeaderFactoryImpl.java b/components/net/sf/briar/protocol/HeaderFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca71a7b02cae351e9a37bf14f36be087e90268d9
--- /dev/null
+++ b/components/net/sf/briar/protocol/HeaderFactoryImpl.java
@@ -0,0 +1,17 @@
+package net.sf.briar.protocol;
+
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.BundleId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Header;
+
+class HeaderFactoryImpl implements HeaderFactory {
+
+	public Header createHeader(BundleId id, Set<BatchId> acks,
+			Set<GroupId> subs, Map<String, String> transports) {
+		return new HeaderImpl(id, acks, subs, transports);
+	}
+}
diff --git a/components/net/sf/briar/protocol/HeaderImpl.java b/components/net/sf/briar/protocol/HeaderImpl.java
index 763daf0f17e43f24bc4b2a0ec4b88edca2ba4efe..c1f81c1c126884771667fc1f9df2b305f678bb9f 100644
--- a/components/net/sf/briar/protocol/HeaderImpl.java
+++ b/components/net/sf/briar/protocol/HeaderImpl.java
@@ -12,44 +12,31 @@ import net.sf.briar.api.protocol.Header;
 class HeaderImpl implements Header {
 
 	private final BundleId id;
-	private final long size;
 	private final Set<BatchId> acks;
-	private final Set<GroupId> subscriptions;
+	private final Set<GroupId> subs;
 	private final Map<String, String> transports;
-	private final byte[] signature;
 
-	HeaderImpl(BundleId id, long size, Set<BatchId> acks,
-			Set<GroupId> subscriptions, Map<String, String> transports,
-			byte[] signature) {
+	HeaderImpl(BundleId id, Set<BatchId> acks, Set<GroupId> subs,
+			Map<String, String> transports) {
 		this.id = id;
-		this.size = size;
 		this.acks = acks;
-		this.subscriptions = subscriptions;
+		this.subs = subs;
 		this.transports = transports;
-		this.signature = signature;
 	}
 
 	public BundleId getId() {
 		return id;
 	}
 
-	public long getSize() {
-		return size;
-	}
-
 	public Set<BatchId> getAcks() {
 		return acks;
 	}
 
 	public Set<GroupId> getSubscriptions() {
-		return subscriptions;
+		return subs;
 	}
 
 	public Map<String, String> getTransports() {
 		return transports;
 	}
-
-	public byte[] getSignature() {
-		return signature;
-	}
 }
diff --git a/components/net/sf/briar/protocol/IncomingBatchBuilder.java b/components/net/sf/briar/protocol/IncomingBatchBuilder.java
deleted file mode 100644
index 30f5952fac422e2ef2b39570957b660133555de2..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/IncomingBatchBuilder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-import java.security.SignatureException;
-
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-
-public class IncomingBatchBuilder extends BatchBuilderImpl {
-
-	IncomingBatchBuilder(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		super(keyPair, signature, messageDigest, writerFactory);
-	}
-
-	private byte[] sig = null;
-
-	public void setSignature(byte[] sig) {
-		this.sig = sig;
-	}
-
-	public Batch build() throws IOException, GeneralSecurityException {
-		if(sig == null) throw new IllegalStateException();
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		Writer w = writerFactory.createWriter(out);
-		w.writeList(messages);
-		byte[] signable = out.toByteArray();
-		signature.initVerify(keyPair.getPublic());
-		signature.update(signable);
-		if(!signature.verify(sig)) throw new SignatureException();
-		w.writeRaw(sig);
-		w.close();
-		byte[] raw = out.toByteArray();
-		messageDigest.reset();
-		messageDigest.update(raw);
-		BatchId id = new BatchId(messageDigest.digest());
-		return new BatchImpl(id, raw.length, messages, sig);
-	}
-}
diff --git a/components/net/sf/briar/protocol/IncomingHeaderBuilder.java b/components/net/sf/briar/protocol/IncomingHeaderBuilder.java
deleted file mode 100644
index e83b07b8cfb6b1d57f52e2c10cf723cd56a5b14d..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/IncomingHeaderBuilder.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.util.HashSet;
-import java.util.Set;
-
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.BundleId;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-
-class IncomingHeaderBuilder extends HeaderBuilderImpl {
-
-	private byte[] sig = null;
-
-	IncomingHeaderBuilder(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		super(keyPair, signature, messageDigest, writerFactory);
-	}
-
-	public void setSignature(byte[] sig) {
-		this.sig = sig;
-	}
-
-	public Header build() throws IOException, GeneralSecurityException {
-		if(sig == null) throw new IllegalStateException();
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		Writer w = writerFactory.createWriter(out);
-		w.writeList(acks);
-		w.writeList(subs);
-		w.writeMap(transports);
-		byte[] signable = out.toByteArray();
-		signature.initVerify(keyPair.getPublic());
-		signature.update(signable);
-		if(!signature.verify(sig)) throw new SignatureException();
-		w.writeRaw(sig);
-		w.close();
-		byte[] raw = out.toByteArray();
-		messageDigest.reset();
-		messageDigest.update(raw);
-		BundleId id = new BundleId(messageDigest.digest());
-		Set<BatchId> ackSet = new HashSet<BatchId>(acks);
-		Set<GroupId> subSet = new HashSet<GroupId>(subs);
-		return new HeaderImpl(id, raw.length, ackSet, subSet, transports, sig);
-	}
-}
diff --git a/components/net/sf/briar/protocol/OutgoingBatchBuilder.java b/components/net/sf/briar/protocol/OutgoingBatchBuilder.java
deleted file mode 100644
index 118886098db5e273786f1cac7944260471d6d3ed..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/OutgoingBatchBuilder.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-
-public class OutgoingBatchBuilder extends BatchBuilderImpl {
-
-	OutgoingBatchBuilder(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		super(keyPair, signature, messageDigest, writerFactory);
-	}
-
-	public void setSignature(byte[] sig) {
-		throw new UnsupportedOperationException();
-	}
-
-	public Batch build() throws IOException, GeneralSecurityException {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		Writer w = writerFactory.createWriter(out);
-		w.writeList(messages);
-		byte[] signable = out.toByteArray();
-		signature.initSign(keyPair.getPrivate());
-		signature.update(signable);
-		byte[] sig = signature.sign();
-		w.writeRaw(sig);
-		w.close();
-		byte[] raw = out.toByteArray();
-		messageDigest.reset();
-		messageDigest.update(raw);
-		BatchId id = new BatchId(messageDigest.digest());
-		return new BatchImpl(id, raw.length, messages, sig);
-	}
-}
diff --git a/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java b/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java
deleted file mode 100644
index 970624defcde4069d2e26771a1b086dc103f64b0..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/protocol/OutgoingHeaderBuilder.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.MessageDigest;
-import java.security.Signature;
-import java.util.HashSet;
-import java.util.Set;
-
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.BundleId;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-
-public class OutgoingHeaderBuilder extends HeaderBuilderImpl {
-
-	OutgoingHeaderBuilder(KeyPair keyPair, Signature signature,
-			MessageDigest messageDigest, WriterFactory writerFactory) {
-		super(keyPair, signature, messageDigest, writerFactory);
-	}
-
-	public void setSignature(byte[] sig) {
-		throw new UnsupportedOperationException();
-	}
-
-	public Header build() throws IOException, GeneralSecurityException {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		Writer w = writerFactory.createWriter(out);
-		w.writeList(acks);
-		w.writeList(subs);
-		w.writeMap(transports);
-		byte[] signable = out.toByteArray();
-		signature.initSign(keyPair.getPrivate());
-		signature.update(signable);
-		byte[] sig = signature.sign();
-		w.writeRaw(sig);
-		w.close();
-		byte[] raw = out.toByteArray();
-		messageDigest.reset();
-		messageDigest.update(raw);
-		BundleId id = new BundleId(messageDigest.digest());
-		Set<BatchId> ackSet = new HashSet<BatchId>(acks);
-		Set<GroupId> subSet = new HashSet<GroupId>(subs);
-		return new HeaderImpl(id, raw.length, ackSet, subSet, transports, sig);
-	}
-}
diff --git a/components/net/sf/briar/protocol/SigningDigestingInputStream.java b/components/net/sf/briar/protocol/SigningDigestingInputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..d95768eff1125b1218f7b780e28925bbd978387c
--- /dev/null
+++ b/components/net/sf/briar/protocol/SigningDigestingInputStream.java
@@ -0,0 +1,116 @@
+package net.sf.briar.protocol;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * An input stream that passes its input through a signature and a message
+ * digest. The signature and message digest lag behind the input by one byte
+ * until the end of the input is reached, to allow users of this class to
+ * maintain one byte of lookahead without affecting the signature or digest.
+ */
+class SigningDigestingInputStream extends FilterInputStream {
+
+	private final Signature signature;
+	private final MessageDigest messageDigest;
+	private byte nextByte = 0;
+	private boolean started = false, eof = false;
+	private boolean signing = false, digesting = false;
+
+	protected SigningDigestingInputStream(InputStream in, Signature signature,
+			MessageDigest messageDigest) {
+		super(in);
+		this.signature = signature;
+		this.messageDigest = messageDigest;
+	}
+
+	public void setSigning(boolean signing) {
+		this.signing = signing;
+	}
+
+	public void setDigesting(boolean digesting) {
+		this.digesting = digesting;
+	}
+
+	private void write(byte b) throws IOException {
+		if(signing) {
+			try {
+				signature.update(b);
+			} catch(SignatureException e) {
+				throw new IOException(e);
+			}
+		}
+		if(digesting) messageDigest.update(b);
+	}
+
+	private void write(byte[] b, int off, int len) throws IOException {
+		if(signing) {
+			try {
+				signature.update(b, off, len);
+			} catch(SignatureException e) {
+				throw new IOException(e);
+			}
+		}
+		if(digesting) messageDigest.update(b, off, len);
+	}
+
+	@Override
+	public void mark(int readLimit) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public boolean markSupported() {
+		return false;
+	}
+
+	@Override
+	public int read() throws IOException {
+		if(eof) return -1;
+		if(started) write(nextByte);
+		started = true;
+		int i = in.read();
+		if(i == -1) {
+			eof = true;
+			return -1;
+		}
+		nextByte = (byte) (i > 127 ? i - 256 : i);
+		return i;
+	}
+
+	@Override
+	public int read(byte[] b) throws IOException {
+		return read(b, 0, b.length);
+	}
+
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		if(eof) return -1;
+		if(started) write(nextByte);
+		started = true;
+		int read = in.read(b, off, len);
+		if(read == -1) {
+			eof = true;
+			return -1;
+		}
+		if(read > 0) {
+			write(b, off, read - 1);
+			nextByte = b[off + read - 1];
+		}
+		return read;
+	}
+
+	@Override
+	public void reset() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public long skip(long n) {
+		throw new UnsupportedOperationException();
+	}
+}
diff --git a/components/net/sf/briar/protocol/SigningOutputStream.java b/components/net/sf/briar/protocol/SigningOutputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..a00df05c6fc16e498f57569739ab158c3b812cdd
--- /dev/null
+++ b/components/net/sf/briar/protocol/SigningOutputStream.java
@@ -0,0 +1,52 @@
+package net.sf.briar.protocol;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/** An output stream that passes its output through a signature. */
+class SigningOutputStream extends FilterOutputStream {
+
+	private final Signature signature;
+	private boolean signing = false;
+
+	public SigningOutputStream(OutputStream out, Signature signature) {
+		super(out);
+		this.signature = signature;
+	}
+
+	void setSigning(boolean signing) {
+		this.signing = signing;
+	}
+
+	@Override
+	public void write(byte[] b) throws IOException {
+		write(b, 0, b.length);
+	}
+
+	@Override
+	public void write(byte[] b, int off, int len) throws IOException {
+		out.write(b, off, len);
+		if(signing) {
+			try {
+				signature.update(b, off, len);
+			} catch(SignatureException e) {
+				throw new IOException(e);
+			}
+		}
+	}
+
+	@Override
+	public void write(int b) throws IOException {
+		out.write(b);
+		if(signing) {
+			try {
+				signature.update((byte) b);
+			} catch(SignatureException e) {
+				throw new IOException(e);
+			}
+		}
+	}
+}
diff --git a/test/net/sf/briar/db/DatabaseComponentImplTest.java b/test/net/sf/briar/db/DatabaseComponentImplTest.java
index ba3e7a4e08fc72c9ba66e652f2dde963f0cb51f8..98e39c1bc67da8cea668620f08a11ebcfde2a2af 100644
--- a/test/net/sf/briar/db/DatabaseComponentImplTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentImplTest.java
@@ -6,8 +6,6 @@ import static net.sf.briar.api.db.DatabaseComponent.MIN_FREE_SPACE;
 import java.util.Collections;
 
 import net.sf.briar.api.db.DbException;
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.db.DatabaseCleaner.Callback;
 
@@ -15,8 +13,6 @@ import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
-import com.google.inject.Provider;
-
 /**
  * Tests that use the DatabaseCleaner.Callback interface of
  * DatabaseComponentImpl.
@@ -24,9 +20,7 @@ import com.google.inject.Provider;
 public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 
 	protected abstract <T> DatabaseComponentImpl<T> createDatabaseComponentImpl(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner);
 
 	@Test
 	public void testNotCleanedIfEnoughFreeSpace() throws DbException {
@@ -34,18 +28,11 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE));
 		}});
-		Callback db = createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		Callback db = createDatabaseComponentImpl(database, cleaner);
 
 		db.checkFreeSpaceAndClean();
 
@@ -58,12 +45,6 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE - 1));
@@ -76,8 +57,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE));
 		}});
-		Callback db = createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		Callback db = createDatabaseComponentImpl(database, cleaner);
 
 		db.checkFreeSpaceAndClean();
 
@@ -91,12 +71,6 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE - 1));
@@ -111,8 +85,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE));
 		}});
-		Callback db = createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		Callback db = createDatabaseComponentImpl(database, cleaner);
 
 		db.checkFreeSpaceAndClean();
 
@@ -126,12 +99,6 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE - 1));
@@ -148,8 +115,7 @@ public abstract class DatabaseComponentImplTest extends DatabaseComponentTest {
 			oneOf(database).getFreeSpace();
 			will(returnValue(MIN_FREE_SPACE));
 		}});
-		Callback db = createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		Callback db = createDatabaseComponentImpl(database, cleaner);
 
 		db.checkFreeSpaceAndClean();
 
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index fc8fa863432d4351d4cc9d80d79f2604d187b488..ebb90483ca4706f98e429d5598d8220c832b7781 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -14,14 +14,12 @@ import net.sf.briar.api.db.NoSuchContactException;
 import net.sf.briar.api.db.Status;
 import net.sf.briar.api.protocol.AuthorId;
 import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.BundleId;
 import net.sf.briar.api.protocol.BundleReader;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.protocol.MessageImpl;
@@ -30,8 +28,6 @@ import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
-import com.google.inject.Provider;
-
 public abstract class DatabaseComponentTest extends TestCase {
 
 	protected final Object txn = new Object();
@@ -73,9 +69,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 	}
 
 	protected abstract <T> DatabaseComponent createDatabaseComponent(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner);
 
 	@Test
 	public void testSimpleCalls() throws DbException {
@@ -85,12 +79,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -129,8 +117,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			oneOf(cleaner).stopCleaning();
 			oneOf(database).close();
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.open(false);
 		assertEquals(Rating.UNRATED, db.getRating(authorId));
@@ -153,12 +140,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// setRating(Rating.GOOD)
 			allowing(database).startTransaction();
@@ -175,8 +156,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(MessageId.NONE));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.setRating(authorId, Rating.GOOD);
 
@@ -189,12 +169,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// setRating(Rating.GOOD)
 			oneOf(database).startTransaction();
@@ -214,8 +188,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.setRating(authorId, Rating.GOOD);
 
@@ -229,12 +202,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// setRating(Rating.GOOD)
 			oneOf(database).startTransaction();
@@ -258,8 +225,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(groupId1));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.setRating(authorId, Rating.GOOD);
 
@@ -273,12 +239,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// setRating(Rating.GOOD)
 			oneOf(database).startTransaction();
@@ -305,8 +265,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			oneOf(database).setSendability(txn, parentId, 2);
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.setRating(authorId, Rating.GOOD);
 
@@ -320,12 +279,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// setRating(Rating.GOOD)
 			oneOf(database).startTransaction();
@@ -354,8 +307,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(MessageId.NONE));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.setRating(authorId, Rating.GOOD);
 
@@ -369,12 +321,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// addLocallyGeneratedMessage(message)
 			oneOf(database).startTransaction();
@@ -383,8 +329,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.addLocallyGeneratedMessage(message);
 
@@ -397,12 +342,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// addLocallyGeneratedMessage(message)
 			oneOf(database).startTransaction();
@@ -413,8 +352,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.addLocallyGeneratedMessage(message);
 
@@ -427,12 +365,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// addLocallyGeneratedMessage(message)
 			oneOf(database).startTransaction();
@@ -452,8 +384,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			oneOf(database).setSendability(txn, messageId, 0);
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.addLocallyGeneratedMessage(message);
 
@@ -467,12 +398,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		context.checking(new Expectations() {{
 			// addLocallyGeneratedMessage(message)
 			oneOf(database).startTransaction();
@@ -495,8 +420,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(MessageId.NONE));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.addLocallyGeneratedMessage(message);
 
@@ -510,12 +434,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		final BundleWriter bundleBuilder = context.mock(BundleWriter.class);
 		context.checking(new Expectations() {{
 			// Check that the contact is still in the DB
@@ -525,8 +443,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		try {
 			db.generateBundle(contactId, bundleBuilder);
@@ -543,72 +460,43 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		final BundleWriter bundleWriter = context.mock(BundleWriter.class);
-		final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class);
-		final Header header = context.mock(Header.class);
-		final BatchBuilder batchBuilder = context.mock(BatchBuilder.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));
-			// Build the header
-			oneOf(headerBuilderProvider).get();
-			will(returnValue(headerBuilder));
 			// Add acks to the header
 			oneOf(database).removeBatchesToAck(txn, contactId);
 			will(returnValue(acks));
-			oneOf(headerBuilder).addAcks(acks);
 			// Add subscriptions to the header
 			oneOf(database).getSubscriptions(txn);
 			will(returnValue(subs));
-			oneOf(headerBuilder).addSubscriptions(subs);
 			// Add transports to the header
 			oneOf(database).getTransports(txn);
 			will(returnValue(transports));
-			oneOf(headerBuilder).addTransports(transports);
 			// Build the header
-			oneOf(headerBuilder).build();
-			will(returnValue(header));
-			oneOf(bundleWriter).getCapacity();
-			will(returnValue(1024L * 1024L));
-			oneOf(header).getSize();
-			will(returnValue(headerSize));
-			oneOf(bundleWriter).addHeader(header);
+			oneOf(bundleWriter).addHeader(acks, subs, transports);
+			will(returnValue(bundleId));
 			// Add a batch to the bundle
+			oneOf(bundleWriter).getRemainingCapacity();
+			will(returnValue(1024L * 1024L - headerSize));
 			oneOf(database).getSendableMessages(txn, contactId,
 					Batch.MAX_SIZE - headerSize);
 			will(returnValue(messages));
-			oneOf(batchBuilderProvider).get();
-			will(returnValue(batchBuilder));
 			oneOf(database).getMessage(txn, messageId);
 			will(returnValue(message));
-			oneOf(batchBuilder).addMessage(message);
-			oneOf(batchBuilder).build();
-			will(returnValue(batch));
-			// Record the batch as outstanding
-			oneOf(batch).getId();
+			// Add the batch to the bundle
+			oneOf(bundleWriter).addBatch(Collections.singletonList(message));
 			will(returnValue(batchId));
+			// Record the outstanding batch
 			oneOf(database).addOutstandingBatch(
 					txn, contactId, batchId, messages);
-			// Add the batch to the bundle
-			oneOf(bundleWriter).addBatch(batch);
-			// Check whether to add another batch
-			oneOf(batch).getSize();
-			will(returnValue((long) message.getSize()));
-			// No, just send the bundle
-			oneOf(bundleWriter).close();
+			// Send the bundle
+			oneOf(bundleWriter).finish();
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.generateBundle(contactId, bundleWriter);
 
@@ -622,12 +510,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		final BundleReader bundleReader = context.mock(BundleReader.class);
 		context.checking(new Expectations() {{
 			// Check that the contact is still in the DB
@@ -637,8 +519,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		try {
 			db.receiveBundle(contactId, bundleReader);
@@ -654,12 +535,6 @@ public abstract class DatabaseComponentTest extends TestCase {
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
 		final BundleReader bundleReader = context.mock(BundleReader.class);
 		final Header header = context.mock(Header.class);
 		final Batch batch = context.mock(Batch.class);
@@ -702,7 +577,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			// Any more batches? Nope
 			oneOf(bundleReader).getNextBatch();
 			will(returnValue(null));
-			oneOf(bundleReader).close();
+			oneOf(bundleReader).finish();
 			// Lost batches
 			oneOf(header).getId();
 			will(returnValue(bundleId));
@@ -710,8 +585,7 @@ public abstract class DatabaseComponentTest extends TestCase {
 			will(returnValue(Collections.singleton(batchId)));
 			oneOf(database).removeLostBatch(txn, contactId, batchId);
 		}});
-		DatabaseComponent db = createDatabaseComponent(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
 		db.receiveBundle(contactId, bundleReader);
 
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 9596a1930d3822de7e0c32901d65a50d0fcc0618..18253c000d2181623bdfa06289435875bd4181f1 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -815,8 +815,8 @@ public class H2DatabaseTest extends TestCase {
 	private static class TestMessageFactory implements MessageFactory {
 
 		public Message createMessage(MessageId id, MessageId parent,
-				GroupId group, AuthorId author, long timestamp, byte[] body) {
-			return new MessageImpl(id, parent, group, author, timestamp, body);
+				GroupId group, AuthorId author, long timestamp, byte[] raw) {
+			return new MessageImpl(id, parent, group, author, timestamp, raw);
 		}
 	}
 }
diff --git a/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java
index ccc1a714d3f4064b2514c3f1b8d659a9243238b6..b0d5291f5aaa22087ef56c589a0ec38ca5907d31 100644
--- a/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java
+++ b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java
@@ -1,29 +1,19 @@
 package net.sf.briar.db;
 
 import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.HeaderBuilder;
-
-import com.google.inject.Provider;
 
 public class ReadWriteLockDatabaseComponentTest
 extends DatabaseComponentImplTest {
 
 	@Override
 	protected <T> DatabaseComponent createDatabaseComponent(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		return createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner) {
+		return createDatabaseComponentImpl(database, cleaner);
 	}
 
 	@Override
 	protected <T> DatabaseComponentImpl<T> createDatabaseComponentImpl(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		return new ReadWriteLockDatabaseComponent<T>(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner) {
+		return new ReadWriteLockDatabaseComponent<T>(database, cleaner);
 	}
 }
diff --git a/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java
index 5c8f576c3d849b5aeae43d770cabfe266c2f6954..4c24f1d0edaf721cc835833d2e83c68fe5727f5e 100644
--- a/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java
+++ b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java
@@ -1,29 +1,19 @@
 package net.sf.briar.db;
 
 import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.HeaderBuilder;
-
-import com.google.inject.Provider;
 
 public class SynchronizedDatabaseComponentTest
 extends DatabaseComponentImplTest {
 
 	@Override
 	protected <T> DatabaseComponent createDatabaseComponent(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		return createDatabaseComponentImpl(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner) {
+		return createDatabaseComponentImpl(database, cleaner);
 	}
 
 	@Override
 	protected <T> DatabaseComponentImpl<T> createDatabaseComponentImpl(
-			Database<T> database, DatabaseCleaner cleaner,
-			Provider<HeaderBuilder> headerBuilderProvider,
-			Provider<BatchBuilder> batchBuilderProvider) {
-		return new SynchronizedDatabaseComponent<T>(database, cleaner,
-				headerBuilderProvider, batchBuilderProvider);
+			Database<T> database, DatabaseCleaner cleaner) {
+		return new SynchronizedDatabaseComponent<T>(database, cleaner);
 	}
 }
diff --git a/test/net/sf/briar/protocol/BundleReadWriteTest.java b/test/net/sf/briar/protocol/BundleReadWriteTest.java
index 8a8851f925b18bb2224e75f557622d1d7566b71e..5460942084622306a6ddfb9852e5a2dd11a52e21 100644
--- a/test/net/sf/briar/protocol/BundleReadWriteTest.java
+++ b/test/net/sf/briar/protocol/BundleReadWriteTest.java
@@ -24,21 +24,17 @@ import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.BundleReader;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.MessageParser;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
-import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
 import net.sf.briar.serial.ReaderFactoryImpl;
 import net.sf.briar.serial.WriterFactoryImpl;
@@ -47,8 +43,6 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import com.google.inject.Provider;
-
 public class BundleReadWriteTest extends TestCase {
 
 	private static final String SIGNATURE_ALGO = "SHA256withRSA";
@@ -74,7 +68,7 @@ public class BundleReadWriteTest extends TestCase {
 
 	private final KeyPair keyPair;
 	private final Signature sig;
-	private final MessageDigest digest;
+	private final MessageDigest dig;
 	private final KeyParser keyParser;
 	private final Message message;
 
@@ -82,7 +76,7 @@ public class BundleReadWriteTest extends TestCase {
 		super();
 		keyPair = KeyPairGenerator.getInstance(KEY_PAIR_ALGO).generateKeyPair();
 		sig = Signature.getInstance(SIGNATURE_ALGO);
-		digest = MessageDigest.getInstance(DIGEST_ALGO);
+		dig = MessageDigest.getInstance(DIGEST_ALGO);
 		final KeyFactory keyFactory = KeyFactory.getInstance(KEY_PAIR_ALGO);
 		keyParser = new KeyParser() {
 			public PublicKey parsePublicKey(byte[] encodedKey)
@@ -91,8 +85,8 @@ public class BundleReadWriteTest extends TestCase {
 				return keyFactory.generatePublic(e);
 			}
 		};
-		assertEquals(digest.getDigestLength(), UniqueId.LENGTH);
-		MessageEncoder messageEncoder = new MessageEncoderImpl(sig, digest, wf);
+		assertEquals(dig.getDigestLength(), UniqueId.LENGTH);
+		MessageEncoder messageEncoder = new MessageEncoderImpl(sig, dig, wf);
 		message = messageEncoder.encodeMessage(MessageId.NONE, sub, nick,
 				keyPair, messageBody.getBytes("UTF-8"));
 	}
@@ -104,23 +98,13 @@ public class BundleReadWriteTest extends TestCase {
 
 	@Test
 	public void testWriteBundle() throws Exception {
-		HeaderBuilder h = new OutgoingHeaderBuilder(keyPair, sig, digest, wf);
-		h.addAcks(acks);
-		h.addSubscriptions(subs);
-		h.addTransports(transports);
-		Header header = h.build();
-
-		BatchBuilder b = new OutgoingBatchBuilder(keyPair, sig, digest, wf);
-		b.addMessage(message);
-		Batch batch = b.build();
-
 		FileOutputStream out = new FileOutputStream(bundle);
-		Writer writer = new WriterFactoryImpl().createWriter(out);
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
+		BundleWriter w = new BundleWriterImpl(out, wf, keyPair.getPrivate(),
+				sig, dig, capacity);
 
-		w.addHeader(header);
-		w.addBatch(batch);
-		w.close();
+		w.addHeader(acks, subs, transports);
+		w.addBatch(Collections.singleton(message));
+		w.finish();
 
 		assertTrue(bundle.exists());
 		assertTrue(bundle.length() > message.getSize());
@@ -132,24 +116,11 @@ public class BundleReadWriteTest extends TestCase {
 		testWriteBundle();
 
 		MessageParser messageParser =
-			new MessageParserImpl(keyParser, sig, digest, rf);
-		Provider<HeaderBuilder> headerBuilderProvider =
-			new Provider<HeaderBuilder>() {
-			public HeaderBuilder get() {
-				return new IncomingHeaderBuilder(keyPair, sig, digest, wf);
-			}
-		};
-		Provider<BatchBuilder> batchBuilderProvider =
-			new Provider<BatchBuilder>() {
-			public BatchBuilder get() {
-				return new IncomingBatchBuilder(keyPair, sig, digest, wf);
-			}
-		};
-
+			new MessageParserImpl(keyParser, sig, dig, rf);
 		FileInputStream in = new FileInputStream(bundle);
-		Reader reader = new ReaderFactoryImpl().createReader(in);
-		BundleReader r = new BundleReaderImpl(reader, bundle.length(),
-				messageParser, headerBuilderProvider, batchBuilderProvider);
+		BundleReader r = new BundleReaderImpl(in, rf, keyPair.getPublic(), sig,
+				dig, messageParser, new HeaderFactoryImpl(),
+				new BatchFactoryImpl());
 
 		Header h = r.getHeader();
 		assertEquals(acks, h.getAcks());
@@ -167,7 +138,7 @@ public class BundleReadWriteTest extends TestCase {
 		assertTrue(Arrays.equals(message.getBytes(), m.getBytes()));
 		assertFalse(i.hasNext());
 		assertNull(r.getNextBatch());
-		r.close();
+		r.finish();
 	}
 
 
@@ -184,24 +155,11 @@ public class BundleReadWriteTest extends TestCase {
 		f.close();
 
 		MessageParser messageParser =
-			new MessageParserImpl(keyParser, sig, digest, rf);
-		Provider<HeaderBuilder> headerBuilderProvider =
-			new Provider<HeaderBuilder>() {
-			public HeaderBuilder get() {
-				return new IncomingHeaderBuilder(keyPair, sig, digest, wf);
-			}
-		};
-		Provider<BatchBuilder> batchBuilderProvider =
-			new Provider<BatchBuilder>() {
-			public BatchBuilder get() {
-				return new IncomingBatchBuilder(keyPair, sig, digest, wf);
-			}
-		};
-
+			new MessageParserImpl(keyParser, sig, dig, rf);
 		FileInputStream in = new FileInputStream(bundle);
-		Reader reader = new ReaderFactoryImpl().createReader(in);
-		BundleReader r = new BundleReaderImpl(reader, bundle.length(),
-				messageParser, headerBuilderProvider, batchBuilderProvider);
+		BundleReader r = new BundleReaderImpl(in, rf, keyPair.getPublic(), sig,
+				dig, messageParser, new HeaderFactoryImpl(),
+				new BatchFactoryImpl());
 
 		Header h = r.getHeader();
 		assertEquals(acks, h.getAcks());
@@ -211,7 +169,7 @@ public class BundleReadWriteTest extends TestCase {
 			r.getNextBatch();
 			assertTrue(false);
 		} catch(GeneralSecurityException expected) {}
-		r.close();
+		r.finish();
 	}
 
 	@After
diff --git a/test/net/sf/briar/protocol/BundleReaderTest.java b/test/net/sf/briar/protocol/BundleReaderTest.java
deleted file mode 100644
index 1923416f4b0095be88ac66b0b3740c012c9aaedd..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/protocol/BundleReaderTest.java
+++ /dev/null
@@ -1,266 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.IOException;
-import java.security.SignatureException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import junit.framework.TestCase;
-import net.sf.briar.TestUtils;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchBuilder;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.BundleReader;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.HeaderBuilder;
-import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.protocol.MessageParser;
-import net.sf.briar.api.serial.Raw;
-import net.sf.briar.api.serial.Reader;
-
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-import com.google.inject.Provider;
-
-public class BundleReaderTest extends TestCase {
-
-	private final long size = 1024L * 1024L;
-	private final BatchId ack = new BatchId(TestUtils.getRandomId());
-	private final List<Raw> rawAcks =
-		Collections.<Raw>singletonList(new TestRaw(ack.getBytes()));
-	private final Set<BatchId> acks = Collections.singleton(ack);
-	private final GroupId sub = new GroupId(TestUtils.getRandomId());
-	private final List<Raw> rawSubs =
-		Collections.<Raw>singletonList(new TestRaw(sub.getBytes()));
-	private final Set<GroupId> subs = Collections.singleton(sub);
-	private final Map<String, String> transports =
-		Collections.singletonMap("foo", "bar");
-	private final byte[] headerSig = TestUtils.getRandomId();
-	private final byte[] messageBody = new byte[123];
-	private final List<Raw> rawMessages =
-		Collections.<Raw>singletonList(new TestRaw(messageBody));
-	private final byte[] batchSig = TestUtils.getRandomId();
-
-	@Test
-	public void testGetHeader() throws Exception {
-		Mockery context = new Mockery();
-		final Reader reader = context.mock(Reader.class);
-		final MessageParser messageParser = context.mock(MessageParser.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
-		final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class);
-		final Header header = context.mock(Header.class);
-		context.checking(new Expectations() {{
-			oneOf(reader).setReadLimit(Header.MAX_SIZE);
-			oneOf(headerBuilderProvider).get();
-			will(returnValue(headerBuilder));
-			// Acks
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawAcks));
-			oneOf(headerBuilder).addAcks(acks);
-			// Subs
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawSubs));
-			oneOf(headerBuilder).addSubscriptions(subs);
-			// Transports
-			oneOf(reader).readMap(String.class, String.class);
-			will(returnValue(transports));
-			oneOf(headerBuilder).addTransports(transports);
-			// Signature
-			oneOf(reader).readRaw();
-			will(returnValue(headerSig));
-			oneOf(headerBuilder).setSignature(headerSig);
-			// Build the header
-			oneOf(headerBuilder).build();
-			will(returnValue(header));
-		}});
-		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
-				headerBuilderProvider, batchBuilderProvider);
-
-		assertEquals(header, r.getHeader());
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testBatchBeforeHeaderThrowsException() throws Exception {
-		Mockery context = new Mockery();
-		final Reader reader = context.mock(Reader.class);
-		final MessageParser messageParser = context.mock(MessageParser.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
-		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
-				headerBuilderProvider, batchBuilderProvider);
-
-		try {
-			r.getNextBatch();
-			assertTrue(false);
-		} catch(IllegalStateException expected) {}
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testCloseBeforeHeaderDoesNotThrowException() throws IOException,
-	SignatureException {
-		Mockery context = new Mockery();
-		final Reader reader = context.mock(Reader.class);
-		final MessageParser messageParser = context.mock(MessageParser.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
-		context.checking(new Expectations() {{
-			oneOf(reader).close();
-		}});
-		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
-				headerBuilderProvider, batchBuilderProvider);
-
-		r.close();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testGetHeaderNoBatches() throws Exception {
-		Mockery context = new Mockery();
-		final Reader reader = context.mock(Reader.class);
-		final MessageParser messageParser = context.mock(MessageParser.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
-		final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class);
-		final Header header = context.mock(Header.class);
-		context.checking(new Expectations() {{
-			oneOf(reader).setReadLimit(Header.MAX_SIZE);
-			oneOf(headerBuilderProvider).get();
-			will(returnValue(headerBuilder));
-			// Acks
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawAcks));
-			oneOf(headerBuilder).addAcks(acks);
-			// Subs
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawSubs));
-			oneOf(headerBuilder).addSubscriptions(subs);
-			// Transports
-			oneOf(reader).readMap(String.class, String.class);
-			will(returnValue(transports));
-			oneOf(headerBuilder).addTransports(transports);
-			// Signature
-			oneOf(reader).readRaw();
-			will(returnValue(headerSig));
-			oneOf(headerBuilder).setSignature(headerSig);
-			// Build the header
-			oneOf(headerBuilder).build();
-			will(returnValue(header));
-			// No batches
-			oneOf(reader).readListStart();
-			oneOf(reader).hasListEnd();
-			will(returnValue(true));
-			oneOf(reader).readListEnd();
-			// Close
-			oneOf(reader).close();
-		}});
-		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
-				headerBuilderProvider, batchBuilderProvider);
-
-		assertEquals(header, r.getHeader());
-		assertNull(r.getNextBatch());
-		r.close();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testGetHeaderOneBatch() throws Exception {
-		Mockery context = new Mockery();
-		final Reader reader = context.mock(Reader.class);
-		final MessageParser messageParser = context.mock(MessageParser.class);
-		@SuppressWarnings("unchecked")
-		final Provider<HeaderBuilder> headerBuilderProvider =
-			context.mock(Provider.class);
-		@SuppressWarnings("unchecked")
-		final Provider<BatchBuilder> batchBuilderProvider =
-			context.mock(Provider.class, "batchBuilderProvider");
-		final HeaderBuilder headerBuilder = context.mock(HeaderBuilder.class);
-		final Header header = context.mock(Header.class);
-		final BatchBuilder batchBuilder = context.mock(BatchBuilder.class);
-		final Batch batch = context.mock(Batch.class);
-		final Message message = context.mock(Message.class);
-		context.checking(new Expectations() {{
-			oneOf(reader).setReadLimit(Header.MAX_SIZE);
-			oneOf(headerBuilderProvider).get();
-			will(returnValue(headerBuilder));
-			// Acks
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawAcks));
-			oneOf(headerBuilder).addAcks(acks);
-			// Subs
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawSubs));
-			oneOf(headerBuilder).addSubscriptions(subs);
-			// Transports
-			oneOf(reader).readMap(String.class, String.class);
-			will(returnValue(transports));
-			oneOf(headerBuilder).addTransports(transports);
-			// Signature
-			oneOf(reader).readRaw();
-			will(returnValue(headerSig));
-			oneOf(headerBuilder).setSignature(headerSig);
-			// Build the header
-			oneOf(headerBuilder).build();
-			will(returnValue(header));
-			// First batch
-			oneOf(reader).readListStart();
-			oneOf(reader).hasListEnd();
-			will(returnValue(false));
-			oneOf(reader).setReadLimit(Batch.MAX_SIZE);
-			oneOf(batchBuilderProvider).get();
-			will(returnValue(batchBuilder));
-			oneOf(reader).readList(Raw.class);
-			will(returnValue(rawMessages));
-			oneOf(messageParser).parseMessage(messageBody);
-			will(returnValue(message));
-			oneOf(batchBuilder).addMessage(message);
-			oneOf(reader).readRaw();
-			will(returnValue(batchSig));
-			oneOf(batchBuilder).setSignature(batchSig);
-			oneOf(batchBuilder).build();
-			will(returnValue(batch));
-			// No more batches
-			oneOf(reader).hasListEnd();
-			will(returnValue(true));
-			oneOf(reader).readListEnd();
-			// Close
-			oneOf(reader).close();
-		}});
-		BundleReader r = new BundleReaderImpl(reader, size, messageParser,
-				headerBuilderProvider, batchBuilderProvider);
-
-		assertEquals(header, r.getHeader());
-		assertEquals(batch, r.getNextBatch());
-		assertNull(r.getNextBatch());
-		r.close();
-
-		context.assertIsSatisfied();
-	}
-}
diff --git a/test/net/sf/briar/protocol/BundleWriterTest.java b/test/net/sf/briar/protocol/BundleWriterTest.java
deleted file mode 100644
index 2ba6fedd38cecfaabee90ab9d9cf5aa2313af366..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/protocol/BundleWriterTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-
-import junit.framework.TestCase;
-import net.sf.briar.TestUtils;
-import net.sf.briar.api.protocol.Batch;
-import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.BundleWriter;
-import net.sf.briar.api.protocol.GroupId;
-import net.sf.briar.api.protocol.Header;
-import net.sf.briar.api.protocol.Message;
-import net.sf.briar.api.serial.Writer;
-
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-public class BundleWriterTest extends TestCase {
-
-	private final long capacity = 1024L * 1024L;
-	private final BatchId ack = new BatchId(TestUtils.getRandomId());
-	private final Set<BatchId> acks = Collections.singleton(ack);
-	private final GroupId sub = new GroupId(TestUtils.getRandomId());
-	private final Set<GroupId> subs = Collections.singleton(sub);
-	private final Map<String, String> transports =
-		Collections.singletonMap("foo", "bar");
-	private final byte[] headerSig = TestUtils.getRandomId();
-	private final byte[] messageBody = new byte[123];
-	private final byte[] batchSig = TestUtils.getRandomId();
-
-	@Test
-	public void testAddHeader() throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		final Header header = context.mock(Header.class);
-		context.checking(new Expectations() {{
-			// Acks
-			oneOf(writer).writeListStart();
-			oneOf(header).getAcks();
-			will(returnValue(acks));
-			oneOf(writer).writeRaw(ack);
-			oneOf(writer).writeListEnd();
-			// Subs
-			oneOf(writer).writeListStart();
-			oneOf(header).getSubscriptions();
-			will(returnValue(subs));
-			oneOf(writer).writeRaw(sub);
-			oneOf(writer).writeListEnd();
-			// Transports
-			oneOf(header).getTransports();
-			will(returnValue(transports));
-			oneOf(writer).writeMap(transports);
-			// Signature
-			oneOf(header).getSignature();
-			will(returnValue(headerSig));
-			oneOf(writer).writeRaw(headerSig);
-		}});
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		w.addHeader(header);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testAddHeaderEmptyLists() throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		final Header header = context.mock(Header.class);
-		context.checking(new Expectations() {{
-			// Acks
-			oneOf(writer).writeListStart();
-			oneOf(header).getAcks();
-			will(returnValue(Collections.emptySet()));
-			oneOf(writer).writeListEnd();
-			// Subs
-			oneOf(writer).writeListStart();
-			oneOf(header).getSubscriptions();
-			will(returnValue(Collections.emptySet()));
-			oneOf(writer).writeListEnd();
-			// Transports
-			oneOf(header).getTransports();
-			will(returnValue(Collections.emptyMap()));
-			oneOf(writer).writeMap(Collections.emptyMap());
-			// Signature
-			oneOf(header).getSignature();
-			will(returnValue(headerSig));
-			oneOf(writer).writeRaw(headerSig);
-		}});
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		w.addHeader(header);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testBatchBeforeHeaderThrowsException() throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		final Batch batch = context.mock(Batch.class);
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		try {
-			w.addBatch(batch);
-			assertTrue(false);
-		} catch(IllegalStateException expected) {}
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testCloseBeforeHeaderThrowsException() throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		try {
-			w.close();
-			assertTrue(false);
-		} catch(IllegalStateException expected) {}
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testCloseWithoutBatchesDoesNotThrowException()
-	throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		final Header header = context.mock(Header.class);
-		context.checking(new Expectations() {{
-			// Acks
-			oneOf(writer).writeListStart();
-			oneOf(header).getAcks();
-			will(returnValue(acks));
-			oneOf(writer).writeRaw(ack);
-			oneOf(writer).writeListEnd();
-			// Subs
-			oneOf(writer).writeListStart();
-			oneOf(header).getSubscriptions();
-			will(returnValue(subs));
-			oneOf(writer).writeRaw(sub);
-			oneOf(writer).writeListEnd();
-			// Transports
-			oneOf(header).getTransports();
-			will(returnValue(transports));
-			oneOf(writer).writeMap(transports);
-			// Signature
-			oneOf(header).getSignature();
-			will(returnValue(headerSig));
-			oneOf(writer).writeRaw(headerSig);
-			// Close - write an empty list of batches
-			oneOf(writer).writeListStart();
-			oneOf(writer).writeListEnd();
-			oneOf(writer).close();
-		}});
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		w.addHeader(header);
-		w.close();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testAddHeaderAndTwoBatches() throws IOException {
-		Mockery context = new Mockery();
-		final Writer writer = context.mock(Writer.class);
-		final Header header = context.mock(Header.class);
-		final Batch batch = context.mock(Batch.class);
-		final Message message = context.mock(Message.class);
-		context.checking(new Expectations() {{
-			// Acks
-			oneOf(writer).writeListStart();
-			oneOf(header).getAcks();
-			will(returnValue(acks));
-			oneOf(writer).writeRaw(ack);
-			oneOf(writer).writeListEnd();
-			// Subs
-			oneOf(writer).writeListStart();
-			oneOf(header).getSubscriptions();
-			will(returnValue(subs));
-			oneOf(writer).writeRaw(sub);
-			oneOf(writer).writeListEnd();
-			// Transports
-			oneOf(header).getTransports();
-			will(returnValue(transports));
-			oneOf(writer).writeMap(transports);
-			// Signature
-			oneOf(header).getSignature();
-			will(returnValue(headerSig));
-			oneOf(writer).writeRaw(headerSig);
-			// First batch
-			oneOf(writer).writeListStart();
-			oneOf(writer).writeListStart();
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singleton(message)));
-			oneOf(message).getBytes();
-			will(returnValue(messageBody));
-			oneOf(writer).writeRaw(messageBody);
-			oneOf(writer).writeListEnd();
-			oneOf(batch).getSignature();
-			will(returnValue(batchSig));
-			oneOf(writer).writeRaw(batchSig);
-			// Second batch
-			oneOf(writer).writeListStart();
-			oneOf(batch).getMessages();
-			will(returnValue(Collections.singleton(message)));
-			oneOf(message).getBytes();
-			will(returnValue(messageBody));
-			oneOf(writer).writeRaw(messageBody);
-			oneOf(writer).writeListEnd();
-			oneOf(batch).getSignature();
-			will(returnValue(batchSig));
-			oneOf(writer).writeRaw(batchSig);
-			// Close
-			oneOf(writer).writeListEnd();
-			oneOf(writer).close();
-		}});
-		BundleWriter w = new BundleWriterImpl(writer, capacity);
-
-		w.addHeader(header);
-		w.addBatch(batch);
-		w.addBatch(batch);
-		w.close();
-
-		context.assertIsSatisfied();
-	}
-}
diff --git a/test/net/sf/briar/protocol/SigningStreamTest.java b/test/net/sf/briar/protocol/SigningStreamTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1b68e23ae2087e314f844966d590fe1195334a5
--- /dev/null
+++ b/test/net/sf/briar/protocol/SigningStreamTest.java
@@ -0,0 +1,158 @@
+package net.sf.briar.protocol;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.security.DigestOutputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.util.Arrays;
+import java.util.Random;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+public class SigningStreamTest extends TestCase {
+
+	private static final String SIGNATURE_ALGO = "SHA256withRSA";
+	private static final String KEY_PAIR_ALGO = "RSA";
+	private static final String DIGEST_ALGO = "SHA-256";
+
+	private final KeyPair keyPair;
+	private final Signature sig;
+	private final MessageDigest dig;
+	private final Random random;
+
+	public SigningStreamTest() throws Exception {
+		super();
+		keyPair = KeyPairGenerator.getInstance(KEY_PAIR_ALGO).generateKeyPair();
+		sig = Signature.getInstance(SIGNATURE_ALGO);
+		dig = MessageDigest.getInstance(DIGEST_ALGO);
+		random = new Random();
+	}
+
+	@Test
+	public void testOutputStreamOutputMatchesInput() throws Exception {
+		byte[] input = new byte[1000];
+		random.nextBytes(input);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		SigningOutputStream signOut = new SigningOutputStream(out, sig);
+		sig.initSign(keyPair.getPrivate());
+
+		signOut.setSigning(true);
+		signOut.write(input, 0, 500);
+		signOut.setSigning(false);
+		signOut.write(input, 500, 250);
+		signOut.setSigning(true);
+		signOut.write(input, 750, 250);
+
+		byte[] output = out.toByteArray();
+		assertTrue(Arrays.equals(input, output));
+	}
+
+	@Test
+	public void testInputStreamOutputMatchesInput() throws Exception {
+		byte[] input = new byte[1000];
+		random.nextBytes(input);
+
+		ByteArrayInputStream in = new ByteArrayInputStream(input);
+		SigningDigestingInputStream signIn =
+			new SigningDigestingInputStream(in, sig, dig);
+		sig.initVerify(keyPair.getPublic());
+
+		byte[] output = new byte[1000];
+		signIn.setSigning(true);
+		assertEquals(500, signIn.read(output, 0, 500));
+		signIn.setSigning(false);
+		assertEquals(250, signIn.read(output, 500, 250));
+		signIn.setSigning(true);
+		assertEquals(250, signIn.read(output, 750, 250));
+
+		assertTrue(Arrays.equals(input, output));
+	}
+
+	@Test
+	public void testVerificationLagsByOneByte() throws Exception {
+		byte[] input = new byte[1000];
+		random.nextBytes(input);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		SigningOutputStream signOut = new SigningOutputStream(out, sig);
+		sig.initSign(keyPair.getPrivate());
+
+		// Sign bytes 0-499, skip bytes 500-749, sign bytes 750-999
+		signOut.setSigning(true);
+		signOut.write(input, 0, 500);
+		signOut.setSigning(false);
+		signOut.write(input, 500, 250);
+		signOut.setSigning(true);
+		signOut.write(input, 750, 250);
+
+		byte[] signature = sig.sign();
+
+		ByteArrayInputStream in = new ByteArrayInputStream(input);
+		SigningDigestingInputStream signIn =
+			new SigningDigestingInputStream(in, sig, dig);
+		sig.initVerify(keyPair.getPublic());
+
+		byte[] output = new byte[1000];
+		// Consume a lookahead byte
+		assertEquals(1, signIn.read(output, 0, 1));
+		// All the offsets are increased by 1 because of the lookahead byte
+		signIn.setSigning(true);
+		assertEquals(500, signIn.read(output, 1, 500));
+		signIn.setSigning(false);
+		assertEquals(250, signIn.read(output, 501, 250));
+		signIn.setSigning(true);
+		assertEquals(249, signIn.read(output, 751, 249));
+		// Have to reach EOF for the lookahead byte to be processed
+		assertEquals(-1, signIn.read());
+
+		assertTrue(Arrays.equals(input, output));
+		assertTrue(sig.verify(signature));
+	}
+
+	@Test
+	public void testDigestionLagsByOneByte() throws Exception {
+		byte[] input = new byte[1000];
+		random.nextBytes(input);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		DigestOutputStream digOut = new DigestOutputStream(out, dig);
+		dig.reset();
+
+		// Digest bytes 0-499, skip bytes 500-749, digest bytes 750-999
+		digOut.on(true);
+		digOut.write(input, 0, 500);
+		digOut.on(false);
+		digOut.write(input, 500, 250);
+		digOut.on(true);
+		digOut.write(input, 750, 250);
+
+		byte[] hash = dig.digest();
+
+		ByteArrayInputStream in = new ByteArrayInputStream(input);
+		SigningDigestingInputStream signIn =
+			new SigningDigestingInputStream(in, sig, dig);
+		dig.reset();
+
+		byte[] output = new byte[1000];
+		// Consume a lookahead byte
+		assertEquals(1, signIn.read(output, 0, 1));
+		// All the offsets are increased by 1 because of the lookahead byte
+		signIn.setDigesting(true);
+		assertEquals(500, signIn.read(output, 1, 500));
+		signIn.setDigesting(false);
+		assertEquals(250, signIn.read(output, 501, 250));
+		signIn.setDigesting(true);
+		assertEquals(249, signIn.read(output, 751, 249));
+		// Have to reach EOF for the lookahead byte to be processed
+		assertEquals(-1, signIn.read());
+
+		assertTrue(Arrays.equals(input, output));
+		assertTrue(Arrays.equals(hash, dig.digest()));
+	}
+}
\ No newline at end of file
diff --git a/test/net/sf/briar/protocol/TestRaw.java b/test/net/sf/briar/protocol/TestRaw.java
deleted file mode 100644
index f9f685b3632bfaca3244040a4c2c7d05cdcbefa3..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/protocol/TestRaw.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.sf.briar.protocol;
-
-import java.util.Arrays;
-
-import net.sf.briar.api.serial.Raw;
-
-class TestRaw implements Raw {
-
-	private final byte[] bytes;
-
-	TestRaw(byte[] bytes) {
-		this.bytes = bytes;
-	}
-
-	public byte[] getBytes() {
-		return bytes;
-	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(bytes);
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if(o instanceof Raw) return Arrays.equals(bytes, ((Raw) o).getBytes());
-		return false;
-	}
-}