diff --git a/components/net/sf/briar/transport/ErasureDecoder.java b/components/net/sf/briar/transport/ErasureDecoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..679c26ab20f19ffa35a17bafdd510488c8485b11
--- /dev/null
+++ b/components/net/sf/briar/transport/ErasureDecoder.java
@@ -0,0 +1,13 @@
+package net.sf.briar.transport;
+
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.transport.Segment;
+
+interface ErasureDecoder {
+
+	/**
+	 * Decodes the given set of segments into the given frame, or returns false
+	 * if the segments cannot be decoded. The segment set may contain nulls.
+	 */
+	public boolean decodeFrame(Frame f, Segment[] set) throws FormatException;
+}
diff --git a/components/net/sf/briar/transport/FrameWindowImpl.java b/components/net/sf/briar/transport/FrameWindowImpl.java
index ddf3e71cf65793dca4c97199dba7bcbfc09afd9f..c3ff67a75a789c42d15fa8528f0f682d7c391c5a 100644
--- a/components/net/sf/briar/transport/FrameWindowImpl.java
+++ b/components/net/sf/briar/transport/FrameWindowImpl.java
@@ -6,6 +6,7 @@ import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 import java.util.Collection;
 import java.util.HashSet;
 
+/** A frame window that allows a limited amount of reordering. */
 class FrameWindowImpl implements FrameWindow {
 
 	private final Collection<Long> window;
diff --git a/components/net/sf/briar/transport/IncomingErrorCorrectionLayerImpl.java b/components/net/sf/briar/transport/IncomingErrorCorrectionLayerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..db3ab691d2713d6470bf11550a6fbf387a182a00
--- /dev/null
+++ b/components/net/sf/briar/transport/IncomingErrorCorrectionLayerImpl.java
@@ -0,0 +1,68 @@
+package net.sf.briar.transport;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.transport.Segment;
+
+class IncomingErrorCorrectionLayerImpl implements IncomingErrorCorrectionLayer {
+
+	private final IncomingEncryptionLayer in;
+	private final ErasureDecoder decoder;
+	private final int n, k;
+	private final Map<Long, Integer> discardCounts;
+	private final Map<Long, Segment[]> segmentSets;
+
+	IncomingErrorCorrectionLayerImpl(IncomingEncryptionLayer in,
+			ErasureDecoder decoder, int n, int k) {
+		this.in = in;
+		this.decoder = decoder;
+		this.n = n;
+		this.k = k;
+		discardCounts = new HashMap<Long, Integer>();
+		segmentSets = new HashMap<Long, Segment[]>();
+	}
+
+	public boolean readFrame(Frame f, FrameWindow window) throws IOException,
+	InvalidDataException {
+		// Free any segment sets that have been removed from the window
+		Iterator<Long> it = segmentSets.keySet().iterator();
+		while(it.hasNext()) if(!window.contains(it.next())) it.remove();
+		// Free any discard counts that are no longer too high for the window
+		Iterator<Long> it1 = discardCounts.keySet().iterator();
+		while(it1.hasNext()) if(!window.isTooHigh(it1.next())) it1.remove();
+		// Allocate a segment
+		Segment s = new SegmentImpl();
+		// Read segments until a frame can be decoded
+		while(true) {
+			// Read segments until a segment in the window is returned
+			long frameNumber;
+			while(true) {
+				if(!in.readSegment(s)) return false;
+				frameNumber = s.getSegmentNumber() / n;
+				if(window.contains(frameNumber)) break;
+				if(window.isTooHigh(frameNumber)) countDiscard(frameNumber);
+			}
+			// Add the segment to its segment set, or create one if necessary
+			Segment[] set = segmentSets.get(frameNumber);
+			if(set == null) {
+				set = new Segment[n];
+				segmentSets.put(frameNumber, set);
+			} else {
+				set[(int) (frameNumber % n)] = s;
+			}
+			// Try to decode the frame
+			if(decoder.decodeFrame(f, set)) return true;
+		}
+	}
+
+	private void countDiscard(long frameNumber) throws FormatException {
+		Integer count = discardCounts.get(frameNumber);
+		if(count == null) discardCounts.put(frameNumber, 1);
+		else if(count == n - k) throw new FormatException();
+		else discardCounts.put(frameNumber, count + 1);
+	}
+}
diff --git a/components/net/sf/briar/transport/NullFrameWindow.java b/components/net/sf/briar/transport/NullFrameWindow.java
index c56a78a37dea818d2c0f01109d39d52c378f4b3f..9f5fb19f7145b800fb871b31b5460472fd5c8621 100644
--- a/components/net/sf/briar/transport/NullFrameWindow.java
+++ b/components/net/sf/briar/transport/NullFrameWindow.java
@@ -2,6 +2,7 @@ package net.sf.briar.transport;
 
 import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
+/** A frame window that does not allow any reordering. */
 class NullFrameWindow implements FrameWindow {
 
 	private long base = 0L;
diff --git a/components/net/sf/briar/transport/NullIncomingErrorCorrectionLayer.java b/components/net/sf/briar/transport/NullIncomingErrorCorrectionLayer.java
index 87ce8d4b358ee0bb4d2dc76b248fc8a4d9e37c69..54e1f04aec8821e127bc7ae4486da58cc6bed18b 100644
--- a/components/net/sf/briar/transport/NullIncomingErrorCorrectionLayer.java
+++ b/components/net/sf/briar/transport/NullIncomingErrorCorrectionLayer.java
@@ -14,12 +14,13 @@ class NullIncomingErrorCorrectionLayer implements IncomingErrorCorrectionLayer {
 		segment = new SegmentImpl();
 	}
 
-	public boolean readFrame(Frame f, FrameWindow window)
-	throws IOException, InvalidDataException {
+	public boolean readFrame(Frame f, FrameWindow window) throws IOException,
+	InvalidDataException {
 		while(true) {
 			if(!in.readSegment(segment)) return false;
 			byte[] buf = segment.getBuffer();
-			if(window.contains(HeaderEncoder.getFrameNumber(buf))) break;
+			long frameNumber = HeaderEncoder.getFrameNumber(buf);
+			if(window.contains(frameNumber)) break;
 		}
 		int length = segment.getLength();
 		// FIXME: Unnecessary copy
diff --git a/components/net/sf/briar/transport/XorErasureDecoder.java b/components/net/sf/briar/transport/XorErasureDecoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..61682185a1673f76f575e50707e6e2f86dd186f5
--- /dev/null
+++ b/components/net/sf/briar/transport/XorErasureDecoder.java
@@ -0,0 +1,60 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.transport.Segment;
+
+/** An erasure decoder that uses k data segments and one parity segment. */
+class XorErasureDecoder implements ErasureDecoder {
+
+	private final int n;
+
+	XorErasureDecoder(int n) {
+		this.n = n;
+	}
+
+	public boolean decodeFrame(Frame f, Segment[] set) throws FormatException {
+		// We need at least n - 1 pieces
+		int pieces = 0;
+		for(int i = 0; i < n; i++) if(set[i] != null) pieces++;
+		if(pieces < n - 1) return false;
+		// All the pieces must have the same length - take the minimum
+		int length = MAX_FRAME_LENGTH;
+		for(int i = 0; i < n; i++) {
+			if(set[i] == null) {
+				int len = set[i].getLength();
+				if(len < length) length = len;
+			}
+		}
+		if(length * (n - 1) > MAX_FRAME_LENGTH) throw new FormatException();
+		// Decode the frame
+		byte[] dest = f.getBuffer();
+		int offset = 0;
+		if(pieces == n || set[n - 1] == null) {
+			// We don't need no stinkin' parity segment
+			for(int i = 0; i < n - 1; i++) {
+				byte[] src = set[i].getBuffer();
+				System.arraycopy(src, 0, dest, offset, length);
+				offset += length;
+			}
+		} else {
+			// Reconstruct the missing segment
+			byte[] parity = new byte[length];
+			int missingOffset = -1;
+			for(int i = 0; i < n; i++) {
+				if(set[i] == null) {
+					missingOffset = offset;
+				} else {
+					byte[] src = set[i].getBuffer();
+					System.arraycopy(src, 0, dest, offset, length);
+					for(int j = 0; j < length; j++) parity[j] ^= src[j];
+				}
+				offset += length;
+			}
+			assert missingOffset != -1;
+			System.arraycopy(parity, 0, dest, missingOffset, length);
+		}
+		f.setLength(offset);
+		return true;
+	}
+}