diff --git a/briar-core/src/net/sf/briar/protocol/AckReader.java b/briar-core/src/net/sf/briar/protocol/AckReader.java
deleted file mode 100644
index beb073633d0a81fa2bb650d8be51ab0896204b0c..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/AckReader.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-import static net.sf.briar.api.protocol.Types.ACK;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.sf.briar.api.Bytes;
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class AckReader implements StructReader<Ack> {
-
-	public Ack readStruct(Reader r) throws IOException {
-		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
-		r.addConsumer(counting);
-		r.readStructId(ACK);
-		// Read the message IDs as byte arrays
-		r.setMaxBytesLength(UniqueId.LENGTH);
-		List<Bytes> raw = r.readList(Bytes.class);
-		r.resetMaxBytesLength();
-		r.removeConsumer(counting);
-		if(raw.isEmpty()) throw new FormatException();
-		// Convert the byte arrays to message IDs
-		List<MessageId> acked = new ArrayList<MessageId>();
-		for(Bytes b : raw) {
-			if(b.getBytes().length != UniqueId.LENGTH)
-				throw new FormatException();
-			acked.add(new MessageId(b.getBytes()));
-		}
-		// Build and return the ack
-		return new Ack(Collections.unmodifiableList(acked));
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/ExpiryAckReader.java b/briar-core/src/net/sf/briar/protocol/ExpiryAckReader.java
deleted file mode 100644
index 48b24de6c0069d4f0861ea1d2407660c0d340542..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/ExpiryAckReader.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.Types.EXPIRY_ACK;
-
-import java.io.IOException;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.ExpiryAck;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class ExpiryAckReader implements StructReader<ExpiryAck> {
-
-	public ExpiryAck readStruct(Reader r) throws IOException {
-		r.readStructId(EXPIRY_ACK);
-		long version = r.readInt64();
-		if(version < 0L) throw new FormatException();
-		return new ExpiryAck(version);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/ExpiryUpdateReader.java b/briar-core/src/net/sf/briar/protocol/ExpiryUpdateReader.java
deleted file mode 100644
index 7b8fe7b761195277e4923077ed51fc3c21f37752..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/ExpiryUpdateReader.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.Types.EXPIRY_UPDATE;
-
-import java.io.IOException;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.ExpiryUpdate;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class ExpiryUpdateReader implements StructReader<ExpiryUpdate> {
-
-	public ExpiryUpdate readStruct(Reader r) throws IOException {
-		r.readStructId(EXPIRY_UPDATE);
-		long expiry = r.readInt64();
-		if(expiry < 0L) throw new FormatException();
-		long version = r.readInt64();
-		if(version < 0L) throw new FormatException();
-		return new ExpiryUpdate(expiry, version);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/OfferReader.java b/briar-core/src/net/sf/briar/protocol/OfferReader.java
deleted file mode 100644
index 62a8d665a83310cfaee15d5f17f0c649f5155073..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/OfferReader.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-import static net.sf.briar.api.protocol.Types.OFFER;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.sf.briar.api.Bytes;
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.Offer;
-import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class OfferReader implements StructReader<Offer> {
-
-	public Offer readStruct(Reader r) throws IOException {
-		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
-		r.addConsumer(counting);
-		r.readStructId(OFFER);
-		// Read the message IDs as byte arrays
-		r.setMaxBytesLength(UniqueId.LENGTH);
-		List<Bytes> raw = r.readList(Bytes.class);
-		r.resetMaxBytesLength();
-		r.removeConsumer(counting);
-		if(raw.isEmpty()) throw new FormatException();
-		// Convert the byte arrays to message IDs
-		List<MessageId> messages = new ArrayList<MessageId>();
-		for(Bytes b : raw) {
-			if(b.getBytes().length != UniqueId.LENGTH)
-				throw new FormatException();
-			messages.add(new MessageId(b.getBytes()));
-		}
-		// Build and return the offer
-		return new Offer(Collections.unmodifiableList(messages));
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolModule.java b/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
index e30884ac8f2bdbcc0f8f460fda2683ecb58db1a7..9d32b9ef19be0ae010917e0c2da501e4002da30e 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolModule.java
@@ -3,23 +3,15 @@ package net.sf.briar.protocol;
 import java.util.concurrent.Executor;
 
 import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
 import net.sf.briar.api.protocol.AuthorFactory;
-import net.sf.briar.api.protocol.ExpiryAck;
-import net.sf.briar.api.protocol.ExpiryUpdate;
 import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.MessageFactory;
 import net.sf.briar.api.protocol.MessageVerifier;
-import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.Request;
-import net.sf.briar.api.protocol.SubscriptionAck;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
-import net.sf.briar.api.protocol.TransportAck;
-import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.serial.StructReader;
@@ -70,21 +62,6 @@ public class ProtocolModule extends AbstractModule {
 		return new GroupReader(crypto);
 	}
 
-	@Provides
-	StructReader<Ack> getAckReader() {
-		return new AckReader();
-	}
-
-	@Provides
-	StructReader<ExpiryAck> getExpiryAckReader() {
-		return new ExpiryAckReader();
-	}
-
-	@Provides
-	StructReader<ExpiryUpdate> getExpiryUpdateReader() {
-		return new ExpiryUpdateReader();
-	}
-
 	@Provides
 	StructReader<UnverifiedMessage> getMessageReader(
 			StructReader<Group> groupReader,
@@ -92,34 +69,9 @@ public class ProtocolModule extends AbstractModule {
 		return new MessageReader(groupReader, authorReader);
 	}
 
-	@Provides
-	StructReader<Offer> getOfferReader() {
-		return new OfferReader();
-	}
-
-	@Provides
-	StructReader<Request> getRequestReader() {
-		return new RequestReader();
-	}
-
-	@Provides
-	StructReader<SubscriptionAck> getSubscriptionAckReader() {
-		return new SubscriptionAckReader();
-	}
-
 	@Provides
 	StructReader<SubscriptionUpdate> getSubscriptionUpdateReader(
 			StructReader<Group> groupReader) {
 		return new SubscriptionUpdateReader(groupReader);
 	}
-
-	@Provides
-	StructReader<TransportAck> getTransportAckReader() {
-		return new TransportAckReader();
-	}
-
-	@Provides
-	StructReader<TransportUpdate> getTransportUpdateReader() {
-		return new TransportUpdateReader();
-	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java b/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
index dbfb622ac873958428a8ce0d2128a3fe789d9aa1..009b26a9fd81f68a4debb9de6e04cdcf3f8757b8 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolReaderFactoryImpl.java
@@ -2,17 +2,9 @@ package net.sf.briar.protocol;
 
 import java.io.InputStream;
 
-import net.sf.briar.api.protocol.Ack;
-import net.sf.briar.api.protocol.ExpiryAck;
-import net.sf.briar.api.protocol.ExpiryUpdate;
-import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
-import net.sf.briar.api.protocol.Request;
-import net.sf.briar.api.protocol.SubscriptionAck;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
-import net.sf.briar.api.protocol.TransportAck;
-import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.UnverifiedMessage;
 import net.sf.briar.api.serial.ReaderFactory;
 import net.sf.briar.api.serial.StructReader;
@@ -20,52 +12,24 @@ import net.sf.briar.api.serial.StructReader;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-// FIXME: Refactor this package to reduce boilerplate
+// FIXME: See whether these providers can be got rid of
 class ProtocolReaderFactoryImpl implements ProtocolReaderFactory {
 
 	private final ReaderFactory readerFactory;
-	private final Provider<StructReader<Ack>> ackProvider;
-	private final Provider<StructReader<ExpiryAck>> expiryAckProvider;
-	private final Provider<StructReader<ExpiryUpdate>> expiryUpdateProvider;
 	private final Provider<StructReader<UnverifiedMessage>> messageProvider;
-	private final Provider<StructReader<Offer>> offerProvider;
-	private final Provider<StructReader<Request>> requestProvider;
-	private final Provider<StructReader<SubscriptionAck>> subscriptionAckProvider;
 	private final Provider<StructReader<SubscriptionUpdate>> subscriptionUpdateProvider;
-	private final Provider<StructReader<TransportAck>> transportAckProvider;
-	private final Provider<StructReader<TransportUpdate>> transportUpdateProvider;
 
 	@Inject
 	ProtocolReaderFactoryImpl(ReaderFactory readerFactory,
-			Provider<StructReader<Ack>> ackProvider,
 			Provider<StructReader<UnverifiedMessage>> messageProvider,
-			Provider<StructReader<ExpiryAck>> expiryAckProvider,
-			Provider<StructReader<ExpiryUpdate>> expiryUpdateProvider,
-			Provider<StructReader<Offer>> offerProvider,
-			Provider<StructReader<Request>> requestProvider,
-			Provider<StructReader<SubscriptionAck>> subscriptionAckProvider,
-			Provider<StructReader<SubscriptionUpdate>> subscriptionUpdateProvider,
-			Provider<StructReader<TransportAck>> transportAckProvider,
-			Provider<StructReader<TransportUpdate>> transportUpdateProvider) {
+			Provider<StructReader<SubscriptionUpdate>> subscriptionUpdateProvider) {
 		this.readerFactory = readerFactory;
-		this.ackProvider = ackProvider;
-		this.expiryAckProvider = expiryAckProvider;
-		this.expiryUpdateProvider = expiryUpdateProvider;
 		this.messageProvider = messageProvider;
-		this.offerProvider = offerProvider;
-		this.requestProvider = requestProvider;
-		this.subscriptionAckProvider = subscriptionAckProvider;
 		this.subscriptionUpdateProvider = subscriptionUpdateProvider;
-		this.transportAckProvider = transportAckProvider;
-		this.transportUpdateProvider = transportUpdateProvider;
 	}
 
 	public ProtocolReader createProtocolReader(InputStream in) {
-		return new ProtocolReaderImpl(in, readerFactory, ackProvider.get(),
-				expiryAckProvider.get(), expiryUpdateProvider.get(),
-				messageProvider.get(), offerProvider.get(),
-				requestProvider.get(), subscriptionAckProvider.get(),
-				subscriptionUpdateProvider.get(), transportAckProvider.get(),
-				transportUpdateProvider.get());
+		return new ProtocolReaderImpl(readerFactory, messageProvider.get(),
+				subscriptionUpdateProvider.get(), in);
 	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java b/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
index b2dd6287ee7352916927a6db1fd9861c54dff9bb..1f4b5acdaa99f5367df35f50e498600b3a39a9a2 100644
--- a/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
+++ b/briar-core/src/net/sf/briar/protocol/ProtocolReaderImpl.java
@@ -1,5 +1,8 @@
 package net.sf.briar.protocol;
 
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT;
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH;
 import static net.sf.briar.api.protocol.Types.ACK;
 import static net.sf.briar.api.protocol.Types.EXPIRY_ACK;
 import static net.sf.briar.api.protocol.Types.EXPIRY_UPDATE;
@@ -13,132 +16,218 @@ import static net.sf.briar.api.protocol.Types.TRANSPORT_UPDATE;
 
 import java.io.IOException;
 import java.io.InputStream;
-
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.briar.api.Bytes;
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.ExpiryAck;
 import net.sf.briar.api.protocol.ExpiryUpdate;
+import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Offer;
 import net.sf.briar.api.protocol.ProtocolReader;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionAck;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportAck;
+import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.protocol.UnverifiedMessage;
+import net.sf.briar.api.serial.Consumer;
+import net.sf.briar.api.serial.CountingConsumer;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
 import net.sf.briar.api.serial.StructReader;
 
+// This class is not thread-safe
 class ProtocolReaderImpl implements ProtocolReader {
 
-	private final Reader reader;
+	private final StructReader<UnverifiedMessage> messageReader;
+	private final StructReader<SubscriptionUpdate> subscriptionUpdateReader;
+	private final Reader r;
 
-	ProtocolReaderImpl(InputStream in, ReaderFactory readerFactory,
-			StructReader<Ack> ackReader,
-			StructReader<ExpiryAck> expiryAckReader,
-			StructReader<ExpiryUpdate> expiryUpdateReader,
+	ProtocolReaderImpl(ReaderFactory readerFactory,
 			StructReader<UnverifiedMessage> messageReader,
-			StructReader<Offer> offerReader,
-			StructReader<Request> requestReader,
-			StructReader<SubscriptionAck> subscriptionAckReader,
 			StructReader<SubscriptionUpdate> subscriptionUpdateReader,
-			StructReader<TransportAck> transportAckReader,
-			StructReader<TransportUpdate> transportUpdateReader) {
-		reader = readerFactory.createReader(in);
-		reader.addStructReader(ACK, ackReader);
-		reader.addStructReader(MESSAGE, messageReader);
-		reader.addStructReader(OFFER, offerReader);
-		reader.addStructReader(REQUEST, requestReader);
-		reader.addStructReader(EXPIRY_ACK, expiryAckReader);
-		reader.addStructReader(EXPIRY_UPDATE, expiryUpdateReader);
-		reader.addStructReader(SUBSCRIPTION_ACK, subscriptionAckReader);
-		reader.addStructReader(SUBSCRIPTION_UPDATE, subscriptionUpdateReader);
-		reader.addStructReader(TRANSPORT_ACK, transportAckReader);
-		reader.addStructReader(TRANSPORT_UPDATE, transportUpdateReader);
+			InputStream in) {
+		this.messageReader = messageReader;
+		this.subscriptionUpdateReader = subscriptionUpdateReader;
+		r = readerFactory.createReader(in);
 	}
 
 	public boolean eof() throws IOException {
-		return reader.eof();
+		return r.eof();
 	}
 
 	public boolean hasAck() throws IOException {
-		return reader.hasStruct(ACK);
+		return r.hasStruct(ACK);
 	}
 
 	public Ack readAck() throws IOException {
-		return reader.readStruct(ACK, Ack.class);
+		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
+		r.addConsumer(counting);
+		r.readStructId(ACK);
+		// Read the message IDs as byte arrays
+		r.setMaxBytesLength(UniqueId.LENGTH);
+		List<Bytes> raw = r.readList(Bytes.class);
+		r.resetMaxBytesLength();
+		r.removeConsumer(counting);
+		if(raw.isEmpty()) throw new FormatException();
+		// Convert the byte arrays to message IDs
+		List<MessageId> acked = new ArrayList<MessageId>();
+		for(Bytes b : raw) {
+			if(b.getBytes().length != UniqueId.LENGTH)
+				throw new FormatException();
+			acked.add(new MessageId(b.getBytes()));
+		}
+		// Build and return the ack
+		return new Ack(Collections.unmodifiableList(acked));
 	}
 
 	public boolean hasExpiryAck() throws IOException {
-		return reader.hasStruct(EXPIRY_ACK);
+		return r.hasStruct(EXPIRY_ACK);
 	}
 
 	public ExpiryAck readExpiryAck() throws IOException {
-		return reader.readStruct(EXPIRY_ACK, ExpiryAck.class);
+		r.readStructId(EXPIRY_ACK);
+		long version = r.readInt64();
+		if(version < 0L) throw new FormatException();
+		return new ExpiryAck(version);
 	}
 
 	public boolean hasExpiryUpdate() throws IOException {
-		return reader.hasStruct(EXPIRY_UPDATE);
+		return r.hasStruct(EXPIRY_UPDATE);
 	}
 
 	public ExpiryUpdate readExpiryUpdate() throws IOException {
-		return reader.readStruct(EXPIRY_UPDATE, ExpiryUpdate.class);
+		r.readStructId(EXPIRY_UPDATE);
+		long expiry = r.readInt64();
+		if(expiry < 0L) throw new FormatException();
+		long version = r.readInt64();
+		if(version < 0L) throw new FormatException();
+		return new ExpiryUpdate(expiry, version);
 	}
 
 	public boolean hasMessage() throws IOException {
-		return reader.hasStruct(MESSAGE);
+		return r.hasStruct(MESSAGE);
 	}
 
 	public UnverifiedMessage readMessage() throws IOException {
-		return reader.readStruct(MESSAGE, UnverifiedMessage.class);
+		return messageReader.readStruct(r);
 	}
 
 	public boolean hasOffer() throws IOException {
-		return reader.hasStruct(OFFER);
+		return r.hasStruct(OFFER);
 	}
 
 	public Offer readOffer() throws IOException {
-		return reader.readStruct(OFFER, Offer.class);
+		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
+		r.addConsumer(counting);
+		r.readStructId(OFFER);
+		// Read the message IDs as byte arrays
+		r.setMaxBytesLength(UniqueId.LENGTH);
+		List<Bytes> raw = r.readList(Bytes.class);
+		r.resetMaxBytesLength();
+		r.removeConsumer(counting);
+		if(raw.isEmpty()) throw new FormatException();
+		// Convert the byte arrays to message IDs
+		List<MessageId> messages = new ArrayList<MessageId>();
+		for(Bytes b : raw) {
+			if(b.getBytes().length != UniqueId.LENGTH)
+				throw new FormatException();
+			messages.add(new MessageId(b.getBytes()));
+		}
+		// Build and return the offer
+		return new Offer(Collections.unmodifiableList(messages));
 	}
 
 	public boolean hasRequest() throws IOException {
-		return reader.hasStruct(REQUEST);
+		return r.hasStruct(REQUEST);
 	}
 
 	public Request readRequest() throws IOException {
-		return reader.readStruct(REQUEST, Request.class);
+		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
+		r.addConsumer(counting);
+		r.readStructId(REQUEST);
+		// There may be up to 7 bits of padding at the end of the bitmap
+		int padding = r.readUint7();
+		if(padding > 7) throw new FormatException();
+		// Read the bitmap
+		byte[] bitmap = r.readBytes(MAX_PACKET_LENGTH);
+		r.removeConsumer(counting);
+		// Convert the bitmap into a BitSet
+		int length = bitmap.length * 8 - padding;
+		BitSet b = new BitSet(length);
+		for(int i = 0; i < bitmap.length; i++) {
+			for(int j = 0; j < 8 && i * 8 + j < length; j++) {
+				byte bit = (byte) (128 >> j);
+				if((bitmap[i] & bit) != 0) b.set(i * 8 + j);
+			}
+		}
+		return new Request(b, length);
 	}
 
 	public boolean hasSubscriptionAck() throws IOException {
-		return reader.hasStruct(SUBSCRIPTION_ACK);
+		return r.hasStruct(SUBSCRIPTION_ACK);
 	}
 
 	public SubscriptionAck readSubscriptionAck() throws IOException {
-		return reader.readStruct(SUBSCRIPTION_ACK, SubscriptionAck.class);
+		r.readStructId(SUBSCRIPTION_ACK);
+		long version = r.readInt64();
+		if(version < 0L) throw new FormatException();
+		return new SubscriptionAck(version);
 	}
 
 	public boolean hasSubscriptionUpdate() throws IOException {
-		return reader.hasStruct(SUBSCRIPTION_UPDATE);
+		return r.hasStruct(SUBSCRIPTION_UPDATE);
 	}
 
 	public SubscriptionUpdate readSubscriptionUpdate() throws IOException {
-		return reader.readStruct(SUBSCRIPTION_UPDATE,
-				SubscriptionUpdate.class);
+		return subscriptionUpdateReader.readStruct(r);
 	}
 
 	public boolean hasTransportAck() throws IOException {
-		return reader.hasStruct(TRANSPORT_ACK);
+		return r.hasStruct(TRANSPORT_ACK);
 	}
 
 	public TransportAck readTransportAck() throws IOException {
-		return reader.readStruct(TRANSPORT_ACK, TransportAck.class);
+		r.readStructId(TRANSPORT_ACK);
+		byte[] b = r.readBytes(UniqueId.LENGTH);
+		if(b.length < UniqueId.LENGTH) throw new FormatException();
+		long version = r.readInt64();
+		if(version < 0L) throw new FormatException();
+		return new TransportAck(new TransportId(b), version);
 	}
 
 	public boolean hasTransportUpdate() throws IOException {
-		return reader.hasStruct(TRANSPORT_UPDATE);
+		return r.hasStruct(TRANSPORT_UPDATE);
 	}
 
 	public TransportUpdate readTransportUpdate() throws IOException {
-		return reader.readStruct(TRANSPORT_UPDATE, TransportUpdate.class);
+		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
+		r.addConsumer(counting);
+		r.readStructId(TRANSPORT_UPDATE);
+		// Read the transport ID
+		byte[] b = r.readBytes(UniqueId.LENGTH);
+		if(b.length < UniqueId.LENGTH) throw new FormatException();
+		TransportId id = new TransportId(b);
+		// Read the transport properties
+		r.setMaxStringLength(MAX_PROPERTY_LENGTH);
+		Map<String, String> m = r.readMap(String.class, String.class);
+		r.resetMaxStringLength();
+		if(m.size() > MAX_PROPERTIES_PER_TRANSPORT)
+			throw new FormatException();
+		// Read the version number
+		long version = r.readInt64();
+		if(version < 0L) throw new FormatException();
+		r.removeConsumer(counting);
+		// Build and return the transport update
+		return new TransportUpdate(id, new TransportProperties(m), version);
 	}
 }
diff --git a/briar-core/src/net/sf/briar/protocol/RequestReader.java b/briar-core/src/net/sf/briar/protocol/RequestReader.java
deleted file mode 100644
index 8bac06ec77fedef112eeff5da94b2ca1d64e0e3d..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/RequestReader.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-import static net.sf.briar.api.protocol.Types.REQUEST;
-
-import java.io.IOException;
-import java.util.BitSet;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.Request;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class RequestReader implements StructReader<Request> {
-
-	public Request readStruct(Reader r) throws IOException {
-		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
-		r.addConsumer(counting);
-		r.readStructId(REQUEST);
-		// There may be up to 7 bits of padding at the end of the bitmap
-		int padding = r.readUint7();
-		if(padding > 7) throw new FormatException();
-		// Read the bitmap
-		byte[] bitmap = r.readBytes(MAX_PACKET_LENGTH);
-		r.removeConsumer(counting);
-		// Convert the bitmap into a BitSet
-		int length = bitmap.length * 8 - padding;
-		BitSet b = new BitSet(length);
-		for(int i = 0; i < bitmap.length; i++) {
-			for(int j = 0; j < 8 && i * 8 + j < length; j++) {
-				byte bit = (byte) (128 >> j);
-				if((bitmap[i] & bit) != 0) b.set(i * 8 + j);
-			}
-		}
-		return new Request(b, length);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/SubscriptionAckReader.java b/briar-core/src/net/sf/briar/protocol/SubscriptionAckReader.java
deleted file mode 100644
index 648fbc7ec1d74ee42080604a85fe73bf1d058ef1..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/SubscriptionAckReader.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.Types.SUBSCRIPTION_ACK;
-
-import java.io.IOException;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.SubscriptionAck;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class SubscriptionAckReader implements StructReader<SubscriptionAck> {
-
-	public SubscriptionAck readStruct(Reader r) throws IOException {
-		r.readStructId(SUBSCRIPTION_ACK);
-		long version = r.readInt64();
-		if(version < 0L) throw new FormatException();
-		return new SubscriptionAck(version);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/TransportAckReader.java b/briar-core/src/net/sf/briar/protocol/TransportAckReader.java
deleted file mode 100644
index 6a7c730d450c2b2610fd710086ad37467627a23a..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/TransportAckReader.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.Types.TRANSPORT_ACK;
-
-import java.io.IOException;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.protocol.TransportAck;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class TransportAckReader implements StructReader<TransportAck> {
-
-	public TransportAck readStruct(Reader r) throws IOException {
-		r.readStructId(TRANSPORT_ACK);
-		byte[] b = r.readBytes(UniqueId.LENGTH);
-		if(b.length < UniqueId.LENGTH) throw new FormatException();
-		long version = r.readInt64();
-		if(version < 0L) throw new FormatException();
-		return new TransportAck(new TransportId(b), version);
-	}
-}
diff --git a/briar-core/src/net/sf/briar/protocol/TransportUpdateReader.java b/briar-core/src/net/sf/briar/protocol/TransportUpdateReader.java
deleted file mode 100644
index ee7c4e5cc655bcd15fa45f43d61de00eab0becea..0000000000000000000000000000000000000000
--- a/briar-core/src/net/sf/briar/protocol/TransportUpdateReader.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.sf.briar.protocol;
-
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT;
-import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH;
-import static net.sf.briar.api.protocol.Types.TRANSPORT_UPDATE;
-
-import java.io.IOException;
-import java.util.Map;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.TransportProperties;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.StructReader;
-
-class TransportUpdateReader implements StructReader<TransportUpdate> {
-
-	public TransportUpdate readStruct(Reader r) throws IOException {
-		Consumer counting = new CountingConsumer(MAX_PACKET_LENGTH);
-		r.addConsumer(counting);
-		r.readStructId(TRANSPORT_UPDATE);
-		// Read the transport ID
-		byte[] b = r.readBytes(UniqueId.LENGTH);
-		if(b.length < UniqueId.LENGTH) throw new FormatException();
-		TransportId id = new TransportId(b);
-		// Read the transport properties
-		r.setMaxStringLength(MAX_PROPERTY_LENGTH);
-		Map<String, String> m = r.readMap(String.class, String.class);
-		r.resetMaxStringLength();
-		if(m.size() > MAX_PROPERTIES_PER_TRANSPORT)
-			throw new FormatException();
-		// Read the version number
-		long version = r.readInt64();
-		if(version < 0L) throw new FormatException();
-		r.removeConsumer(counting);
-		// Build and return the transport update
-		return new TransportUpdate(id, new TransportProperties(m), version);
-	}
-}