diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
index 3a76421b375e9c8f9b51ac4fa6976a923cd56582..a052e7393423b772b928f17bf217c9b2b6c50b96 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java
@@ -11,7 +11,6 @@ import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
-import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.contact.ContactManager;
@@ -35,7 +34,7 @@ import static android.app.Activity.RESULT_OK;
 import static android.view.View.GONE;
 import static android.widget.Toast.LENGTH_SHORT;
 import static java.util.logging.Level.WARNING;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
 
 public class IntroductionMessageFragment extends BaseFragment
 		implements TextInputListener {
@@ -175,7 +174,7 @@ public class IntroductionMessageFragment extends BaseFragment
 		ui.message.setSendButtonEnabled(false);
 
 		String msg = ui.message.getText().toString();
-		msg = StringUtils.truncateUtf8(msg, MAX_INTRODUCTION_MESSAGE_LENGTH);
+		msg = StringUtils.truncateUtf8(msg, MAX_REQUEST_MESSAGE_LENGTH);
 		makeIntroduction(contact1, contact2, msg);
 
 		// don't wait for the introduction to be made before finishing activity
@@ -190,7 +189,7 @@ public class IntroductionMessageFragment extends BaseFragment
 			try {
 				long timestamp = System.currentTimeMillis();
 				introductionManager.makeIntroduction(c1, c2, msg, timestamp);
-			} catch (DbException | FormatException e) {
+			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				introductionError();
 			}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java
deleted file mode 100644
index 68881b9dc0d57e5993afb38e35943652141629a5..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.briarproject.briar.api.introduction;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import javax.annotation.Nullable;
-
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-
-@NotNullByDefault
-public enum IntroduceeAction {
-
-	LOCAL_ACCEPT,
-	LOCAL_DECLINE,
-	LOCAL_ABORT,
-	REMOTE_REQUEST,
-	REMOTE_ACCEPT,
-	REMOTE_DECLINE,
-	REMOTE_ABORT,
-	ACK;
-
-	@Nullable
-	public static IntroduceeAction getRemote(int type, boolean accept) {
-		if (type == TYPE_REQUEST) return REMOTE_REQUEST;
-		if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT;
-		if (type == TYPE_RESPONSE) return REMOTE_DECLINE;
-		if (type == TYPE_ACK) return ACK;
-		if (type == TYPE_ABORT) return REMOTE_ABORT;
-		return null;
-	}
-
-	@Nullable
-	public static IntroduceeAction getRemote(int type) {
-		return getRemote(type, true);
-	}
-
-	@Nullable
-	public static IntroduceeAction getLocal(int type, boolean accept) {
-		if (type == TYPE_RESPONSE && accept) return LOCAL_ACCEPT;
-		if (type == TYPE_RESPONSE) return LOCAL_DECLINE;
-		if (type == TYPE_ACK) return ACK;
-		if (type == TYPE_ABORT) return LOCAL_ABORT;
-		return null;
-	}
-
-	@Nullable
-	public static IntroduceeAction getLocal(int type) {
-		return getLocal(type, true);
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java
deleted file mode 100644
index e696181a01715261bd2d58b697ce03ff61907d67..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package org.briarproject.briar.api.introduction;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import javax.annotation.concurrent.Immutable;
-
-import static org.briarproject.briar.api.introduction.IntroduceeAction.ACK;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_DECLINE;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_DECLINE;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_REQUEST;
-
-@Immutable
-@NotNullByDefault
-public enum IntroduceeProtocolState {
-
-	ERROR(0),
-	AWAIT_REQUEST(1) {
-		@Override
-		public IntroduceeProtocolState next(IntroduceeAction a) {
-			if (a == REMOTE_REQUEST) return AWAIT_RESPONSES;
-			return ERROR;
-		}
-	},
-	AWAIT_RESPONSES(2) {
-		@Override
-		public IntroduceeProtocolState next(IntroduceeAction a) {
-			if (a == REMOTE_ACCEPT) return AWAIT_LOCAL_RESPONSE;
-			if (a == REMOTE_DECLINE) return FINISHED;
-			if (a == LOCAL_ACCEPT) return AWAIT_REMOTE_RESPONSE;
-			if (a == LOCAL_DECLINE) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_REMOTE_RESPONSE(3) {
-		@Override
-		public IntroduceeProtocolState next(IntroduceeAction a) {
-			if (a == REMOTE_ACCEPT) return AWAIT_ACK;
-			if (a == REMOTE_DECLINE) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_LOCAL_RESPONSE(4) {
-		@Override
-		public IntroduceeProtocolState next(IntroduceeAction a) {
-			if (a == LOCAL_ACCEPT) return AWAIT_ACK;
-			if (a == LOCAL_DECLINE) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_ACK(5) {
-		@Override
-		public IntroduceeProtocolState next(IntroduceeAction a) {
-			if (a == ACK) return FINISHED;
-			return ERROR;
-		}
-	},
-	FINISHED(6);
-
-	private final int value;
-
-	IntroduceeProtocolState(int value) {
-		this.value = value;
-	}
-
-	public int getValue() {
-		return value;
-	}
-
-	public static IntroduceeProtocolState fromValue(int value) {
-		for (IntroduceeProtocolState s : values()) {
-			if (s.value == value) return s;
-		}
-		throw new IllegalArgumentException();
-	}
-
-	public IntroduceeProtocolState next(IntroduceeAction a) {
-		return this;
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java
deleted file mode 100644
index 7123c7eb26f8abf6f9c455307253aaaf04f59c75..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.briarproject.briar.api.introduction;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import javax.annotation.Nullable;
-
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-
-@NotNullByDefault
-public enum IntroducerAction {
-
-	LOCAL_REQUEST,
-	LOCAL_ABORT,
-	REMOTE_ACCEPT_1,
-	REMOTE_ACCEPT_2,
-	REMOTE_DECLINE_1,
-	REMOTE_DECLINE_2,
-	REMOTE_ABORT,
-	ACK_1,
-	ACK_2;
-
-	@Nullable
-	public static IntroducerAction getLocal(int type) {
-		if (type == TYPE_REQUEST) return LOCAL_REQUEST;
-		if (type == TYPE_ABORT) return LOCAL_ABORT;
-		return null;
-	}
-
-	@Nullable
-	public static IntroducerAction getRemote(int type, boolean one,
-			boolean accept) {
-
-		if (one) {
-			if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_1;
-			if (type == TYPE_RESPONSE) return REMOTE_DECLINE_1;
-			if (type == TYPE_ACK) return ACK_1;
-		} else {
-			if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_2;
-			if (type == TYPE_RESPONSE) return REMOTE_DECLINE_2;
-			if (type == TYPE_ACK) return ACK_2;
-		}
-		if (type == TYPE_ABORT) return REMOTE_ABORT;
-		return null;
-	}
-
-	@Nullable
-	public static IntroducerAction getRemote(int type, boolean one) {
-		return getRemote(type, one, true);
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java
deleted file mode 100644
index b3d89864e421b60853111c67e43fdbe9a2c6a6f3..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.briarproject.briar.api.introduction;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import javax.annotation.concurrent.Immutable;
-
-import static org.briarproject.briar.api.introduction.IntroducerAction.ACK_1;
-import static org.briarproject.briar.api.introduction.IntroducerAction.ACK_2;
-import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_1;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_2;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_1;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_2;
-
-@Immutable
-@NotNullByDefault
-public enum IntroducerProtocolState {
-
-	ERROR(0),
-	PREPARE_REQUESTS(1) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == LOCAL_REQUEST) return AWAIT_RESPONSES;
-			return ERROR;
-		}
-	},
-	AWAIT_RESPONSES(2) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == REMOTE_ACCEPT_1) return AWAIT_RESPONSE_2;
-			if (a == REMOTE_ACCEPT_2) return AWAIT_RESPONSE_1;
-			if (a == REMOTE_DECLINE_1) return FINISHED;
-			if (a == REMOTE_DECLINE_2) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_RESPONSE_1(3) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == REMOTE_ACCEPT_1) return AWAIT_ACKS;
-			if (a == REMOTE_DECLINE_1) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_RESPONSE_2(4) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == REMOTE_ACCEPT_2) return AWAIT_ACKS;
-			if (a == REMOTE_DECLINE_2) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_ACKS(5) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == ACK_1) return AWAIT_ACK_2;
-			if (a == ACK_2) return AWAIT_ACK_1;
-			return ERROR;
-		}
-	},
-	AWAIT_ACK_1(6) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == ACK_1) return FINISHED;
-			return ERROR;
-		}
-	},
-	AWAIT_ACK_2(7) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == ACK_2) return FINISHED;
-			return ERROR;
-		}
-	},
-	FINISHED(8) {
-		@Override
-		public IntroducerProtocolState next(IntroducerAction a) {
-			if (a == REMOTE_ABORT) return ERROR;
-			return FINISHED;
-		}
-	};
-
-	private final int value;
-
-	IntroducerProtocolState(int value) {
-		this.value = value;
-	}
-
-	public int getValue() {
-		return value;
-	}
-
-	public static IntroducerProtocolState fromValue(int value) {
-		for (IntroducerProtocolState s : values()) {
-			if (s.value == value) return s;
-		}
-		throw new IllegalArgumentException();
-	}
-
-	public static boolean isOngoing(IntroducerProtocolState state) {
-		return state != FINISHED && state != ERROR;
-	}
-
-	public IntroducerProtocolState next(IntroducerAction a) {
-		return this;
-	}
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
index 72e6030b10b207a179abf99be29e7897af2777cb..9b454218d5e736f5d17546e4bbe67775e40684f1 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
@@ -4,126 +4,26 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_L
 
 public interface IntroductionConstants {
 
-	/* Protocol roles */
-	int ROLE_INTRODUCER = 0;
-	int ROLE_INTRODUCEE = 1;
-
-	/* Message types */
-	int TYPE_REQUEST = 1;
-	int TYPE_RESPONSE = 2;
-	int TYPE_ACK = 3;
-	int TYPE_ABORT = 4;
-
-	/* Message Constants */
-	String TYPE = "type";
-	String GROUP_ID = "groupId";
-	String SESSION_ID = "sessionId";
-	String CONTACT = "contactId";
-	String NAME = "name";
-	String PUBLIC_KEY = "publicKey";
-	String E_PUBLIC_KEY = "ephemeralPublicKey";
-	String MSG = "msg";
-	String ACCEPT = "accept";
-	String TIME = "time";
-	String TRANSPORT = "transport";
-	String MESSAGE_ID = "messageId";
-	String MESSAGE_TIME = "timestamp";
-	String MAC = "mac";
-	String SIGNATURE = "signature";
-
-	/* Validation Constants */
-
-	/**
-	 * The length of the message authentication code in bytes.
-	 */
-	int MAC_LENGTH = 32;
-
 	/**
 	 * The maximum length of the introducer's optional message to the
 	 * introducees in UTF-8 bytes.
 	 */
-	int MAX_INTRODUCTION_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
-
-	/* Introducer Local State Metadata */
-	String STATE = "state";
-	String ROLE = "role";
-	String GROUP_ID_1 = "groupId1";
-	String GROUP_ID_2 = "groupId2";
-	String CONTACT_1 = "contact1";
-	String CONTACT_2 = "contact2";
-	String AUTHOR_ID_1 = "authorId1";
-	String AUTHOR_ID_2 = "authorId2";
-	String CONTACT_ID_1 = "contactId1";
-	String CONTACT_ID_2 = "contactId2";
-	String RESPONSE_1 = "response1";
-	String RESPONSE_2 = "response2";
-
-	/* Introduction Request Action */
-	String PUBLIC_KEY1 = "publicKey1";
-	String PUBLIC_KEY2 = "publicKey2";
-
-	/* Introducee Local State Metadata (without those already defined) */
-	String STORAGE_ID = "storageId";
-	String INTRODUCER = "introducer";
-	String LOCAL_AUTHOR_ID = "localAuthorId";
-	String REMOTE_AUTHOR_ID = "remoteAuthorId";
-	String OUR_PUBLIC_KEY = "ourEphemeralPublicKey";
-	String OUR_PRIVATE_KEY = "ourEphemeralPrivateKey";
-	String OUR_TIME = "ourTime";
-	String ADDED_CONTACT_ID = "addedContactId";
-	String NOT_OUR_RESPONSE = "notOurResponse";
-	String EXISTS = "contactExists";
-	String REMOTE_AUTHOR_IS_US = "remoteAuthorIsUs";
-	String ANSWERED = "answered";
-	String NONCE = "nonce";
-	String MAC_KEY = "macKey";
-	String OUR_TRANSPORT = "ourTransport";
-	String OUR_MAC = "ourMac";
-	String OUR_SIGNATURE = "ourSignature";
-
-	String TASK = "task";
-	int TASK_ADD_CONTACT = 0;
-	int TASK_ACTIVATE_CONTACT = 1;
-	int TASK_ABORT = 2;
-
-	/**
-	 * Label for deriving the shared secret.
-	 */
-	String SHARED_SECRET_LABEL =
-			"org.briarproject.briar.introduction/SHARED_SECRET";
+	int MAX_REQUEST_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
 
-	/**
-	 * Label for deriving Alice's key binding nonce from the shared secret.
-	 */
-	String ALICE_NONCE_LABEL =
-			"org.briarproject.briar.introduction/ALICE_NONCE";
+	String LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID";
 
-	/**
-	 * Label for deriving Bob's key binding nonce from the shared secret.
-	 */
-	String BOB_NONCE_LABEL =
-			"org.briarproject.briar.introduction/BOB_NONCE";
+	String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY";
 
-	/**
-	 * Label for deriving Alice's MAC key from the shared secret.
-	 */
-	String ALICE_MAC_KEY_LABEL =
+	String LABEL_ALICE_MAC_KEY =
 			"org.briarproject.briar.introduction/ALICE_MAC_KEY";
 
-	/**
-	 * Label for deriving Bob's MAC key from the shared secret.
-	 */
-	String BOB_MAC_KEY_LABEL =
+	String LABEL_BOB_MAC_KEY =
 			"org.briarproject.briar.introduction/BOB_MAC_KEY";
 
-	/**
-	 * Label for signing the introduction response.
-	 */
-	String SIGNING_LABEL =
-			"org.briarproject.briar.introduction/RESPONSE_SIGNATURE";
+	String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC";
+
+	String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN";
+
+	String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE";
 
-	/**
-	 * Label for MACing the introduction response.
-	 */
-	String MAC_LABEL = "org.briarproject.briar.introduction/RESPONSE_MAC";
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
index ce9d067277fabd9aed94a254f3c86f2b47ee3775..15936e8ad84f9fad746902d0ac88293a01b0495f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.api.introduction;
 
-import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DbException;
@@ -24,25 +23,25 @@ public interface IntroductionManager extends ConversationClient {
 	/**
 	 * The current version of the introduction client.
 	 */
-	int CLIENT_VERSION = 0;
+	int CLIENT_VERSION = 1;
 
 	/**
 	 * Sends two initial introduction messages.
 	 */
 	void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
-			long timestamp) throws DbException, FormatException;
+			long timestamp) throws DbException;
 
 	/**
 	 * Accepts an introduction.
 	 */
 	void acceptIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException, FormatException;
+			long timestamp) throws DbException;
 
 	/**
 	 * Declines an introduction.
 	 */
 	void declineIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException, FormatException;
+			long timestamp) throws DbException;
 
 	/**
 	 * Returns all introduction messages for the given contact.
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
index 861469a3a4d0c8eb6b9f34b5c6e5247db98fe995..009487fa2faf852fc5aa843db4d3aba9a09ba24f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java
@@ -8,7 +8,7 @@ import org.briarproject.briar.api.client.SessionId;
 
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 
 @Immutable
 @NotNullByDefault
@@ -16,10 +16,10 @@ public class IntroductionMessage extends BaseMessageHeader {
 
 	private final SessionId sessionId;
 	private final MessageId messageId;
-	private final int role;
+	private final Role role;
 
 	IntroductionMessage(SessionId sessionId, MessageId messageId,
-			GroupId groupId, int role, long time, boolean local, boolean sent,
+			GroupId groupId, Role role, long time, boolean local, boolean sent,
 			boolean seen, boolean read) {
 
 		super(messageId, groupId, time, local, sent, seen, read);
@@ -37,7 +37,7 @@ public class IntroductionMessage extends BaseMessageHeader {
 	}
 
 	public boolean isIntroducer() {
-		return role == ROLE_INTRODUCER;
+		return role == INTRODUCER;
 	}
 
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
index 428528260c6a4f11dbd88983e78f55183f9b1614..b2a804bd88b8de918546c68e15a3327e4605180f 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.api.introduction;
 
-import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
@@ -15,21 +14,19 @@ public class IntroductionRequest extends IntroductionResponse {
 
 	@Nullable
 	private final String message;
-	private final boolean answered, exists, introducesOtherIdentity;
+	private final boolean answered, exists;
 
 	public IntroductionRequest(SessionId sessionId, MessageId messageId,
-			GroupId groupId, int role, long time, boolean local, boolean sent,
-			boolean seen, boolean read, AuthorId authorId, String name,
-			boolean accepted, @Nullable String message, boolean answered,
-			boolean exists, boolean introducesOtherIdentity) {
+			GroupId groupId, Role role, long time, boolean local, boolean sent,
+			boolean seen, boolean read, String name, boolean accepted,
+			@Nullable String message, boolean answered, boolean exists) {
 
 		super(sessionId, messageId, groupId, role, time, local, sent, seen,
-				read, authorId, name, accepted);
+				read, name, accepted);
 
 		this.message = message;
 		this.answered = answered;
 		this.exists = exists;
-		this.introducesOtherIdentity = introducesOtherIdentity;
 	}
 
 	@Nullable
@@ -44,8 +41,4 @@ public class IntroductionRequest extends IntroductionResponse {
 	public boolean contactExists() {
 		return exists;
 	}
-
-	public boolean doesIntroduceOtherIdentity() {
-		return introducesOtherIdentity;
-	}
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
index 22df6eba89e2e44d38fb15cdd94bd75dfacd3cb4..816135d43f59668a4ecfa4baf27e625ba2469462 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.api.introduction;
 
-import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
@@ -12,19 +11,15 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class IntroductionResponse extends IntroductionMessage {
 
-	private final AuthorId remoteAuthorId;
 	private final String name;
 	private final boolean accepted;
 
 	public IntroductionResponse(SessionId sessionId, MessageId messageId,
-			GroupId groupId, int role, long time, boolean local, boolean sent,
-			boolean seen, boolean read, AuthorId remoteAuthorId, String name,
-			boolean accepted) {
-
+			GroupId groupId, Role role, long time, boolean local, boolean sent,
+			boolean seen, boolean read, String name, boolean accepted) {
 		super(sessionId, messageId, groupId, role, time, local, sent, seen,
 				read);
 
-		this.remoteAuthorId = remoteAuthorId;
 		this.name = name;
 		this.accepted = accepted;
 	}
@@ -37,7 +32,4 @@ public class IntroductionResponse extends IntroductionMessage {
 		return accepted;
 	}
 
-	public AuthorId getRemoteAuthorId() {
-		return remoteAuthorId;
-	}
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java
similarity index 91%
rename from briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java
rename to briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java
index bc7e4e3d907b92ca717e59519ea509c1a38222fb..38f0bd44cf85fb804fcff9f49e520474423e4a19 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.api.introduction2;
+package org.briarproject.briar.api.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
index 5855275918a6001dbabf6211183d5aad40d4b9a5..113ba400efdebb484b0b41b84d70ccc64443f2b3 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.api.introduction.event;
 
-import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.client.SessionId;
@@ -11,19 +10,14 @@ import javax.annotation.concurrent.Immutable;
 @NotNullByDefault
 public class IntroductionAbortedEvent extends Event {
 
-	private final ContactId contactId;
 	private final SessionId sessionId;
 
-	public IntroductionAbortedEvent(ContactId contactId, SessionId sessionId) {
-		this.contactId = contactId;
+	public IntroductionAbortedEvent(SessionId sessionId) {
 		this.sessionId = sessionId;
 	}
 
-	public ContactId getContactId() {
-		return contactId;
-	}
-
 	public SessionId getSessionId() {
 		return sessionId;
 	}
+
 }
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java
index 88998789c201b3ea1a5d4a788d3651904f91b4aa..44d166863ea1094b72358c7fb7eb4c514e347d00 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionSucceededEvent.java
@@ -8,6 +8,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
+// TODO still needed?
 public class IntroductionSucceededEvent extends Event {
 
 	private final Contact contact;
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java
deleted file mode 100644
index 962bd0a52b723f6089f70a74a26f7a3654321e8b..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.briarproject.briar.api.introduction2;
-
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
-
-public interface IntroductionConstants {
-
-	/**
-	 * The maximum length of the introducer's optional message to the
-	 * introducees in UTF-8 bytes.
-	 */
-	int MAX_REQUEST_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
-
-	String LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID";
-
-	String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY";
-
-	String LABEL_ALICE_MAC_KEY =
-			"org.briarproject.briar.introduction/ALICE_MAC_KEY";
-
-	String LABEL_BOB_MAC_KEY =
-			"org.briarproject.briar.introduction/BOB_MAC_KEY";
-
-	String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC";
-
-	String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN";
-
-	String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE";
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java
deleted file mode 100644
index f3d5c40fa4f6a03736060492e6defea4c8258e4d..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.briarproject.briar.api.introduction2;
-
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.ClientId;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
-
-import java.util.Collection;
-
-import javax.annotation.Nullable;
-
-@NotNullByDefault
-public interface IntroductionManager extends ConversationClient {
-
-	/**
-	 * The unique ID of the introduction client.
-	 */
-	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.introduction");
-
-	/**
-	 * The current version of the introduction client.
-	 */
-	int CLIENT_VERSION = 1;
-
-	/**
-	 * Sends two initial introduction messages.
-	 */
-	void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
-			long timestamp) throws DbException;
-
-	/**
-	 * Accepts an introduction.
-	 */
-	void acceptIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException;
-
-	/**
-	 * Declines an introduction.
-	 */
-	void declineIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException;
-
-	/**
-	 * Returns all introduction messages for the given contact.
-	 */
-	Collection<IntroductionMessage> getIntroductionMessages(ContactId contactId)
-			throws DbException;
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionMessage.java
deleted file mode 100644
index a4b3999d1602dd109f5505f9c331ebcda5869875..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionMessage.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.briarproject.briar.api.introduction2;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.BaseMessageHeader;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.concurrent.Immutable;
-
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
-
-@Immutable
-@NotNullByDefault
-public class IntroductionMessage extends BaseMessageHeader {
-
-	private final SessionId sessionId;
-	private final MessageId messageId;
-	private final Role role;
-
-	IntroductionMessage(SessionId sessionId, MessageId messageId,
-			GroupId groupId, Role role, long time, boolean local, boolean sent,
-			boolean seen, boolean read) {
-
-		super(messageId, groupId, time, local, sent, seen, read);
-		this.sessionId = sessionId;
-		this.messageId = messageId;
-		this.role = role;
-	}
-
-	public SessionId getSessionId() {
-		return sessionId;
-	}
-
-	public MessageId getMessageId() {
-		return messageId;
-	}
-
-	public boolean isIntroducer() {
-		return role == INTRODUCER;
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionRequest.java
deleted file mode 100644
index 4494fa123d42a2b162a993429b5ffbebf1938323..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionRequest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.briarproject.briar.api.introduction2;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-public class IntroductionRequest extends IntroductionResponse {
-
-	@Nullable
-	private final String message;
-	private final boolean answered, exists;
-
-	public IntroductionRequest(SessionId sessionId, MessageId messageId,
-			GroupId groupId, Role role, long time, boolean local, boolean sent,
-			boolean seen, boolean read, String name, boolean accepted,
-			@Nullable String message, boolean answered, boolean exists) {
-
-		super(sessionId, messageId, groupId, role, time, local, sent, seen,
-				read, name, accepted);
-
-		this.message = message;
-		this.answered = answered;
-		this.exists = exists;
-	}
-
-	@Nullable
-	public String getMessage() {
-		return message;
-	}
-
-	public boolean wasAnswered() {
-		return answered;
-	}
-
-	public boolean contactExists() {
-		return exists;
-	}
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionResponse.java
deleted file mode 100644
index b9d2bd99367a6b63656c42ade5bb4a3d59d01546..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionResponse.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.briarproject.briar.api.introduction2;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-public class IntroductionResponse extends IntroductionMessage {
-
-	private final String name;
-	private final boolean accepted;
-
-	public IntroductionResponse(SessionId sessionId, MessageId messageId,
-			GroupId groupId, Role role, long time, boolean local, boolean sent,
-			boolean seen, boolean read, String name, boolean accepted) {
-		super(sessionId, messageId, groupId, role, time, local, sent, seen,
-				read);
-
-		this.name = name;
-		this.accepted = accepted;
-	}
-
-	public String getName() {
-		return name;
-	}
-
-	public boolean wasAccepted() {
-		return accepted;
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionAbortedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionAbortedEvent.java
deleted file mode 100644
index 610db397054b9c0b984ac275f2d8663ba6462283..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionAbortedEvent.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.briarproject.briar.api.introduction2.event;
-
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.client.SessionId;
-
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-// TODO still needed?
-public class IntroductionAbortedEvent extends Event {
-
-	private final AuthorId remoteAuthorId;
-	private final SessionId sessionId;
-
-	public IntroductionAbortedEvent(AuthorId remoteAuthorId,
-			SessionId sessionId) {
-		this.remoteAuthorId = remoteAuthorId;
-		this.sessionId = sessionId;
-	}
-
-	public AuthorId getRemoteAuthorId() {
-		return remoteAuthorId;
-	}
-
-	public SessionId getSessionId() {
-		return sessionId;
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionRequestReceivedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionRequestReceivedEvent.java
deleted file mode 100644
index 0798ac356fc88ed78136448712e2607a548fe0fd..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionRequestReceivedEvent.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.briarproject.briar.api.introduction2.event;
-
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.introduction2.IntroductionRequest;
-
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-public class IntroductionRequestReceivedEvent extends Event {
-
-	private final ContactId contactId;
-	private final IntroductionRequest introductionRequest;
-
-	public IntroductionRequestReceivedEvent(ContactId contactId,
-			IntroductionRequest introductionRequest) {
-
-		this.contactId = contactId;
-		this.introductionRequest = introductionRequest;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-
-	public IntroductionRequest getIntroductionRequest() {
-		return introductionRequest;
-	}
-
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionResponseReceivedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionResponseReceivedEvent.java
deleted file mode 100644
index a05731b5996f620c6f7d296c6efc108408a50185..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionResponseReceivedEvent.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.briarproject.briar.api.introduction2.event;
-
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.briar.api.introduction2.IntroductionResponse;
-
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-public class IntroductionResponseReceivedEvent extends Event {
-
-	private final ContactId contactId;
-	private final IntroductionResponse introductionResponse;
-
-	public IntroductionResponseReceivedEvent(ContactId contactId,
-			IntroductionResponse introductionResponse) {
-
-		this.contactId = contactId;
-		this.introductionResponse = introductionResponse;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-
-	public IntroductionResponse getIntroductionResponse() {
-		return introductionResponse;
-	}
-}
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionSucceededEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionSucceededEvent.java
deleted file mode 100644
index 11f85c2ce83b114a5c83cbbb09fecc1a9b9c339f..0000000000000000000000000000000000000000
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/event/IntroductionSucceededEvent.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.briarproject.briar.api.introduction2.event;
-
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import javax.annotation.concurrent.Immutable;
-
-@Immutable
-@NotNullByDefault
-// TODO still needed?
-public class IntroductionSucceededEvent extends Event {
-
-	private final Contact contact;
-
-	public IntroductionSucceededEvent(Contact contact) {
-		this.contact = contact;
-	}
-
-	public Contact getContact() {
-		return contact;
-	}
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
similarity index 86%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/AbortMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
index 9d7112407614ec456ce65647174fe272cc09f1c8..e9a2d1233489716140048f80642ca2687c3a33de 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/AbortMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class AbortMessage extends IntroductionMessage {
+class AbortMessage extends AbstractIntroductionMessage {
 
 	private final SessionId sessionId;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java
similarity index 84%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java
index 24a252f5ef31170a37415c851dee3c2d152c435c..240b5ddecf2e7915041da47b0b7df0e0bc59a7e9 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-abstract class IntroductionMessage {
+abstract class AbstractIntroductionMessage {
 
 	private final MessageId messageId;
 	private final GroupId groupId;
@@ -17,7 +17,7 @@ abstract class IntroductionMessage {
 	@Nullable
 	private final MessageId previousMessageId;
 
-	IntroductionMessage(MessageId messageId, GroupId groupId,
+	AbstractIntroductionMessage(MessageId messageId, GroupId groupId,
 			long timestamp, @Nullable MessageId previousMessageId) {
 		this.messageId = messageId;
 		this.groupId = groupId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
similarity index 90%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
index d2c9f227098617991291ad63b2b6a81de614c523..20ac59307842b7edc02296a42fbdca5ce095c453 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -25,14 +25,14 @@ import java.util.Map;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_ID;
-import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_VERSION;
-import static org.briarproject.briar.introduction2.MessageType.ABORT;
-import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
-import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
-import static org.briarproject.briar.introduction2.MessageType.AUTH;
-import static org.briarproject.briar.introduction2.MessageType.DECLINE;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
+import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.introduction.MessageType.ABORT;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+import static org.briarproject.briar.introduction.MessageType.DECLINE;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 
 @Immutable
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/AcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
similarity index 93%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/AcceptMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
index 77d09c74ac33cf3a5d6df2a3c958a02a3e5f20bd..6cadae73b6257bbe76c67d5904a47dba286838c3 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/AcceptMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.TransportId;
@@ -14,7 +14,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class AcceptMessage extends IntroductionMessage {
+class AcceptMessage extends AbstractIntroductionMessage {
 
 	private final SessionId sessionId;
 	private final byte[] ephemeralPublicKey;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/ActivateMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
similarity index 85%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/ActivateMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
index 0d5e935b489ff703998ceecea70c13fa07557a91..e7498dec2781c0ed36f8bebd8077ebabb47d2f5e 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/ActivateMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class ActivateMessage extends IntroductionMessage {
+class ActivateMessage extends AbstractIntroductionMessage {
 
 	private final SessionId sessionId;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/AuthMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
similarity index 89%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/AuthMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
index 5833a64d689e540fb0b53b1d37e5199055d8192e..1de1a4eb5803cbbfef2fb82c6fbc99e88ea09f5d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/AuthMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class AuthMessage extends IntroductionMessage {
+class AuthMessage extends AbstractIntroductionMessage {
 
 	private final SessionId sessionId;
 	private final byte[] mac, signature;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
similarity index 86%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/DeclineMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
index c419704c5c72e2e3aed1438f893a57ea372e2302..27386b90587a7297174ffbbf27e2ef82544b2444 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/DeclineMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class DeclineMessage extends IntroductionMessage {
+class DeclineMessage extends AbstractIntroductionMessage {
 
 	private final SessionId sessionId;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java
deleted file mode 100644
index 2d5bb95c3ad591c57b5aabb0889f48c4803d93ab..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java
+++ /dev/null
@@ -1,372 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.ProtocolEngine;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.IntroduceeAction;
-import org.briarproject.briar.api.introduction.IntroduceeProtocolState;
-import org.briarproject.briar.api.introduction.IntroductionRequest;
-import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.logging.Logger;
-
-import javax.annotation.concurrent.Immutable;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.ACK;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ABORT;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_DECLINE;
-import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_ACK;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_RESPONSES;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.ERROR;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-
-@Immutable
-@NotNullByDefault
-class IntroduceeEngine
-		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
-
-	private static final Logger LOG =
-			Logger.getLogger(IntroduceeEngine.class.getName());
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
-			BdfDictionary localState, BdfDictionary localAction) {
-
-		try {
-			IntroduceeProtocolState currentState =
-					getState(localState.getLong(STATE));
-			int type = localAction.getLong(TYPE).intValue();
-			IntroduceeAction action;
-			if (localState.containsKey(ACCEPT)) action = IntroduceeAction
-					.getLocal(type, localState.getBoolean(ACCEPT));
-			else action = IntroduceeAction.getLocal(type);
-			IntroduceeProtocolState nextState = currentState.next(action);
-
-			if (action == LOCAL_ABORT && currentState != ERROR) {
-				return abortSession(currentState, localState);
-			}
-
-			if (nextState == ERROR) {
-				if (LOG.isLoggable(WARNING)) {
-					LOG.warning("Error: Invalid action in state " +
-							currentState.name());
-				}
-				if (currentState == ERROR) return noUpdate(localState);
-				else return abortSession(currentState, localState);
-			}
-
-			List<BdfDictionary> messages = new ArrayList<>(1);
-			if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) {
-				localState.put(STATE, nextState.getValue());
-				localState.put(ANSWERED, true);
-				// create the introduction response message
-				BdfDictionary msg = new BdfDictionary();
-				msg.put(TYPE, TYPE_RESPONSE);
-				msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
-				msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
-				msg.put(ACCEPT, localState.getBoolean(ACCEPT));
-				if (localState.getBoolean(ACCEPT)) {
-					msg.put(TIME, localState.getLong(OUR_TIME));
-					msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY));
-					msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT));
-				}
-				msg.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
-				messages.add(msg);
-				logAction(currentState, localState, msg);
-
-				if (nextState == AWAIT_ACK) {
-					localState.put(TASK, TASK_ADD_CONTACT);
-				}
-			} else if (action == ACK) {
-				// just send ACK, don't update local state again
-				BdfDictionary ack = getAckMessage(localState);
-				messages.add(ack);
-			} else {
-				throw new IllegalArgumentException();
-			}
-			List<Event> events = Collections.emptyList();
-			return new StateUpdate<>(false, false,
-					localState, messages, events);
-		} catch (FormatException e) {
-			throw new IllegalArgumentException(e);
-		}
-	}
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
-			BdfDictionary localState, BdfDictionary msg) {
-
-		try {
-			IntroduceeProtocolState currentState =
-					getState(localState.getLong(STATE));
-			int type = msg.getLong(TYPE).intValue();
-			IntroduceeAction action = IntroduceeAction.getRemote(type);
-			IntroduceeProtocolState nextState = currentState.next(action);
-
-			logMessageReceived(currentState, nextState, localState, type, msg);
-
-			if (nextState == ERROR) {
-				if (currentState != ERROR && action != REMOTE_ABORT) {
-					return abortSession(currentState, localState);
-				} else {
-					return noUpdate(localState);
-				}
-			}
-
-			// update local session state with next protocol state
-			localState.put(STATE, nextState.getValue());
-			List<BdfDictionary> messages;
-			List<Event> events;
-			// we received the introduction request
-			if (currentState == AWAIT_REQUEST) {
-				// remember the session ID used by the introducer
-				localState.put(SESSION_ID, msg.getRaw(SESSION_ID));
-
-				addRequestData(localState, msg);
-				messages = Collections.emptyList();
-				events = Collections.singletonList(getEvent(localState, msg));
-			}
-			// we had the request and now one response came in _OR_
-			// we had sent our response already and now received the other one
-			else if (currentState == AWAIT_RESPONSES ||
-					currentState == AWAIT_REMOTE_RESPONSE) {
-				// update next state based on message content
-				action = IntroduceeAction
-						.getRemote(type, msg.getBoolean(ACCEPT));
-				nextState = currentState.next(action);
-				localState.put(STATE, nextState.getValue());
-
-				addResponseData(localState, msg);
-				if (nextState == AWAIT_ACK) {
-					localState.put(TASK, TASK_ADD_CONTACT);
-				}
-				messages = Collections.emptyList();
-				events = Collections.emptyList();
-			}
-			// we already sent our ACK and now received the other one
-			else if (currentState == AWAIT_ACK) {
-				localState.put(TASK, TASK_ACTIVATE_CONTACT);
-				addAckData(localState, msg);
-				messages = Collections.emptyList();
-				events = Collections.emptyList();
-			}
-			// we are done (probably declined response), ignore & delete message
-			else if (currentState == FINISHED) {
-				return new StateUpdate<>(true, false, localState,
-						Collections.<BdfDictionary>emptyList(),
-						Collections.emptyList());
-			}
-			// this should not happen
-			else {
-				throw new IllegalArgumentException();
-			}
-			return new StateUpdate<>(false, false,
-					localState, messages, events);
-		} catch (FormatException e) {
-			throw new IllegalArgumentException(e);
-		}
-	}
-
-	private void addRequestData(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		localState.put(NAME, msg.getString(NAME));
-		localState.put(PUBLIC_KEY, msg.getRaw(PUBLIC_KEY));
-		if (msg.containsKey(MSG)) {
-			localState.put(MSG, msg.getString(MSG));
-		}
-	}
-
-	private void addResponseData(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		if (localState.containsKey(ACCEPT)) {
-			localState.put(ACCEPT,
-					localState.getBoolean(ACCEPT) && msg.getBoolean(ACCEPT));
-		} else {
-			localState.put(ACCEPT, msg.getBoolean(ACCEPT));
-		}
-		localState.put(NOT_OUR_RESPONSE, msg.getRaw(MESSAGE_ID));
-
-		if (msg.getBoolean(ACCEPT)) {
-			localState.put(TIME, msg.getLong(TIME));
-			localState.put(E_PUBLIC_KEY, msg.getRaw(E_PUBLIC_KEY));
-			localState.put(TRANSPORT, msg.getDictionary(TRANSPORT));
-		}
-	}
-
-	private void addAckData(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		localState.put(MAC, msg.getRaw(MAC));
-		localState.put(SIGNATURE, msg.getRaw(SIGNATURE));
-	}
-
-	private BdfDictionary getAckMessage(BdfDictionary localState)
-			throws FormatException {
-
-		BdfDictionary m = new BdfDictionary();
-		m.put(TYPE, TYPE_ACK);
-		m.put(GROUP_ID, localState.getRaw(GROUP_ID));
-		m.put(SESSION_ID, localState.getRaw(SESSION_ID));
-		m.put(MAC, localState.getRaw(OUR_MAC));
-		m.put(SIGNATURE, localState.getRaw(OUR_SIGNATURE));
-		return m;
-	}
-
-	private void logAction(IntroduceeProtocolState state,
-			BdfDictionary localState, BdfDictionary msg) {
-
-		if (!LOG.isLoggable(INFO)) return;
-
-		try {
-			LOG.info("Sending " +
-					(localState.getBoolean(ACCEPT) ? "accept " : "decline ") +
-					"response in state " + state.name());
-			LOG.info("Moving on to state " +
-					getState(localState.getLong(STATE)).name());
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	private void logMessageReceived(IntroduceeProtocolState currentState,
-			IntroduceeProtocolState nextState, BdfDictionary localState,
-			int type, BdfDictionary msg) {
-
-		if (!LOG.isLoggable(INFO)) return;
-
-		String t = "unknown";
-		if (type == TYPE_REQUEST) t = "Introduction";
-		else if (type == TYPE_RESPONSE) t = "Response";
-		else if (type == TYPE_ACK) t = "ACK";
-		else if (type == TYPE_ABORT) t = "Abort";
-
-		LOG.info("Received " + t + " in state " + currentState.name());
-		LOG.info("Moving on to state " + nextState.name());
-	}
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
-			BdfDictionary localState, BdfDictionary delivered) {
-		try {
-			return noUpdate(localState);
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			return null;
-		}
-	}
-
-	private IntroduceeProtocolState getState(Long state) {
-		return IntroduceeProtocolState.fromValue(state.intValue());
-	}
-
-	private Event getEvent(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		ContactId contactId =
-				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
-		AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID));
-
-		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
-		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
-		GroupId groupId = new GroupId(msg.getRaw(GROUP_ID));
-		long time = msg.getLong(MESSAGE_TIME);
-		String name = msg.getString(NAME);
-		String message = msg.getOptionalString(MSG);
-		boolean exists = localState.getBoolean(EXISTS);
-		boolean introducesOtherIdentity =
-				localState.getBoolean(REMOTE_AUTHOR_IS_US);
-
-		IntroductionRequest ir = new IntroductionRequest(sessionId, messageId,
-				groupId, ROLE_INTRODUCEE, time, false, false, false, false,
-				authorId, name, false, message, false, exists,
-				introducesOtherIdentity);
-		return new IntroductionRequestReceivedEvent(contactId, ir);
-	}
-
-	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
-			IntroduceeProtocolState currentState, BdfDictionary localState)
-			throws FormatException {
-
-		if (LOG.isLoggable(WARNING))
-			LOG.warning("Aborting protocol session in state " +
-					currentState.name());
-
-		localState.put(STATE, ERROR.getValue());
-		localState.put(TASK, TASK_ABORT);
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_ABORT);
-		msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
-		msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
-		List<BdfDictionary> messages = Collections.singletonList(msg);
-
-		// send abort event
-		ContactId contactId =
-				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
-		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
-		Event event = new IntroductionAbortedEvent(contactId, sessionId);
-		List<Event> events = Collections.singletonList(event);
-
-		return new StateUpdate<>(false, false, localState, messages, events);
-	}
-
-	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
-			BdfDictionary localState) throws FormatException {
-
-		return new StateUpdate<>(false, false, localState,
-				Collections.<BdfDictionary>emptyList(),
-				Collections.emptyList());
-	}
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java
deleted file mode 100644
index 4666ac92e68bdf30621f35fa33e3801c221ef23f..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java
+++ /dev/null
@@ -1,569 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.Bytes;
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.contact.ContactManager;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.crypto.KeyPair;
-import org.briarproject.bramble.api.crypto.KeyParser;
-import org.briarproject.bramble.api.crypto.PrivateKey;
-import org.briarproject.bramble.api.crypto.PublicKey;
-import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.AuthorFactory;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.identity.IdentityManager;
-import org.briarproject.bramble.api.identity.LocalAuthor;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.plugin.TransportId;
-import org.briarproject.bramble.api.properties.TransportProperties;
-import org.briarproject.bramble.api.properties.TransportPropertyManager;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-import javax.inject.Inject;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ADDED_CONTACT_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_MAC_KEY_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_NONCE_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
-
-@Immutable
-@NotNullByDefault
-class IntroduceeManager {
-
-	private static final Logger LOG =
-			Logger.getLogger(IntroduceeManager.class.getName());
-
-	private final MessageSender messageSender;
-	private final DatabaseComponent db;
-	private final ClientHelper clientHelper;
-	private final Clock clock;
-	private final CryptoComponent cryptoComponent;
-	private final TransportPropertyManager transportPropertyManager;
-	private final AuthorFactory authorFactory;
-	private final ContactManager contactManager;
-	private final IdentityManager identityManager;
-	private final IntroductionGroupFactory introductionGroupFactory;
-
-	@Inject
-	IntroduceeManager(MessageSender messageSender, DatabaseComponent db,
-			ClientHelper clientHelper, Clock clock,
-			CryptoComponent cryptoComponent,
-			TransportPropertyManager transportPropertyManager,
-			AuthorFactory authorFactory, ContactManager contactManager,
-			IdentityManager identityManager,
-			IntroductionGroupFactory introductionGroupFactory) {
-
-		this.messageSender = messageSender;
-		this.db = db;
-		this.clientHelper = clientHelper;
-		this.clock = clock;
-		this.cryptoComponent = cryptoComponent;
-		this.transportPropertyManager = transportPropertyManager;
-		this.authorFactory = authorFactory;
-		this.contactManager = contactManager;
-		this.identityManager = identityManager;
-		this.introductionGroupFactory = introductionGroupFactory;
-	}
-
-	public BdfDictionary initialize(Transaction txn, GroupId groupId,
-			BdfDictionary message) throws DbException, FormatException {
-
-		// create local message to keep engine state
-		long now = clock.currentTimeMillis();
-		Bytes salt = new Bytes(new byte[64]);
-		cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
-
-		Message localMsg = clientHelper.createMessage(
-				introductionGroupFactory.createLocalGroup().getId(), now,
-				BdfList.of(salt));
-		MessageId storageId = localMsg.getId();
-
-		// find out who is introducing us
-		BdfDictionary gd =
-				clientHelper.getGroupMetadataAsDictionary(txn, groupId);
-		ContactId introducerId =
-				new ContactId(gd.getLong(CONTACT).intValue());
-		Contact introducer = db.getContact(txn, introducerId);
-
-		BdfDictionary d = new BdfDictionary();
-		d.put(STORAGE_ID, storageId);
-		d.put(STATE, AWAIT_REQUEST.getValue());
-		d.put(ROLE, ROLE_INTRODUCEE);
-		d.put(GROUP_ID, groupId);
-		d.put(INTRODUCER, introducer.getAuthor().getName());
-		d.put(CONTACT_ID_1, introducer.getId().getInt());
-		d.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes());
-		d.put(NOT_OUR_RESPONSE, storageId);
-		d.put(ANSWERED, false);
-
-		// check if the contact we are introduced to does already exist
-		// TODO: Exchange author format version
-		AuthorId remoteAuthorId = authorFactory
-				.createAuthor(message.getString(NAME),
-						message.getRaw(PUBLIC_KEY)).getId();
-		boolean exists = contactManager.contactExists(txn, remoteAuthorId,
-				introducer.getLocalAuthorId());
-		d.put(EXISTS, exists);
-		d.put(REMOTE_AUTHOR_ID, remoteAuthorId);
-
-		// check if someone is trying to introduce us to ourselves
-		if (remoteAuthorId.equals(introducer.getLocalAuthorId())) {
-			LOG.warning("Received Introduction Request to Ourselves");
-			throw new FormatException();
-		}
-
-		// check if remote author is actually one of our other identities
-		boolean introducesOtherIdentity =
-				db.containsLocalAuthor(txn, remoteAuthorId);
-		d.put(REMOTE_AUTHOR_IS_US, introducesOtherIdentity);
-
-		// save local state to database
-		clientHelper.addLocalMessage(txn, localMsg, d, false);
-
-		return d;
-	}
-
-	public void incomingMessage(Transaction txn, BdfDictionary state,
-			BdfDictionary message) throws DbException, FormatException {
-
-		IntroduceeEngine engine = new IntroduceeEngine();
-		processStateUpdate(txn, message,
-				engine.onMessageReceived(state, message));
-	}
-
-	void acceptIntroduction(Transaction txn, BdfDictionary state,
-			long timestamp) throws DbException, FormatException {
-
-		// get data to connect and derive a shared secret later
-		long now = clock.currentTimeMillis();
-		KeyPair keyPair = cryptoComponent.generateAgreementKeyPair();
-		byte[] publicKey = keyPair.getPublic().getEncoded();
-		byte[] privateKey = keyPair.getPrivate().getEncoded();
-		Map<TransportId, TransportProperties> transportProperties =
-				transportPropertyManager.getLocalProperties(txn);
-		BdfDictionary tp = encodeTransportProperties(transportProperties);
-
-		// update session state for later
-		state.put(ACCEPT, true);
-		state.put(OUR_TIME, now);
-		state.put(OUR_PUBLIC_KEY, publicKey);
-		state.put(OUR_PRIVATE_KEY, privateKey);
-		state.put(OUR_TRANSPORT, tp);
-
-		// define action
-		BdfDictionary localAction = new BdfDictionary();
-		localAction.put(TYPE, TYPE_RESPONSE);
-		localAction.put(TRANSPORT, tp);
-		localAction.put(MESSAGE_TIME, timestamp);
-
-		// start engine and process its state update
-		IntroduceeEngine engine = new IntroduceeEngine();
-		processStateUpdate(txn, null, engine.onLocalAction(state, localAction));
-	}
-
-	void declineIntroduction(Transaction txn, BdfDictionary state,
-			long timestamp) throws DbException, FormatException {
-
-		// update session state
-		state.put(ACCEPT, false);
-
-		// define action
-		BdfDictionary localAction = new BdfDictionary();
-		localAction.put(TYPE, TYPE_RESPONSE);
-		localAction.put(MESSAGE_TIME, timestamp);
-
-		// start engine and process its state update
-		IntroduceeEngine engine = new IntroduceeEngine();
-		processStateUpdate(txn, null,
-				engine.onLocalAction(state, localAction));
-	}
-
-	private void processStateUpdate(Transaction txn,
-			@Nullable BdfDictionary msg,
-			IntroduceeEngine.StateUpdate<BdfDictionary, BdfDictionary> result)
-			throws DbException, FormatException {
-
-		// perform actions based on new local state
-		BdfDictionary followUpAction = performTasks(txn, result.localState);
-
-		// save new local state
-		MessageId storageId =
-				new MessageId(result.localState.getRaw(STORAGE_ID));
-		clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
-
-		// send messages
-		for (BdfDictionary d : result.toSend) {
-			messageSender.sendMessage(txn, d);
-		}
-
-		// broadcast events
-		for (Event event : result.toBroadcast) {
-			txn.attach(event);
-		}
-
-		// delete message
-		if (result.deleteMessage && msg != null) {
-			MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
-			if (LOG.isLoggable(INFO)) {
-				LOG.info("Deleting message with id " + messageId.hashCode());
-			}
-			db.deleteMessage(txn, messageId);
-			db.deleteMessageMetadata(txn, messageId);
-		}
-
-		// process follow up action at the end if available
-		if (followUpAction != null) {
-			IntroduceeEngine engine = new IntroduceeEngine();
-			processStateUpdate(txn, null,
-					engine.onLocalAction(result.localState, followUpAction));
-		}
-	}
-
-	@Nullable
-	private BdfDictionary performTasks(Transaction txn,
-			BdfDictionary localState) throws FormatException, DbException {
-
-		if (!localState.containsKey(TASK) || localState.get(TASK) == NULL_VALUE)
-			return null;
-
-		// remember task and remove it from localState
-		long task = localState.getLong(TASK);
-		localState.put(TASK, NULL_VALUE);
-
-		if (task == TASK_ADD_CONTACT) {
-			if (localState.getBoolean(EXISTS)) {
-				// we have this contact already, so do not perform actions
-				LOG.info("We have this contact already, do not add");
-				return null;
-			}
-
-			// figure out who takes which role by comparing public keys
-			byte[] ourPublicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
-			byte[] theirPublicKeyBytes = localState.getRaw(E_PUBLIC_KEY);
-			int comp = Bytes.COMPARATOR.compare(new Bytes(ourPublicKeyBytes),
-					new Bytes(theirPublicKeyBytes));
-			boolean alice = comp < 0;
-
-			// get our local author
-			LocalAuthor author = identityManager.getLocalAuthor(txn);
-
-			SecretKey secretKey;
-			byte[] ourPrivateKeyBytes = localState.getRaw(OUR_PRIVATE_KEY);
-			try {
-				// derive secret master key
-				secretKey = deriveSecretKey(ourPublicKeyBytes,
-						ourPrivateKeyBytes, alice, theirPublicKeyBytes);
-				// derive MAC keys and nonces, sign our nonce and calculate MAC
-				deriveMacKeysAndNonces(localState, author, secretKey, alice);
-			} catch (GeneralSecurityException e) {
-				// we can not continue without the signature
-				throw new DbException(e);
-			}
-
-			LOG.info("Adding contact in inactive state");
-
-			// The agreed timestamp is the minimum of the peers' timestamps
-			long ourTime = localState.getLong(OUR_TIME);
-			long theirTime = localState.getLong(TIME);
-			long timestamp = Math.min(ourTime, theirTime);
-
-			// Add the contact to the database as inactive
-			// TODO: Exchange author format version
-			Author remoteAuthor = authorFactory
-					.createAuthor(localState.getString(NAME),
-							localState.getRaw(PUBLIC_KEY));
-			ContactId contactId = contactManager
-					.addContact(txn, remoteAuthor, author.getId(), secretKey,
-							timestamp, alice, false, false);
-
-			// Update local state with ContactId, so we know what to activate
-			localState.put(ADDED_CONTACT_ID, contactId.getInt());
-
-			// let the transport manager know how to connect to the contact
-			Map<TransportId, TransportProperties> transportProperties =
-					parseTransportProperties(localState);
-			transportPropertyManager.addRemoteProperties(txn, contactId,
-					transportProperties);
-
-			// delete the ephemeral private key by overwriting with NULL value
-			// this ensures future ephemeral keys can not be recovered when
-			// this device should gets compromised
-			localState.put(OUR_PRIVATE_KEY, NULL_VALUE);
-
-			// define next action: Send ACK
-			BdfDictionary localAction = new BdfDictionary();
-			localAction.put(TYPE, TYPE_ACK);
-
-			// return follow up action to start engine
-			// and process its state update again
-			return localAction;
-		}
-
-		// we sent and received an ACK, so activate contact
-		if (task == TASK_ACTIVATE_CONTACT) {
-			if (!localState.getBoolean(EXISTS) &&
-					localState.containsKey(ADDED_CONTACT_ID)) {
-				try {
-					LOG.info("Verifying Signature...");
-					verifySignature(localState);
-					LOG.info("Verifying MAC...");
-					verifyMac(localState);
-				} catch (GeneralSecurityException e) {
-					throw new DbException(e);
-				}
-
-				LOG.info("Activating Contact...");
-
-				ContactId contactId = new ContactId(
-						localState.getLong(ADDED_CONTACT_ID).intValue());
-
-				// activate and show contact in contact list
-				contactManager.setContactActive(txn, contactId, true);
-
-				// broadcast event informing of successful introduction
-				Contact contact = db.getContact(txn, contactId);
-				Event event = new IntroductionSucceededEvent(contact);
-				txn.attach(event);
-			} else {
-				LOG.info(
-						"We must have had this contact already, not activating...");
-			}
-		}
-
-		// we need to abort the protocol, clean up what has been done
-		if (task == TASK_ABORT) {
-			if (localState.containsKey(ADDED_CONTACT_ID)) {
-				LOG.info("Deleting added contact due to abort...");
-				ContactId contactId = new ContactId(
-						localState.getLong(ADDED_CONTACT_ID).intValue());
-				contactManager.removeContact(txn, contactId);
-			}
-		}
-		return null;
-	}
-
-	private SecretKey deriveSecretKey(byte[] ourPublicKeyBytes,
-			byte[] ourPrivateKeyBytes, boolean alice,
-			byte[] theirPublicKeyBytes) throws GeneralSecurityException {
-		// parse the local ephemeral key pair
-		KeyParser keyParser = cryptoComponent.getAgreementKeyParser();
-		PublicKey ourPublicKey;
-		PrivateKey ourPrivateKey;
-		try {
-			ourPublicKey = keyParser.parsePublicKey(ourPublicKeyBytes);
-			ourPrivateKey = keyParser.parsePrivateKey(ourPrivateKeyBytes);
-		} catch (GeneralSecurityException e) {
-			if (LOG.isLoggable(WARNING)) {
-				LOG.log(WARNING, e.toString(), e);
-			}
-			throw new RuntimeException("Our own ephemeral key is invalid");
-		}
-		KeyPair ourKeyPair = new KeyPair(ourPublicKey, ourPrivateKey);
-		PublicKey theirPublicKey =
-				keyParser.parsePublicKey(theirPublicKeyBytes);
-
-		// The shared secret is derived from the local ephemeral key pair
-		// and the remote ephemeral public key
-		byte[][] inputs = {
-				new byte[] {CLIENT_VERSION},
-				alice ? ourPublicKeyBytes : theirPublicKeyBytes,
-				alice ? theirPublicKeyBytes : ourPublicKeyBytes
-		};
-		return cryptoComponent.deriveSharedSecret(SHARED_SECRET_LABEL,
-				theirPublicKey, ourKeyPair, inputs);
-	}
-
-	/**
-	 * Derives nonces, signs our nonce and calculates MAC
-	 * <p>
-	 * Derives two nonces and two MAC keys from the shared secret key.
-	 * The other introducee's nonce and MAC key are added to the localState.
-	 * <p>
-	 * Our nonce is signed with the local author's long-term private key.
-	 * The signature is added to the localState.
-	 * <p>
-	 * Calculates a MAC and stores it in the localState.
-	 */
-	private void deriveMacKeysAndNonces(BdfDictionary localState,
-			LocalAuthor author, SecretKey secretKey, boolean alice)
-			throws FormatException, GeneralSecurityException {
-		// Derive two nonces and two MAC keys from the shared secret key
-		String ourNonceLabel = alice ? ALICE_NONCE_LABEL : BOB_NONCE_LABEL;
-		String theirNonceLabel = alice ? BOB_NONCE_LABEL : ALICE_NONCE_LABEL;
-		byte[] ourNonce = cryptoComponent.mac(ourNonceLabel, secretKey);
-		byte[] theirNonce = cryptoComponent.mac(theirNonceLabel, secretKey);
-		String ourKeyLabel = alice ? ALICE_MAC_KEY_LABEL : BOB_MAC_KEY_LABEL;
-		String theirKeyLabel = alice ? BOB_MAC_KEY_LABEL : ALICE_MAC_KEY_LABEL;
-		SecretKey ourMacKey = cryptoComponent.deriveKey(ourKeyLabel, secretKey);
-		SecretKey theirMacKey =
-				cryptoComponent.deriveKey(theirKeyLabel, secretKey);
-
-		// Save the other nonce and MAC key for the verification
-		localState.put(NONCE, theirNonce);
-		localState.put(MAC_KEY, theirMacKey.getBytes());
-
-		// Sign our nonce with our long-term identity public key
-		byte[] sig = cryptoComponent.sign(SIGNING_LABEL, ourNonce,
-				author.getPrivateKey());
-
-		// Calculate a MAC over identity public key, ephemeral public key,
-		// transport properties and timestamp.
-		byte[] publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
-		BdfDictionary tp = localState.getDictionary(OUR_TRANSPORT);
-		long ourTime = localState.getLong(OUR_TIME);
-		BdfList toMacList = BdfList.of(author.getPublicKey(),
-				publicKeyBytes, tp, ourTime);
-		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] mac = cryptoComponent.mac(MAC_LABEL, ourMacKey, toMac);
-
-		// Add MAC and signature to localState, so it can be included in ACK
-		localState.put(OUR_MAC, mac);
-		localState.put(OUR_SIGNATURE, sig);
-	}
-
-	void verifySignature(BdfDictionary localState)
-			throws FormatException, GeneralSecurityException {
-		byte[] nonce = localState.getRaw(NONCE);
-		byte[] sig = localState.getRaw(SIGNATURE);
-		byte[] key = localState.getRaw(PUBLIC_KEY);
-
-		// Verify the signature
-		if (!cryptoComponent.verifySignature(sig, SIGNING_LABEL, nonce, key)) {
-			LOG.warning("Invalid nonce signature in ACK");
-			throw new GeneralSecurityException();
-		}
-	}
-
-	void verifyMac(BdfDictionary localState)
-			throws FormatException, GeneralSecurityException {
-		// get MAC and MAC key from session state
-		byte[] mac = localState.getRaw(MAC);
-		byte[] macKeyBytes = localState.getRaw(MAC_KEY);
-		SecretKey macKey = new SecretKey(macKeyBytes);
-
-		// get MAC data and calculate a new MAC with stored key
-		byte[] pubKey = localState.getRaw(PUBLIC_KEY);
-		byte[] ePubKey = localState.getRaw(E_PUBLIC_KEY);
-		BdfDictionary tp = localState.getDictionary(TRANSPORT);
-		long timestamp = localState.getLong(TIME);
-		BdfList toMacList = BdfList.of(pubKey, ePubKey, tp, timestamp);
-		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] calculatedMac = cryptoComponent.mac(MAC_LABEL, macKey, toMac);
-		if (!Arrays.equals(mac, calculatedMac)) {
-			LOG.warning("Received ACK with invalid MAC");
-			throw new GeneralSecurityException();
-		}
-	}
-
-	public void abort(Transaction txn, BdfDictionary state) {
-		IntroduceeEngine engine = new IntroduceeEngine();
-		BdfDictionary localAction = new BdfDictionary();
-		localAction.put(TYPE, TYPE_ABORT);
-		try {
-			processStateUpdate(txn, null,
-					engine.onLocalAction(state, localAction));
-		} catch (DbException | IOException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	private BdfDictionary encodeTransportProperties(
-			Map<TransportId, TransportProperties> map) {
-
-		BdfDictionary d = new BdfDictionary();
-		for (Map.Entry<TransportId, TransportProperties> e : map.entrySet()) {
-			d.put(e.getKey().getString(), e.getValue());
-		}
-		return d;
-	}
-
-	private Map<TransportId, TransportProperties> parseTransportProperties(
-			BdfDictionary d) throws FormatException {
-
-		Map<TransportId, TransportProperties> tpMap = new HashMap<>();
-		BdfDictionary tpMapDict = d.getDictionary(TRANSPORT);
-		for (String key : tpMapDict.keySet()) {
-			TransportId transportId = new TransportId(key);
-			TransportProperties transportProperties = new TransportProperties();
-			BdfDictionary tpDict = tpMapDict.getDictionary(key);
-			for (String tkey : tpDict.keySet()) {
-				transportProperties.put(tkey, tpDict.getString(tkey));
-			}
-			tpMap.put(transportId, transportProperties);
-		}
-		return tpMap;
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
similarity index 89%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index 1efb6919bb10ed2a4759ef36565fbf61d39a29bd..e463dd39a79626f5e6588bf05a84a9f4f17c8f07 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -28,11 +28,12 @@ import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.IntroductionRequest;
-import org.briarproject.briar.api.introduction2.IntroductionResponse;
-import org.briarproject.briar.api.introduction2.event.IntroductionRequestReceivedEvent;
-import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent;
-import org.briarproject.briar.api.introduction2.event.IntroductionSucceededEvent;
+import org.briarproject.briar.api.introduction.IntroductionRequest;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
+import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
+import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
+import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent;
 
 import java.security.GeneralSecurityException;
 import java.util.Map;
@@ -41,11 +42,11 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
-import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_AUTH;
-import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_RESPONSES;
-import static org.briarproject.briar.introduction2.IntroduceeState.LOCAL_ACCEPTED;
-import static org.briarproject.briar.introduction2.IntroduceeState.REMOTE_ACCEPTED;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_AUTH;
+import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES;
+import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_ACCEPTED;
 
 @Immutable
 @NotNullByDefault
@@ -145,10 +146,11 @@ class IntroduceeProtocolEngine
 			IntroduceeSession session, AcceptMessage m)
 			throws DbException, FormatException {
 		switch (session.getState()) {
+			case START:
+				return onRemoteResponseInStart(txn, session, m);
 			case AWAIT_RESPONSES:
 			case LOCAL_ACCEPTED:
 				return onRemoteAccept(txn, session, m);
-			case START:
 			case LOCAL_DECLINED:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
@@ -165,7 +167,7 @@ class IntroduceeProtocolEngine
 			throws DbException, FormatException {
 		switch (session.getState()) {
 			case START:
-				return session; // Ignore in the START state
+				return onRemoteResponseInStart(txn, session, m);
 			case AWAIT_RESPONSES:
 			case LOCAL_DECLINED:
 			case LOCAL_ACCEPTED:
@@ -322,18 +324,6 @@ class IntroduceeProtocolEngine
 		if (isInvalidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
 
-		// Broadcast IntroductionResponseReceivedEvent
-		Contact c = contactManager.getContact(s.getIntroducer().getId(),
-				identityManager.getLocalAuthor(txn).getId());
-		IntroductionResponse request =
-				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
-						m.getGroupId(), INTRODUCEE, m.getTimestamp(), false,
-						false, false, false, s.getRemoteAuthor().getName(),
-						true);
-		IntroductionResponseReceivedEvent e =
-				new IntroductionResponseReceivedEvent(c.getId(), request);
-		txn.attach(e);
-
 		// Determine next state
 		IntroduceeState state =
 				s.getState() == AWAIT_RESPONSES ? REMOTE_ACCEPTED : AWAIT_AUTH;
@@ -356,12 +346,46 @@ class IntroduceeProtocolEngine
 		if (isInvalidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
 
+		// Mark the request visible in the UI
+		markMessageVisibleInUi(txn, m.getMessageId());
+
+		// Track the incoming message
+		messageTracker
+				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
+
+		// Broadcast IntroductionResponseReceivedEvent
+		Contact c = contactManager.getContact(txn, s.getIntroducer().getId(),
+				identityManager.getLocalAuthor(txn).getId());
+		IntroductionResponse request =
+				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
+						m.getGroupId(), INTRODUCEE, m.getTimestamp(), false,
+						false, false, false, s.getRemoteAuthor().getName(),
+						false);
+		IntroductionResponseReceivedEvent e =
+				new IntroductionResponseReceivedEvent(c.getId(), request);
+		txn.attach(e);
+
 		// Move back to START state
 		return IntroduceeSession
 				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
 						m.getMessageId());
 	}
 
+	private IntroduceeSession onRemoteResponseInStart(Transaction txn,
+			IntroduceeSession s, AbstractIntroductionMessage m) throws DbException {
+		// The timestamp must be higher than the last request message
+		if (m.getTimestamp() <= s.getRequestTimestamp())
+			return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+
+		// Stay in START state
+		return IntroduceeSession
+				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
+						m.getMessageId());
+	}
+
 	private IntroduceeSession onLocalAuth(Transaction txn, IntroduceeSession s)
 			throws DbException {
 		boolean alice = isAlice(txn, s);
@@ -459,6 +483,9 @@ class IntroduceeProtocolEngine
 		if (requestId == null) throw new IllegalStateException();
 		markRequestUnavailableToAnswer(txn, requestId);
 
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
 		// Reset the session back to initial state
 		return IntroduceeSession
 				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
@@ -475,6 +502,9 @@ class IntroduceeProtocolEngine
 		// Send an ABORT message
 		Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
 
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
 		// Reset the session back to initial state
 		return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(),
 				s.getLastRemoteMessageId());
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
similarity index 95%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
index 61e3014981034076c1a8cb92573af37f1358664f..1b44452d972b1407591d6c716ff4788f805d771a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.identity.Author;
@@ -10,16 +10,16 @@ import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.Role;
+import org.briarproject.briar.api.introduction.Role;
 
 import java.util.Map;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_ACTIVATE;
-import static org.briarproject.briar.introduction2.IntroduceeState.START;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
+import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_ACTIVATE;
+import static org.briarproject.briar.introduction.IntroduceeState.START;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
 
 @Immutable
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
similarity index 93%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
index 4a901752df07d53d489945ca8a4229869e7ae815..7a54abde8d0938a4201dd307013264431cc2e3ef 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java
deleted file mode 100644
index df364b34ff0f7cdc39e4c607678ae6cdb1e64398..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java
+++ /dev/null
@@ -1,370 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.client.ProtocolEngine;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.IntroducerAction;
-import org.briarproject.briar.api.introduction.IntroducerProtocolState;
-import org.briarproject.briar.api.introduction.IntroductionResponse;
-import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.logging.Logger;
-
-import javax.annotation.concurrent.Immutable;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_ABORT;
-import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_1;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_2;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_1;
-import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_2;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACKS;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACK_1;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACK_2;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_1;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_2;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.ERROR;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.FINISHED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-
-@Immutable
-@NotNullByDefault
-class IntroducerEngine
-		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
-
-	private static final Logger LOG =
-			Logger.getLogger(IntroducerEngine.class.getName());
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
-			BdfDictionary localState, BdfDictionary localAction) {
-
-		try {
-			IntroducerProtocolState currentState =
-					getState(localState.getLong(STATE));
-			int type = localAction.getLong(TYPE).intValue();
-			IntroducerAction action = IntroducerAction.getLocal(type);
-			IntroducerProtocolState nextState = currentState.next(action);
-
-			if (action == LOCAL_ABORT && currentState != ERROR) {
-				return abortSession(currentState, localState);
-			}
-
-			if (nextState == ERROR) {
-				if (LOG.isLoggable(WARNING)) {
-					LOG.warning("Error: Invalid action in state " +
-							currentState.name());
-				}
-				return noUpdate(localState);
-			}
-
-			localState.put(STATE, nextState.getValue());
-			if (action == LOCAL_REQUEST) {
-				// create the introduction requests for both contacts
-				List<BdfDictionary> messages = new ArrayList<>(2);
-				BdfDictionary msg1 = new BdfDictionary();
-				msg1.put(TYPE, TYPE_REQUEST);
-				msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
-				msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
-				msg1.put(NAME, localState.getString(CONTACT_2));
-				msg1.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY2));
-				if (localAction.containsKey(MSG)) {
-					msg1.put(MSG, localAction.getString(MSG));
-				}
-				msg1.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
-				messages.add(msg1);
-				logLocalAction(currentState, localState);
-				BdfDictionary msg2 = new BdfDictionary();
-				msg2.put(TYPE, TYPE_REQUEST);
-				msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
-				msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
-				msg2.put(NAME, localState.getString(CONTACT_1));
-				msg2.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY1));
-				if (localAction.containsKey(MSG)) {
-					msg2.put(MSG, localAction.getString(MSG));
-				}
-				msg2.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
-				messages.add(msg2);
-				logLocalAction(currentState, localState);
-
-				List<Event> events = Collections.emptyList();
-				return new StateUpdate<>(false, false,
-						localState, messages, events);
-			} else {
-				throw new IllegalArgumentException("Unknown Local Action");
-			}
-		} catch (FormatException e) {
-			throw new IllegalArgumentException(e);
-		}
-	}
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
-			BdfDictionary localState, BdfDictionary msg) {
-
-		try {
-			IntroducerProtocolState currentState =
-					getState(localState.getLong(STATE));
-			int type = msg.getLong(TYPE).intValue();
-			boolean one = isContact1(localState, msg);
-			IntroducerAction action = IntroducerAction.getRemote(type, one);
-			IntroducerProtocolState nextState = currentState.next(action);
-
-			logMessageReceived(currentState, nextState, localState, type, msg);
-
-			if (nextState == ERROR) {
-				if (currentState != ERROR) {
-					return abortSession(currentState, localState);
-				} else {
-					return noUpdate(localState);
-				}
-			}
-
-			List<BdfDictionary> messages;
-			List<Event> events;
-
-			// we have sent our requests and just got the 1st or 2nd response
-			if (currentState == AWAIT_RESPONSES ||
-					currentState == AWAIT_RESPONSE_1 ||
-					currentState == AWAIT_RESPONSE_2) {
-				// update next state based on message content
-				action = IntroducerAction
-						.getRemote(type, one, msg.getBoolean(ACCEPT));
-				nextState = currentState.next(action);
-				localState.put(STATE, nextState.getValue());
-				if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
-				else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
-
-				messages = forwardMessage(localState, msg);
-				events = Collections.singletonList(getEvent(localState, msg));
-			}
-			// we have forwarded both responses and now received the 1st or 2nd ACK
-			else if (currentState == AWAIT_ACKS ||
-					currentState == AWAIT_ACK_1 ||
-					currentState == AWAIT_ACK_2) {
-				localState.put(STATE, nextState.getValue());
-				messages = forwardMessage(localState, msg);
-				events = Collections.emptyList();
-			}
-			// we probably received a response while already being FINISHED
-			else if (currentState == FINISHED) {
-				// if it was a response store it to be found later
-				if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) {
-					localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
-					messages = Collections.emptyList();
-					events = Collections
-							.singletonList(getEvent(localState, msg));
-				} else if (action == REMOTE_ACCEPT_2 ||
-						action == REMOTE_DECLINE_2) {
-					localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
-					messages = Collections.emptyList();
-					events = Collections
-							.singletonList(getEvent(localState, msg));
-				} else return noUpdate(localState);
-			} else {
-				throw new IllegalArgumentException("Bad state");
-			}
-			return new StateUpdate<>(false, false,
-					localState, messages, events);
-		} catch (FormatException e) {
-			throw new IllegalArgumentException(e);
-		}
-	}
-
-	private void logLocalAction(IntroducerProtocolState state,
-			BdfDictionary localState) {
-
-		if (!LOG.isLoggable(INFO)) return;
-		try {
-			LOG.info("Sending introduction request in state " + state.name());
-			LOG.info("Moving on to state " +
-					getState(localState.getLong(STATE)).name());
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	private void logMessageReceived(IntroducerProtocolState currentState,
-			IntroducerProtocolState nextState,
-			BdfDictionary localState, int type, BdfDictionary msg) {
-
-		if (!LOG.isLoggable(INFO)) return;
-
-		String t = "unknown";
-		if (type == TYPE_REQUEST) t = "Introduction";
-		else if (type == TYPE_RESPONSE) t = "Response";
-		else if (type == TYPE_ACK) t = "ACK";
-		else if (type == TYPE_ABORT) t = "Abort";
-
-		LOG.info("Received " + t + " in state " + currentState.name());
-		LOG.info("Moving on to state " + nextState.name());
-	}
-
-	private List<BdfDictionary> forwardMessage(BdfDictionary localState,
-			BdfDictionary message) throws FormatException {
-
-		// clone the message here, because we still need the original
-		BdfDictionary msg = (BdfDictionary) message.clone();
-		if (isContact1(localState, msg)) {
-			msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
-		} else {
-			msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
-		}
-
-		return Collections.singletonList(msg);
-	}
-
-	@Override
-	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
-			BdfDictionary localState, BdfDictionary delivered) {
-		try {
-			return noUpdate(localState);
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			return null;
-		}
-	}
-
-	private IntroducerProtocolState getState(Long state) {
-		return IntroducerProtocolState.fromValue(state.intValue());
-	}
-
-	private Event getEvent(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		ContactId contactId =
-				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
-		AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1));
-		if (Arrays
-				.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
-			contactId =
-					new ContactId(localState.getLong(CONTACT_ID_2).intValue());
-			authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2));
-		}
-
-		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
-		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
-		GroupId groupId = new GroupId(msg.getRaw(GROUP_ID));
-		long time = msg.getLong(MESSAGE_TIME);
-		String name = getOtherContact(localState, msg);
-		boolean accept = msg.getBoolean(ACCEPT);
-
-		IntroductionResponse ir =
-				new IntroductionResponse(sessionId, messageId, groupId,
-						ROLE_INTRODUCER, time, false, false, false, false,
-						authorId, name, accept);
-		return new IntroductionResponseReceivedEvent(contactId, ir);
-	}
-
-	private boolean isContact1(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		byte[] group = msg.getRaw(GROUP_ID);
-		byte[] group1 = localState.getRaw(GROUP_ID_1);
-		byte[] group2 = localState.getRaw(GROUP_ID_2);
-
-		if (Arrays.equals(group, group1)) {
-			return true;
-		} else if (Arrays.equals(group, group2)) {
-			return false;
-		} else {
-			throw new FormatException();
-		}
-	}
-
-	private String getOtherContact(BdfDictionary localState, BdfDictionary msg)
-			throws FormatException {
-
-		String to = localState.getString(CONTACT_2);
-		if (Arrays
-				.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
-			to = localState.getString(CONTACT_1);
-		}
-		return to;
-	}
-
-	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
-			IntroducerProtocolState currentState, BdfDictionary localState)
-			throws FormatException {
-
-		if (LOG.isLoggable(WARNING))
-			LOG.warning("Aborting protocol session in state " +
-					currentState.name());
-
-		localState.put(STATE, ERROR.getValue());
-		List<BdfDictionary> messages = new ArrayList<>(2);
-		BdfDictionary msg1 = new BdfDictionary();
-		msg1.put(TYPE, TYPE_ABORT);
-		msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
-		msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
-		messages.add(msg1);
-		BdfDictionary msg2 = new BdfDictionary();
-		msg2.put(TYPE, TYPE_ABORT);
-		msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
-		msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
-		messages.add(msg2);
-
-		// send one abort event per contact
-		List<Event> events = new ArrayList<>(2);
-		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
-		ContactId contactId1 =
-				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
-		ContactId contactId2 =
-				new ContactId(localState.getLong(CONTACT_ID_2).intValue());
-		Event event1 = new IntroductionAbortedEvent(contactId1, sessionId);
-		events.add(event1);
-		Event event2 = new IntroductionAbortedEvent(contactId2, sessionId);
-		events.add(event2);
-
-		return new StateUpdate<>(false, false, localState, messages, events);
-	}
-
-	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
-			BdfDictionary localState) throws FormatException {
-
-		return new StateUpdate<>(false, false, localState,
-				Collections.<BdfDictionary>emptyList(),
-				Collections.emptyList());
-	}
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java
deleted file mode 100644
index b24109396c7f634c8668ff9aea64dca16750cca8..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.Bytes;
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.bramble.util.StringUtils;
-
-import java.io.IOException;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-import javax.inject.Inject;
-
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-
-@Immutable
-@NotNullByDefault
-class IntroducerManager {
-
-	private static final Logger LOG =
-			Logger.getLogger(IntroducerManager.class.getName());
-
-	private final MessageSender messageSender;
-	private final ClientHelper clientHelper;
-	private final Clock clock;
-	private final CryptoComponent cryptoComponent;
-	private final IntroductionGroupFactory introductionGroupFactory;
-
-	@Inject
-	IntroducerManager(MessageSender messageSender, ClientHelper clientHelper,
-			Clock clock, CryptoComponent cryptoComponent,
-			IntroductionGroupFactory introductionGroupFactory) {
-
-		this.messageSender = messageSender;
-		this.clientHelper = clientHelper;
-		this.clock = clock;
-		this.cryptoComponent = cryptoComponent;
-		this.introductionGroupFactory = introductionGroupFactory;
-	}
-
-	public BdfDictionary initialize(Transaction txn, Contact c1, Contact c2)
-			throws FormatException, DbException {
-
-		// create local message to keep engine state
-		long now = clock.currentTimeMillis();
-		Bytes salt = new Bytes(new byte[64]);
-		cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
-
-		Message m = clientHelper.createMessage(
-				introductionGroupFactory.createLocalGroup().getId(), now,
-				BdfList.of(salt));
-		MessageId sessionId = m.getId();
-
-		Group g1 = introductionGroupFactory.createIntroductionGroup(c1);
-		Group g2 = introductionGroupFactory.createIntroductionGroup(c2);
-
-		BdfDictionary d = new BdfDictionary();
-		d.put(SESSION_ID, sessionId);
-		d.put(STORAGE_ID, sessionId);
-		d.put(STATE, PREPARE_REQUESTS.getValue());
-		d.put(ROLE, ROLE_INTRODUCER);
-		d.put(GROUP_ID_1, g1.getId());
-		d.put(GROUP_ID_2, g2.getId());
-		d.put(CONTACT_1, c1.getAuthor().getName());
-		d.put(CONTACT_2, c2.getAuthor().getName());
-		d.put(CONTACT_ID_1, c1.getId().getInt());
-		d.put(CONTACT_ID_2, c2.getId().getInt());
-		d.put(AUTHOR_ID_1, c1.getAuthor().getId());
-		d.put(AUTHOR_ID_2, c2.getAuthor().getId());
-
-		// save local state to database
-		clientHelper.addLocalMessage(txn, m, d, false);
-
-		return d;
-	}
-
-	void makeIntroduction(Transaction txn, Contact c1, Contact c2,
-			@Nullable String msg, long timestamp)
-			throws DbException, FormatException {
-
-		// TODO check for existing session with those contacts?
-		//      deny new introduction under which conditions?
-
-		// initialize engine state
-		BdfDictionary localState = initialize(txn, c1, c2);
-
-		// define action
-		BdfDictionary localAction = new BdfDictionary();
-		localAction.put(TYPE, TYPE_REQUEST);
-		if (!StringUtils.isNullOrEmpty(msg)) {
-			int msgLength = StringUtils.toUtf8(msg).length;
-			if (msgLength > MAX_INTRODUCTION_MESSAGE_LENGTH)
-				throw new IllegalArgumentException();
-			localAction.put(MSG, msg);
-		}
-		localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey());
-		localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey());
-		localAction.put(MESSAGE_TIME, timestamp);
-
-		// start engine and process its state update
-		IntroducerEngine engine = new IntroducerEngine();
-		processStateUpdate(txn,
-				engine.onLocalAction(localState, localAction));
-	}
-
-	public void incomingMessage(Transaction txn, BdfDictionary state,
-			BdfDictionary message) throws DbException, FormatException {
-
-		IntroducerEngine engine = new IntroducerEngine();
-		processStateUpdate(txn,
-				engine.onMessageReceived(state, message));
-	}
-
-	private void processStateUpdate(Transaction txn,
-			IntroducerEngine.StateUpdate<BdfDictionary, BdfDictionary>
-					result) throws DbException, FormatException {
-
-		// save new local state
-		MessageId storageId =
-				new MessageId(result.localState.getRaw(STORAGE_ID));
-		clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
-
-		// send messages
-		for (BdfDictionary d : result.toSend) {
-			messageSender.sendMessage(txn, d);
-		}
-
-		// broadcast events
-		for (Event event : result.toBroadcast) {
-			txn.attach(event);
-		}
-	}
-
-	public void abort(Transaction txn, BdfDictionary state) {
-		IntroducerEngine engine = new IntroducerEngine();
-		BdfDictionary localAction = new BdfDictionary();
-		localAction.put(TYPE, TYPE_ABORT);
-		try {
-			processStateUpdate(txn,
-					engine.onLocalAction(state, localAction));
-		} catch (DbException | IOException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
similarity index 78%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
index a65f18301a0a31c072f793ccace5de91f9e81645..044b7ccd2cfd46927b951f44d8254dfeb71eab00 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -17,9 +18,10 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.introduction2.IntroductionResponse;
-import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent;
-import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
+import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
+import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
 import java.util.Map;
 
@@ -27,17 +29,17 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATES;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_A;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_B;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTHS;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_A;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_B;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSES;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_A;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_B;
-import static org.briarproject.briar.introduction2.IntroducerState.START;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATES;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_A;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_B;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTHS;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTH_A;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTH_B;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSES;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_A;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_B;
+import static org.briarproject.briar.introduction.IntroducerState.START;
 
 @Immutable
 @NotNullByDefault
@@ -94,6 +96,11 @@ class IntroducerProtocolEngine
 		throw new UnsupportedOperationException(); // Invalid in this role
 	}
 
+	IntroducerSession onAbortAction(Transaction txn, IntroducerSession s)
+			throws DbException, FormatException {
+		return abort(txn, s);
+	}
+
 	@Override
 	public IntroducerSession onRequestMessage(Transaction txn,
 			IntroducerSession s, RequestMessage m)
@@ -111,8 +118,7 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSE_B:
 				return onRemoteAccept(txn, s, m);
 			case START:
-				// TODO check and update lastRemoteMsgId?
-				return s; // Ignored in this state
+				return onRemoteResponseInStart(txn, s, m);
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -135,8 +141,7 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSE_B:
 				return onRemoteDecline(txn, s, m);
 			case START:
-				// TODO check and update lastRemoteMsgId?
-				return s; // Ignored in this state
+				return onRemoteResponseInStart(txn, s, m);
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -253,14 +258,16 @@ class IntroducerProtocolEngine
 			if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A;
 			introducee1 = new Introducee(s.getIntroducee1(), sent);
 			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-			c = contactManager.getContact(s.getIntroducee2().author.getId(),
-					identityManager.getLocalAuthor(txn).getId());
+			c = contactManager
+					.getContact(txn, s.getIntroducee2().author.getId(),
+							identityManager.getLocalAuthor(txn).getId());
 		} else if (i.equals(s.getIntroducee2())) {
 			if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B;
 			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
 			introducee2 = new Introducee(s.getIntroducee2(), sent);
-			c = contactManager.getContact(s.getIntroducee1().author.getId(),
-					identityManager.getLocalAuthor(txn).getId());
+			c = contactManager
+					.getContact(txn, s.getIntroducee1().author.getId(),
+							identityManager.getLocalAuthor(txn).getId());
 		} else throw new AssertionError();
 
 		// Broadcast IntroductionResponseReceivedEvent
@@ -296,22 +303,23 @@ class IntroducerProtocolEngine
 		Introducee i = getOtherIntroducee(s, m.getGroupId());
 		long timestamp = getLocalTimestamp(s, i);
 		Message sent = sendDeclineMessage(txn, i, timestamp, false);
-		// Track the message
-		messageTracker.trackOutgoingMessage(txn, sent);
 
 		// Move to the START state
 		Introducee introducee1, introducee2;
+		AuthorId localAuthorId =identityManager.getLocalAuthor(txn).getId();
 		Contact c;
 		if (i.equals(s.getIntroducee1())) {
 			introducee1 = new Introducee(s.getIntroducee1(), sent);
 			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-			c = contactManager.getContact(s.getIntroducee2().author.getId(),
-					identityManager.getLocalAuthor(txn).getId());
+			c = contactManager
+					.getContact(txn, s.getIntroducee2().author.getId(),
+							localAuthorId);
 		} else if (i.equals(s.getIntroducee2())) {
 			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
 			introducee2 = new Introducee(s.getIntroducee2(), sent);
-			c = contactManager.getContact(s.getIntroducee2().author.getId(),
-					identityManager.getLocalAuthor(txn).getId());
+			c = contactManager
+					.getContact(txn, s.getIntroducee1().author.getId(),
+							localAuthorId);
 		} else throw new AssertionError();
 
 		// Broadcast IntroductionResponseReceivedEvent
@@ -327,6 +335,54 @@ class IntroducerProtocolEngine
 				s.getRequestTimestamp(), introducee1, introducee2);
 	}
 
+	private IntroducerSession onRemoteResponseInStart(Transaction txn,
+			IntroducerSession s, AbstractIntroductionMessage m)
+			throws DbException, FormatException {
+		// The timestamp must be higher than the last request message
+		if (m.getTimestamp() <= s.getRequestTimestamp())
+			return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
+			return abort(txn, s);
+
+		// Mark the response visible in the UI
+		markMessageVisibleInUi(txn, m.getMessageId());
+		// Track the incoming message
+		messageTracker
+				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
+
+		Introducee i = getIntroducee(s, m.getGroupId());
+		Introducee introducee1, introducee2;
+		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
+		Contact c;
+		if (i.equals(s.getIntroducee1())) {
+			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
+			introducee2 = s.getIntroducee2();
+			c = contactManager
+					.getContact(txn, s.getIntroducee1().author.getId(),
+							localAuthorId);
+		} else if (i.equals(s.getIntroducee2())) {
+			introducee1 = s.getIntroducee1();
+			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
+			c = contactManager
+					.getContact(txn, s.getIntroducee2().author.getId(),
+							localAuthorId);
+		} else throw new AssertionError();
+
+		// Broadcast IntroductionResponseReceivedEvent
+		IntroductionResponse request =
+				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
+						m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
+						false, false, false, c.getAuthor().getName(),
+						m instanceof AcceptMessage);
+		IntroductionResponseReceivedEvent e =
+				new IntroductionResponseReceivedEvent(c.getId(), request);
+		txn.attach(e);
+
+		return new IntroducerSession(s.getSessionId(), START,
+				s.getRequestTimestamp(), introducee1, introducee2);
+	}
+
 	private IntroducerSession onRemoteAuth(Transaction txn,
 			IntroducerSession s, AuthMessage m)
 			throws DbException, FormatException {
@@ -395,6 +451,9 @@ class IntroducerProtocolEngine
 		long timestamp = getLocalTimestamp(s, i);
 		Message sent = sendAbortMessage(txn, i, timestamp);
 
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
 		// Reset the session back to initial state
 		Introducee introducee1, introducee2;
 		if (i.equals(s.getIntroducee1())) {
@@ -412,6 +471,10 @@ class IntroducerProtocolEngine
 			IntroducerSession s) throws DbException, FormatException {
 		// Mark any REQUEST messages in the session unavailable to answer
 		markRequestsUnavailableToAnswer(txn, s);
+
+		// Broadcast abort event for testing
+		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
+
 		// Send an ABORT message to both introducees
 		long timestamp1 = getLocalTimestamp(s, s.getIntroducee1());
 		Message sent1 = sendAbortMessage(txn, s.getIntroducee1(), timestamp1);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
similarity index 94%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
index cbfc53923e47f22b4000f46c273b6a1fb4e88f36..906947969224f19ab084dfc52bec5f52da333e22 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -6,12 +6,12 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.Role;
+import org.briarproject.briar.api.introduction.Role;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 
 @Immutable
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
similarity index 94%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
index 43e51d96af8e2b77abb57d92951f07f0cc2247f3..99c3fbf86c755ed80104ab6e2748a187342ccec5 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
similarity index 97%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
index 634c0fff1ea320eeb97a49ba89709263813dc059..bda50f47b98250fe247285cfb5303050a34514fe 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 interface IntroductionConstants {
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
similarity index 98%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
index a91184db2a60c1f032272a3c99125f40ce43f4d0..a563e3c753de524bb6da390ea4a1446fa7868df6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.crypto.KeyPair;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
similarity index 89%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
index 93ff9b33e30b5fddc06dec4c24421bf555f7a085..9bf4e46c2be23ad933f874777359811317e85269 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.FormatException;
@@ -24,14 +24,14 @@ import java.util.Map;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_ALICE_MAC_KEY;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_MAC;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_NONCE;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_SIGN;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_BOB_MAC_KEY;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_MASTER_KEY;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_SESSION_ID;
-import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_ALICE_MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_MAC;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_NONCE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_SIGN;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_BOB_MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_MASTER_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
 
 @Immutable
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java
deleted file mode 100644
index 050d2b9f4430bcf38f950ea6c3dd941d0ac44cd2..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.client.ContactGroupFactory;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.sync.Group;
-
-import javax.inject.Inject;
-
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
-
-class IntroductionGroupFactory {
-
-	private final ContactGroupFactory contactGroupFactory;
-	private final Group localGroup;
-
-	@Inject
-	IntroductionGroupFactory(ContactGroupFactory contactGroupFactory) {
-		this.contactGroupFactory = contactGroupFactory;
-		localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
-				CLIENT_VERSION);
-	}
-
-	Group createIntroductionGroup(Contact c) {
-		return contactGroupFactory.createContactGroup(CLIENT_ID,
-				CLIENT_VERSION, c);
-	}
-
-	Group createLocalGroup() {
-		return localGroup;
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index 9ccf06b6796fdf869500d8e7233042950c9a84ec..7b7f9cd9b32b0571365ee53a5126ee4e85e8ae94 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -2,19 +2,20 @@ package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.ContactGroupFactory;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
 import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.data.MetadataParser;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.NoSuchContactException;
-import org.briarproject.bramble.api.db.NoSuchMessageException;
+import org.briarproject.bramble.api.db.Metadata;
 import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Client;
 import org.briarproject.bramble.api.sync.Group;
@@ -24,264 +25,278 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.MessageStatus;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.IntroducerProtocolState;
 import org.briarproject.briar.api.introduction.IntroductionManager;
 import org.briarproject.briar.api.introduction.IntroductionMessage;
 import org.briarproject.briar.api.introduction.IntroductionRequest;
 import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.Role;
 import org.briarproject.briar.client.ConversationClientImpl;
+import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
-import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static java.util.logging.Level.WARNING;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroductionConstants.GROUP_KEY_CONTACT_ID;
+import static org.briarproject.briar.introduction.MessageType.ABORT;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+import static org.briarproject.briar.introduction.MessageType.DECLINE;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 
 @Immutable
 @NotNullByDefault
 class IntroductionManagerImpl extends ConversationClientImpl
 		implements IntroductionManager, Client, ContactHook {
 
-	private static final Logger LOG =
-			Logger.getLogger(IntroductionManagerImpl.class.getName());
-
-	private final IntroducerManager introducerManager;
-	private final IntroduceeManager introduceeManager;
-	private final IntroductionGroupFactory introductionGroupFactory;
+	private final ContactGroupFactory contactGroupFactory;
+	private final MessageParser messageParser;
+	private final SessionEncoder sessionEncoder;
+	private final SessionParser sessionParser;
+	private final IntroducerProtocolEngine introducerEngine;
+	private final IntroduceeProtocolEngine introduceeEngine;
+	private final IntroductionCrypto crypto;
+	private final IdentityManager identityManager;
 
 	@Inject
-	IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
-			MetadataParser metadataParser, MessageTracker messageTracker,
-			IntroducerManager introducerManager,
-			IntroduceeManager introduceeManager,
-			IntroductionGroupFactory introductionGroupFactory) {
-
+	IntroductionManagerImpl(
+			DatabaseComponent db,
+			ClientHelper clientHelper,
+			MetadataParser metadataParser,
+			MessageTracker messageTracker,
+			ContactGroupFactory contactGroupFactory,
+			MessageParser messageParser,
+			SessionEncoder sessionEncoder,
+			SessionParser sessionParser,
+			IntroducerProtocolEngine introducerEngine,
+			IntroduceeProtocolEngine introduceeEngine,
+			IntroductionCrypto crypto,
+			IdentityManager identityManager) {
 		super(db, clientHelper, metadataParser, messageTracker);
-		this.introducerManager = introducerManager;
-		this.introduceeManager = introduceeManager;
-		this.introductionGroupFactory = introductionGroupFactory;
+		this.contactGroupFactory = contactGroupFactory;
+		this.messageParser = messageParser;
+		this.sessionEncoder = sessionEncoder;
+		this.sessionParser = sessionParser;
+		this.introducerEngine = introducerEngine;
+		this.introduceeEngine = introduceeEngine;
+		this.crypto = crypto;
+		this.identityManager = identityManager;
 	}
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
-		Group localGroup = introductionGroupFactory.createLocalGroup();
+		// Create a local group to store protocol sessions
+		Group localGroup = getLocalGroup();
 		if (db.containsGroup(txn, localGroup.getId())) return;
 		db.addGroup(txn, localGroup);
-		// Ensure we've set things up for any pre-existing contacts
+		// Set up groups for communication with any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
 	@Override
+	// TODO adapt to use upcoming ClientVersioning client
 	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		// Return if we've already set things up for this contact
+		if (db.containsGroup(txn, g.getId())) return;
+		// Store the group and share it with the contact
+		db.addGroup(txn, g);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
+		// Attach the contact ID to the group
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
 		try {
-			// Create an introduction group for sending introduction messages
-			Group g = getContactGroup(c);
-			// Return if we've already set things up for this contact
-			if (db.containsGroup(txn, g.getId())) return;
-			// Store the group and share it with the contact
-			db.addGroup(txn, g);
-			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
-			// Attach the contact ID to the group
-			BdfDictionary gm = new BdfDictionary();
-			gm.put(CONTACT, c.getId().getInt());
-			clientHelper.mergeGroupMetadata(txn, g.getId(), gm);
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
 		} catch (FormatException e) {
-			throw new RuntimeException(e);
+			throw new AssertionError(e);
 		}
 	}
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
-		GroupId gId = introductionGroupFactory.createLocalGroup().getId();
-
-		// search for session states where c introduced us
-		BdfDictionary query = BdfDictionary.of(
-				new BdfEntry(ROLE, ROLE_INTRODUCEE),
-				new BdfEntry(CONTACT_ID_1, c.getId().getInt())
-		);
-		try {
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn, gId, query);
-			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
-				// delete states if introducee removes introducer
-				deleteMessage(txn, entry.getKey());
-			}
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-
-		// check for open sessions with c and abort those,
-		// so the other introducee knows
-		query = BdfDictionary.of(
-				new BdfEntry(ROLE, ROLE_INTRODUCER)
-		);
 		try {
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn, gId, query);
-			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
-				BdfDictionary d = entry.getValue();
-				ContactId c1 = new ContactId(d.getLong(CONTACT_ID_1).intValue());
-				ContactId c2 = new ContactId(d.getLong(CONTACT_ID_2).intValue());
-
-				if (c1.equals(c.getId()) || c2.equals(c.getId())) {
-					IntroducerProtocolState state = IntroducerProtocolState
-							.fromValue(d.getLong(STATE).intValue());
-					// abort protocol if still ongoing
-					if (IntroducerProtocolState.isOngoing(state)) {
-						introducerManager.abort(txn, d);
-					}
-					// also delete state if both contacts have been deleted
-					if (c1.equals(c.getId())) {
-						try {
-							db.getContact(txn, c2);
-						} catch (NoSuchContactException e) {
-							deleteMessage(txn, entry.getKey());
-						}
-					} else if (c2.equals(c.getId())) {
-						try {
-							db.getContact(txn, c1);
-						} catch (NoSuchContactException e) {
-							deleteMessage(txn, entry.getKey());
-						}
-					}
-				}
-			}
+			removeSessionWithIntroducer(txn, c);
+			abortOrRemoveSessionWithIntroducee(txn, c);
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			throw new AssertionError();
 		}
-
-		// remove the group (all messages will be removed with it)
-		// this contact won't get our abort message, but the other will
+		// Remove the contact group (all messages will be removed with it)
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
-	/**
-	 * This is called when a new message arrived and is being validated.
-	 * It is the central method where we determine which role we play
-	 * in the introduction protocol and which engine we need to start.
-	 */
+	@Override
+	public Group getContactGroup(Contact c) {
+		return contactGroupFactory
+				.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
+	}
+
 	@Override
 	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
-			BdfDictionary message) throws DbException, FormatException {
-
-		// Get message data and type
-		GroupId groupId = m.getGroupId();
-		long type = message.getLong(TYPE, -1L);
-
-		// we are an introducee, need to initialize new state
-		if (type == TYPE_REQUEST) {
-			boolean stateExists = true;
-			try {
-				getSessionState(txn, groupId, message.getRaw(SESSION_ID), false);
-			} catch (FormatException e) {
-				stateExists = false;
-			}
-			if (stateExists) throw new FormatException();
-			BdfDictionary state =
-					introduceeManager.initialize(txn, groupId, message);
-			try {
-				introduceeManager.incomingMessage(txn, state, message);
-				messageTracker.trackIncomingMessage(txn, m);
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				introduceeManager.abort(txn, state);
-			} catch (FormatException e) {
-				// FIXME necessary?
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				introduceeManager.abort(txn, state);
-			}
+			BdfDictionary bdfMeta) throws DbException, FormatException {
+		// Parse the metadata
+		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
+		// Look up the session, if there is one
+		SessionId sessionId = meta.getSessionId();
+		IntroduceeSession newIntroduceeSession = null;
+		if (sessionId == null) {
+			if (meta.getMessageType() != REQUEST) throw new AssertionError();
+			newIntroduceeSession = createNewIntroduceeSession(txn, m, body);
+			sessionId = newIntroduceeSession.getSessionId();
 		}
-		// our role can be anything
-		else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
-			BdfDictionary state =
-					getSessionState(txn, groupId, message.getRaw(SESSION_ID));
-
-			long role = state.getLong(ROLE, -1L);
-			try {
-				if (role == ROLE_INTRODUCER) {
-					introducerManager.incomingMessage(txn, state, message);
-					if (type == TYPE_RESPONSE)
-						messageTracker.trackIncomingMessage(txn, m);
-				} else if (role == ROLE_INTRODUCEE) {
-					introduceeManager.incomingMessage(txn, state, message);
-					if (type == TYPE_RESPONSE && !message.getBoolean(ACCEPT))
-						messageTracker.trackIncomingMessage(txn, m);
-				} else {
-					if (LOG.isLoggable(WARNING))
-						LOG.warning("Unknown role '" + role + "'");
-					throw new DbException();
-				}
-			} catch (DbException | FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
-				else introduceeManager.abort(txn, state);
-			}
+		StoredSession ss = getSession(txn, sessionId);
+		// Handle the message
+		Session session;
+		MessageId storageId;
+		if (ss == null) {
+			if (meta.getMessageType() != REQUEST) throw new FormatException();
+			if (newIntroduceeSession == null) throw new AssertionError();
+			storageId = createStorageId(txn);
+			session = handleMessage(txn, m, body, meta.getMessageType(),
+					newIntroduceeSession, introduceeEngine);
 		} else {
-			// the message has been validated, so this should not happen
-			if(LOG.isLoggable(WARNING)) {
-				LOG.warning("Unknown message type '" + type + "', deleting...");
-			}
+			storageId = ss.storageId;
+			Role role = sessionParser.getRole(ss.bdfSession);
+			if (role == INTRODUCER) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseIntroducerSession(ss.bdfSession),
+						introducerEngine);
+			} else if (role == INTRODUCEE) {
+				session = handleMessage(txn, m, body, meta.getMessageType(),
+						sessionParser.parseIntroduceeSession(m.getGroupId(),
+								ss.bdfSession), introduceeEngine);
+			} else throw new AssertionError();
 		}
+		// Store the updated session
+		storeSession(txn, storageId, session);
 		return false;
 	}
 
-	@Override
-	public Group getContactGroup(Contact contact) {
-		return introductionGroupFactory.createIntroductionGroup(contact);
+	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
+			Message m, BdfList body) throws DbException, FormatException {
+		ContactId introducerId = getContactId(txn, m.getGroupId());
+		Author introducer = db.getContact(txn, introducerId).getAuthor();
+		Author alice = identityManager.getLocalAuthor(txn);
+		Author bob = messageParser.parseRequestMessage(m, body).getAuthor();
+		if (alice.equals(bob)) throw new FormatException();
+		SessionId sessionId = crypto.getSessionId(introducer, alice, bob);
+		return IntroduceeSession
+				.getInitial(m.getGroupId(), sessionId, introducer, bob);
+	}
+
+	private <S extends Session> S handleMessage(Transaction txn, Message m,
+			BdfList body, MessageType type, S session, ProtocolEngine<S> engine)
+			throws DbException, FormatException {
+		if (type == REQUEST) {
+			RequestMessage request = messageParser.parseRequestMessage(m, body);
+			return engine.onRequestMessage(txn, session, request);
+		} else if (type == ACCEPT) {
+			AcceptMessage accept = messageParser.parseAcceptMessage(m, body);
+			return engine.onAcceptMessage(txn, session, accept);
+		} else if (type == DECLINE) {
+			DeclineMessage decline = messageParser.parseDeclineMessage(m, body);
+			return engine.onDeclineMessage(txn, session, decline);
+		} else if (type == AUTH) {
+			AuthMessage auth = messageParser.parseAuthMessage(m, body);
+			return engine.onAuthMessage(txn, session, auth);
+		} else if (type == ACTIVATE) {
+			ActivateMessage activate =
+					messageParser.parseActivateMessage(m, body);
+			return engine.onActivateMessage(txn, session, activate);
+		} else if (type == ABORT) {
+			AbortMessage abort = messageParser.parseAbortMessage(m, body);
+			return engine.onAbortMessage(txn, session, abort);
+		} else {
+			throw new AssertionError();
+		}
+	}
+
+	@Nullable
+	private StoredSession getSession(Transaction txn,
+			@Nullable SessionId sessionId) throws DbException, FormatException {
+		if (sessionId == null) return null;
+		BdfDictionary query = sessionParser.getSessionQuery(sessionId);
+		Map<MessageId, BdfDictionary> results = clientHelper
+				.getMessageMetadataAsDictionary(txn, getLocalGroup().getId(),
+						query);
+		if (results.size() > 1) throw new DbException();
+		if (results.isEmpty()) return null;
+		return new StoredSession(results.keySet().iterator().next(),
+				results.values().iterator().next());
+	}
+
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		BdfDictionary meta =
+				clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId);
+		return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
+	}
+
+	private MessageId createStorageId(Transaction txn) throws DbException {
+		Message m = clientHelper
+				.createMessageForStoringMetadata(getLocalGroup().getId());
+		db.addLocalMessage(txn, m, new Metadata(), false);
+		return m.getId();
+	}
+
+	private void storeSession(Transaction txn, MessageId storageId,
+			Session session) throws DbException, FormatException {
+		BdfDictionary d;
+		if (session.getRole() == INTRODUCER) {
+			d = sessionEncoder
+					.encodeIntroducerSession((IntroducerSession) session);
+		} else if (session.getRole() == INTRODUCEE) {
+			d = sessionEncoder
+					.encodeIntroduceeSession((IntroduceeSession) session);
+		} else {
+			throw new AssertionError();
+		}
+		clientHelper.mergeMessageMetadata(txn, storageId, d);
 	}
 
 	@Override
 	public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
-			long timestamp) throws DbException, FormatException {
-
+			long timestamp) throws DbException {
 		Transaction txn = db.startTransaction(false);
 		try {
-			introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
-			Group g1 = getContactGroup(c1);
-			Group g2 = getContactGroup(c2);
-			messageTracker.trackMessage(txn, g1.getId(), timestamp, true);
-			messageTracker.trackMessage(txn, g2.getId(), timestamp, true);
+			// Look up the session, if there is one
+			Author introducer = identityManager.getLocalAuthor(txn);
+			SessionId sessionId =
+					crypto.getSessionId(introducer, c1.getAuthor(),
+							c2.getAuthor());
+			StoredSession ss = getSession(txn, sessionId);
+			// Create or parse the session
+			IntroducerSession session;
+			MessageId storageId;
+			if (ss == null) {
+				// This is the first request - create a new session
+				GroupId groupId1 = getContactGroup(c1).getId();
+				GroupId groupId2 = getContactGroup(c2).getId();
+				session = new IntroducerSession(sessionId, groupId1,
+						c1.getAuthor(), groupId2, c2.getAuthor());
+				storageId = createStorageId(txn);
+			} else {
+				// An earlier request exists, so we already have a session
+				session = sessionParser.parseIntroducerSession(ss.bdfSession);
+				storageId = ss.storageId;
+			}
+			// Handle the request action
+			session = introducerEngine
+					.onRequestAction(txn, session, msg, timestamp);
+			// Store the updated session
+			storeSession(txn, storageId, session);
 			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
@@ -289,147 +304,78 @@ class IntroductionManagerImpl extends ConversationClientImpl
 
 	@Override
 	public void acceptIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException, FormatException {
-
-		Transaction txn = db.startTransaction(false);
-		try {
-			Contact c = db.getContact(txn, contactId);
-			Group g = getContactGroup(c);
-			BdfDictionary state =
-					getSessionState(txn, g.getId(), sessionId.getBytes());
-
-			introduceeManager.acceptIntroduction(txn, state, timestamp);
-			messageTracker.trackMessage(txn, g.getId(), timestamp, true);
-			db.commitTransaction(txn);
-		} finally {
-			db.endTransaction(txn);
-		}
+			long timestamp) throws DbException {
+		respondToRequest(contactId, sessionId, timestamp, true);
 	}
 
 	@Override
 	public void declineIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException, FormatException {
+			long timestamp) throws DbException {
+		respondToRequest(contactId, sessionId, timestamp, false);
+	}
 
+	private void respondToRequest(ContactId contactId, SessionId sessionId,
+			long timestamp, boolean accept) throws DbException {
 		Transaction txn = db.startTransaction(false);
 		try {
-			Contact c = db.getContact(txn, contactId);
-			Group g = getContactGroup(c);
-			BdfDictionary state =
-					getSessionState(txn, g.getId(), sessionId.getBytes());
-
-			introduceeManager.declineIntroduction(txn, state, timestamp);
-			messageTracker.trackMessage(txn, g.getId(), timestamp, true);
+			// Look up the session
+			StoredSession ss = getSession(txn, sessionId);
+			if (ss == null) throw new IllegalArgumentException();
+			// Parse the session
+			Contact contact = db.getContact(txn, contactId);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			IntroduceeSession session = sessionParser
+					.parseIntroduceeSession(contactGroupId, ss.bdfSession);
+			// Handle the join or leave action
+			if (accept) {
+				session = introduceeEngine
+						.onAcceptAction(txn, session, timestamp);
+			} else {
+				session = introduceeEngine
+						.onDeclineAction(txn, session, timestamp);
+			}
+			// Store the updated session
+			storeSession(txn, ss.storageId, session);
 			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
 	}
 
 	@Override
-	public Collection<IntroductionMessage> getIntroductionMessages(
-			ContactId contactId) throws DbException {
-
-		Collection<IntroductionMessage> list = new ArrayList<>();
-
-		Map<MessageId, BdfDictionary> metadata;
-		Collection<MessageStatus> statuses;
+	public Collection<IntroductionMessage> getIntroductionMessages(ContactId c)
+			throws DbException {
+		List<IntroductionMessage> messages;
 		Transaction txn = db.startTransaction(true);
 		try {
-			// get messages and their status
-			GroupId g = getContactGroup(db.getContact(txn, contactId)).getId();
-			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
-			statuses = db.getMessageStatus(txn, contactId, g);
-
-			// turn messages into classes for the UI
-			for (MessageStatus s : statuses) {
-				MessageId messageId = s.getMessageId();
-				BdfDictionary msg = metadata.get(messageId);
-				if (msg == null) continue;
-
-				try {
-					long type = msg.getLong(TYPE);
-					if (type == TYPE_ACK || type == TYPE_ABORT) continue;
-
-					// get session state
-					SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
-					BdfDictionary state =
-							getSessionState(txn, g, sessionId.getBytes());
-
-					int role = state.getLong(ROLE).intValue();
-					boolean local;
-					long time = msg.getLong(MESSAGE_TIME);
-					boolean accepted = msg.getBoolean(ACCEPT, false);
-					boolean read = msg.getBoolean(MSG_KEY_READ, false);
-					AuthorId authorId;
-					String name;
-					if (type == TYPE_RESPONSE) {
-						if (role == ROLE_INTRODUCER) {
-							if (!concernsThisContact(contactId, messageId, state)) {
-								// this response is not from contactId
-								continue;
-							}
-							local = false;
-							authorId =
-									getAuthorIdForIntroducer(contactId, state);
-							name = getNameForIntroducer(contactId, state);
-						} else {
-							if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE),
-									messageId.getBytes())) {
-								// this response is not ours,
-								// check if it was a decline
-								if (!accepted) {
-									local = false;
-								} else {
-									// don't include positive responses
-									continue;
-								}
-							} else {
-								local = true;
-							}
-							authorId = new AuthorId(
-									state.getRaw(REMOTE_AUTHOR_ID));
-							name = state.getString(NAME);
-						}
-						IntroductionResponse ir = new IntroductionResponse(
-								sessionId, messageId, g, role, time, local,
-								s.isSent(), s.isSeen(), read, authorId, name,
-								accepted);
-						list.add(ir);
-					} else if (type == TYPE_REQUEST) {
-						String message;
-						boolean answered, exists, introducesOtherIdentity;
-						if (role == ROLE_INTRODUCER) {
-							local = true;
-							authorId =
-									getAuthorIdForIntroducer(contactId, state);
-							name = getNameForIntroducer(contactId, state);
-							message = msg.getOptionalString(MSG);
-							answered = false;
-							exists = false;
-							introducesOtherIdentity = false;
-						} else {
-							local = false;
-							authorId = new AuthorId(
-									state.getRaw(REMOTE_AUTHOR_ID));
-							name = state.getString(NAME);
-							message = state.getOptionalString(MSG);
-							boolean finished = state.getLong(STATE) ==
-									FINISHED.getValue();
-							answered = finished || state.getBoolean(ANSWERED);
-							exists = state.getBoolean(EXISTS);
-							introducesOtherIdentity =
-									state.getBoolean(REMOTE_AUTHOR_IS_US);
-						}
-						IntroductionRequest ir = new IntroductionRequest(
-								sessionId, messageId, g, role, time, local,
-								s.isSent(), s.isSeen(), read, authorId, name,
-								accepted, message, answered, exists,
-								introducesOtherIdentity);
-						list.add(ir);
-					}
-				} catch (FormatException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
+			Contact contact = db.getContact(txn, c);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
+			Map<MessageId, BdfDictionary> results = clientHelper
+					.getMessageMetadataAsDictionary(txn, contactGroupId, query);
+			messages = new ArrayList<>(results.size());
+			for (Map.Entry<MessageId, BdfDictionary> e : results.entrySet()) {
+				MessageId m = e.getKey();
+				MessageMetadata meta =
+						messageParser.parseMetadata(e.getValue());
+				MessageStatus status = db.getMessageStatus(txn, c, m);
+				StoredSession ss = getSession(txn, meta.getSessionId());
+				if (ss == null) throw new AssertionError();
+				MessageType type = meta.getMessageType();
+				if (type == REQUEST) {
+					messages.add(
+							parseInvitationRequest(txn, contactGroupId, m,
+									meta, status, ss.bdfSession));
+				} else if (type == ACCEPT) {
+					messages.add(
+							parseInvitationResponse(txn, contactGroupId, m,
+									meta, status, ss.bdfSession, true));
+				} else if (type == DECLINE) {
+					messages.add(
+							parseInvitationResponse(txn, contactGroupId, m,
+									meta, status, ss.bdfSession, false));
 				}
 			}
 			db.commitTransaction(txn);
@@ -438,88 +384,129 @@ class IntroductionManagerImpl extends ConversationClientImpl
 		} finally {
 			db.endTransaction(txn);
 		}
-		return list;
+		return messages;
 	}
 
-	private String getNameForIntroducer(ContactId contactId,
-			BdfDictionary state) throws FormatException {
-
-		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
-			return state.getString(CONTACT_2);
-		if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
-			return state.getString(CONTACT_1);
-		throw new RuntimeException(
-				"Contact not part of this introduction session");
+	private IntroductionRequest parseInvitationRequest(Transaction txn,
+			GroupId contactGroupId, MessageId m, MessageMetadata meta,
+			MessageStatus status, BdfDictionary bdfSession)
+			throws DbException, FormatException {
+		Role role = sessionParser.getRole(bdfSession);
+		SessionId sessionId;
+		Author author;
+		if (role == INTRODUCER) {
+			IntroducerSession session =
+					sessionParser.parseIntroducerSession(bdfSession);
+			sessionId = session.getSessionId();
+			LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+			if (localAuthor.equals(session.getIntroducee1().author)) {
+				author = session.getIntroducee2().author;
+			} else {
+				author = session.getIntroducee1().author;
+			}
+		} else if (role == INTRODUCEE) {
+			IntroduceeSession session = sessionParser
+					.parseIntroduceeSession(contactGroupId, bdfSession);
+			sessionId = session.getSessionId();
+			author = session.getRemoteAuthor();
+		} else throw new AssertionError();
+		String message = ""; // TODO
+		boolean contactExists = false; // TODO
+
+		return new IntroductionRequest(sessionId, m, contactGroupId,
+				role, meta.getTimestamp(), meta.isLocal(),
+				status.isSent(), status.isSeen(), meta.isRead(),
+				author.getName(), false, message, !meta.isAvailableToAnswer(),
+				contactExists);
 	}
 
-	private AuthorId getAuthorIdForIntroducer(ContactId contactId,
-			BdfDictionary state) throws FormatException {
-
-		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
-			return new AuthorId(state.getRaw(AUTHOR_ID_2));
-		if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
-			return new AuthorId(state.getRaw(AUTHOR_ID_1));
-		throw new RuntimeException(
-				"Contact not part of this introduction session");
+	private IntroductionResponse parseInvitationResponse(Transaction txn,
+			GroupId contactGroupId, MessageId m, MessageMetadata meta,
+			MessageStatus status, BdfDictionary bdfSession, boolean accept)
+			throws FormatException, DbException {
+		Role role = sessionParser.getRole(bdfSession);
+		SessionId sessionId;
+		Author author;
+		if (role == INTRODUCER) {
+			IntroducerSession session =
+					sessionParser.parseIntroducerSession(bdfSession);
+			sessionId = session.getSessionId();
+			LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+			if (localAuthor.equals(session.getIntroducee1().author)) {
+				author = session.getIntroducee2().author;
+			} else {
+				author = session.getIntroducee1().author;
+			}
+		} else if (role == INTRODUCEE) {
+			IntroduceeSession session = sessionParser
+					.parseIntroduceeSession(contactGroupId, bdfSession);
+			sessionId = session.getSessionId();
+			author = session.getRemoteAuthor();
+		} else throw new AssertionError();
+		return new IntroductionResponse(sessionId, m, contactGroupId,
+				role, meta.getTimestamp(), meta.isLocal(), status.isSent(),
+				status.isSeen(), meta.isRead(), author.getName(), accept);
 	}
 
-	private boolean concernsThisContact(ContactId contactId, MessageId messageId,
-			BdfDictionary state) throws FormatException {
-
-		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) {
-			return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]),
-					messageId.getBytes());
-		} else {
-			return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]),
-					messageId.getBytes());
+	private void removeSessionWithIntroducer(Transaction txn,
+			Contact introducer) throws DbException, FormatException {
+		BdfDictionary query = sessionEncoder
+				.getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor());
+		Map<MessageId, BdfDictionary> sessions = clientHelper
+				.getMessageMetadataAsDictionary(txn, getLocalGroup().getId(),
+						query);
+		for (MessageId id : sessions.keySet()) {
+			db.deleteMessageMetadata(txn, id); // TODO needed?
+			db.removeMessage(txn, id);
 		}
 	}
 
-	private BdfDictionary getSessionState(Transaction txn, GroupId groupId,
-			byte[] sessionId, boolean warn)
-			throws DbException, FormatException {
-
-		try {
-			// See if we can find the state directly for the introducer
-			BdfDictionary state = clientHelper
-					.getMessageMetadataAsDictionary(txn,
-							new MessageId(sessionId));
-			GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1));
-			GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2));
-			if (!g1.equals(groupId) && !g2.equals(groupId)) {
-				throw new NoSuchMessageException();
-			}
-			return state;
-		} catch (NoSuchMessageException e) {
-			// State not found directly, so iterate over all states
-			// to find state for introducee
-			Map<MessageId, BdfDictionary> map = clientHelper
-					.getMessageMetadataAsDictionary(txn,
-							introductionGroupFactory.createLocalGroup().getId());
-			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
-				if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) {
-					BdfDictionary state = m.getValue();
-					GroupId g = new GroupId(state.getRaw(GROUP_ID));
-					if (g.equals(groupId)) return state;
-				}
+	private void abortOrRemoveSessionWithIntroducee(Transaction txn,
+			Contact c) throws DbException, FormatException {
+		BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery();
+		Map<MessageId, BdfDictionary> sessions = clientHelper
+				.getMessageMetadataAsDictionary(txn, getLocalGroup().getId(),
+						query);
+		LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
+		for (Map.Entry<MessageId, BdfDictionary> session : sessions
+				.entrySet()) {
+			IntroducerSession s =
+					sessionParser.parseIntroducerSession(session.getValue());
+			if (s.getIntroducee1().author.equals(c.getAuthor())) {
+				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
+						s.getIntroducee2(), localAuthor);
+			} else if (s.getIntroducee2().author.equals(c.getAuthor())) {
+				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
+						s.getIntroducee1(), localAuthor);
 			}
-			if (warn && LOG.isLoggable(WARNING))
-				LOG.warning("No session state found");
-			throw new FormatException();
 		}
 	}
 
-	private BdfDictionary getSessionState(Transaction txn, GroupId groupId,
-			byte[] sessionId) throws DbException, FormatException {
+	private void abortOrRemoveSessionWithIntroducee(Transaction txn,
+			IntroducerSession s, MessageId storageId, Introducee i,
+			LocalAuthor localAuthor) throws DbException, FormatException {
+		if (db.containsContact(txn, i.author.getId(), localAuthor.getId())) {
+			IntroducerSession session = introducerEngine.onAbortAction(txn, s);
+			storeSession(txn, storageId, session);
+		} else {
+			db.deleteMessageMetadata(txn, storageId); // TODO needed?
+			db.removeMessage(txn, storageId);
+		}
+	}
 
-		return getSessionState(txn, groupId, sessionId, true);
+	private Group getLocalGroup() {
+		return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
 	}
 
-	private void deleteMessage(Transaction txn, MessageId messageId)
-			throws DbException {
+	private static class StoredSession {
 
-		db.deleteMessage(txn, messageId);
-		db.deleteMessageMetadata(txn, messageId);
+		private final MessageId storageId;
+		private final BdfDictionary bdfSession;
+
+		private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
+			this.storageId = storageId;
+			this.bdfSession = bdfSession;
+		}
 	}
 
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
index e0122faa9d315af728599a43220b89c4afdec756..24c649c5fd6cbb0e4a1fe7ad838af2ff13ac4d20 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java
@@ -4,8 +4,8 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.MessageQueueManager;
 import org.briarproject.briar.api.introduction.IntroductionManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
 
@@ -21,22 +21,22 @@ import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT
 public class IntroductionModule {
 
 	public static class EagerSingletons {
-		@Inject
-		IntroductionManager introductionManager;
 		@Inject
 		IntroductionValidator introductionValidator;
+		@Inject
+		IntroductionManager introductionManager;
 	}
 
 	@Provides
 	@Singleton
-	IntroductionValidator provideValidator(
-			MessageQueueManager messageQueueManager,
-			MetadataEncoder metadataEncoder, ClientHelper clientHelper,
-			Clock clock) {
-
-		IntroductionValidator introductionValidator = new IntroductionValidator(
-				clientHelper, metadataEncoder, clock);
-		messageQueueManager.registerMessageValidator(CLIENT_ID,
+	IntroductionValidator provideValidator(ValidationManager validationManager,
+			MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
+			ClientHelper clientHelper, Clock clock) {
+
+		IntroductionValidator introductionValidator =
+				new IntroductionValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock);
+		validationManager.registerMessageValidator(CLIENT_ID,
 				introductionValidator);
 
 		return introductionValidator;
@@ -46,16 +46,42 @@ public class IntroductionModule {
 	@Singleton
 	IntroductionManager provideIntroductionManager(
 			LifecycleManager lifecycleManager, ContactManager contactManager,
-			MessageQueueManager messageQueueManager,
+			ValidationManager validationManager,
 			ConversationManager conversationManager,
 			IntroductionManagerImpl introductionManager) {
-
 		lifecycleManager.registerClient(introductionManager);
 		contactManager.registerContactHook(introductionManager);
-		messageQueueManager.registerIncomingMessageHook(CLIENT_ID,
+		validationManager.registerIncomingMessageHook(CLIENT_ID,
 				introductionManager);
 		conversationManager.registerConversationClient(introductionManager);
 
 		return introductionManager;
 	}
+
+	@Provides
+	MessageParser provideMessageParser(MessageParserImpl messageParser) {
+		return messageParser;
+	}
+
+	@Provides
+	MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
+	@Provides
+	SessionParser provideSessionParser(SessionParserImpl sessionParser) {
+		return sessionParser;
+	}
+
+	@Provides
+	SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
+		return sessionEncoder;
+	}
+
+	@Provides
+	IntroductionCrypto provideIntroductionCrypto(
+			IntroductionCryptoImpl introductionCrypto) {
+		return introductionCrypto;
+	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
index 9fc526e49c24a709cc2a43d9abaea0b5973c944d..16d26afdf38da22645c7ffedf9a89406628d0ea0 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java
@@ -1,7 +1,9 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.client.BdfMessageContext;
+import org.briarproject.bramble.api.client.BdfMessageValidator;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfList;
@@ -9,183 +11,166 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.client.BdfQueueMessageValidator;
+
+import java.util.Collections;
 
 import javax.annotation.concurrent.Immutable;
 
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
 import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
-import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
-import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
 import static org.briarproject.bramble.util.ValidationUtils.checkLength;
 import static org.briarproject.bramble.util.ValidationUtils.checkSize;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+
 
 @Immutable
 @NotNullByDefault
-class IntroductionValidator extends BdfQueueMessageValidator {
+class IntroductionValidator extends BdfMessageValidator {
+
+	private final MessageEncoder messageEncoder;
 
-	IntroductionValidator(ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
+	IntroductionValidator(MessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
 		super(clientHelper, metadataEncoder, clock);
+		this.messageEncoder = messageEncoder;
 	}
 
 	@Override
 	protected BdfMessageContext validateMessage(Message m, Group g,
 			BdfList body) throws FormatException {
+		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
+
+		switch (type) {
+			case REQUEST:
+				return validateRequestMessage(m, body);
+			case ACCEPT:
+				return validateAcceptMessage(m, body);
+			case AUTH:
+				return validateAuthMessage(m, body);
+			case DECLINE:
+			case ACTIVATE:
+			case ABORT:
+				return validateOtherMessage(type, m, body);
+			default:
+				throw new FormatException();
+		}
+	}
 
-		BdfDictionary d;
-		long type = body.getLong(0);
-		byte[] id = body.getRaw(1);
-		checkLength(id, SessionId.LENGTH);
-
-		if (type == TYPE_REQUEST) {
-			d = validateRequest(body);
-		} else if (type == TYPE_RESPONSE) {
-			d = validateResponse(body);
-		} else if (type == TYPE_ACK) {
-			d = validateAck(body);
-		} else if (type == TYPE_ABORT) {
-			d = validateAbort(body);
+	private BdfMessageContext validateRequestMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 4);
+
+		byte[] previousMessageId = body.getOptionalRaw(1);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		BdfList authorList = body.getList(2);
+		clientHelper.parseAndValidateAuthor(authorList);
+
+		String msg = body.getOptionalString(3);
+		checkLength(msg, 1, MAX_REQUEST_MESSAGE_LENGTH);
+
+		BdfDictionary meta = messageEncoder
+				.encodeRequestMetadata(m.getTimestamp(), false, false,
+						false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
 		} else {
-			throw new FormatException();
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
 		}
-
-		d.put(TYPE, type);
-		d.put(SESSION_ID, id);
-		d.put(GROUP_ID, m.getGroupId());
-		d.put(MESSAGE_ID, m.getId());
-		d.put(MESSAGE_TIME, m.getTimestamp());
-		return new BdfMessageContext(d);
 	}
 
-	private BdfDictionary validateRequest(BdfList message)
+	private BdfMessageContext validateAcceptMessage(Message m, BdfList body)
 			throws FormatException {
+		checkSize(body, 6);
 
-		checkSize(message, 4, 5);
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
 
-		// TODO: Exchange author format version
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
 
-		// parse contact name
-		String name = message.getString(2);
-		checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
+		byte[] ephemeralPublicKey = body.getRaw(3);
+		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
 
-		// parse contact's public key
-		byte[] key = message.getRaw(3);
-		checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH);
+		body.getLong(4);
 
-		// parse (optional) message
-		String msg = null;
-		if (message.size() == 5) {
-			msg = message.getString(4);
-			checkLength(msg, 0, MAX_INTRODUCTION_MESSAGE_LENGTH);
+		BdfDictionary transportProperties = body.getDictionary(5);
+		if (transportProperties.size() < 1) throw new FormatException();
+		for (String tId : transportProperties.keySet()) {
+			checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH);
+			BdfDictionary tProps = transportProperties.getDictionary(tId);
+			clientHelper.parseAndValidateTransportProperties(tProps);
 		}
 
-		// Return the metadata
-		BdfDictionary d = new BdfDictionary();
-		d.put(NAME, name);
-		d.put(PUBLIC_KEY, key);
-		if (msg != null) {
-			d.put(MSG, msg);
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(ACCEPT, sessionId, m.getTimestamp(), false,
+						false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
 		}
-		return d;
 	}
 
-	private BdfDictionary validateResponse(BdfList message)
+	private BdfMessageContext validateAuthMessage(Message m, BdfList body)
 			throws FormatException {
+		checkSize(body, 5);
 
-		checkSize(message, 3, 6);
-
-		// parse accept/decline
-		boolean accept = message.getBoolean(2);
-
-		long time = 0;
-		byte[] pubkey = null;
-		BdfDictionary tp = new BdfDictionary();
-		if (accept) {
-			checkSize(message, 6);
-
-			// parse timestamp
-			time = message.getLong(3);
-
-			// parse ephemeral public key
-			pubkey = message.getRaw(4);
-			checkLength(pubkey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES);
-
-			// parse transport properties
-			tp = message.getDictionary(5);
-			if (tp.size() < 1) throw new FormatException();
-			for (String tId : tp.keySet()) {
-				checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH);
-				BdfDictionary tProps = tp.getDictionary(tId);
-				checkSize(tProps, 0, MAX_PROPERTIES_PER_TRANSPORT);
-				for (String propId : tProps.keySet()) {
-					checkLength(propId, 0, MAX_PROPERTY_LENGTH);
-					String prop = tProps.getString(propId);
-					checkLength(prop, 0, MAX_PROPERTY_LENGTH);
-				}
-			}
-		} else {
-			checkSize(message, 3);
-		}
-
-		// Return the metadata
-		BdfDictionary d = new BdfDictionary();
-		d.put(ACCEPT, accept);
-		if (accept) {
-			d.put(TIME, time);
-			d.put(E_PUBLIC_KEY, pubkey);
-			d.put(TRANSPORT, tp);
-		}
-		return d;
-	}
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
 
-	private BdfDictionary validateAck(BdfList message) throws FormatException {
-		checkSize(message, 4);
+		byte[] previousMessageId = body.getRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
 
-		byte[] mac = message.getRaw(2);
-		checkLength(mac, 1, MAC_LENGTH);
+		byte[] mac = body.getRaw(3);
+		checkLength(mac, MAC_BYTES);
 
-		byte[] sig = message.getRaw(3);
-		checkLength(sig, 1, MAX_SIGNATURE_LENGTH);
+		byte[] signature = body.getRaw(4);
+		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
 
-		// Return the metadata
-		BdfDictionary d = new BdfDictionary();
-		d.put(MAC, mac);
-		d.put(SIGNATURE, sig);
-		return d;
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(AUTH, sessionId, m.getTimestamp(), false, false,
+						false);
+		MessageId dependency = new MessageId(previousMessageId);
+		return new BdfMessageContext(meta,
+				Collections.singletonList(dependency));
 	}
 
-	private BdfDictionary validateAbort(BdfList message)
-			throws FormatException {
+	private BdfMessageContext validateOtherMessage(MessageType type,
+			Message m, BdfList body) throws FormatException {
+		checkSize(body, 3);
+
+		byte[] sessionIdBytes = body.getRaw(1);
+		checkLength(sessionIdBytes, UniqueId.LENGTH);
 
-		checkSize(message, 2);
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
 
-		// Return the metadata
-		return new BdfDictionary();
+		SessionId sessionId = new SessionId(sessionIdBytes);
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, sessionId, m.getTimestamp(), false, false,
+						false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
 	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
similarity index 97%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoder.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
index 390ea9b0fbb83c1c1161e3d95be1e38c06948ee3..d619e3d76eb2f26ce4b417ce82245b743cf02f81 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoder.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.identity.Author;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
similarity index 83%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoderImpl.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
index 7f44671e0c73960f105fb90b5173cc582a09ede0..5bade9d86f338b30bd2badb1dacad9a7fec9f9af 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -20,19 +20,19 @@ import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_INVITATION_ACCEPTED;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_LOCAL;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_SESSION_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
-import static org.briarproject.briar.introduction2.MessageType.ABORT;
-import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
-import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
-import static org.briarproject.briar.introduction2.MessageType.AUTH;
-import static org.briarproject.briar.introduction2.MessageType.DECLINE;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_INVITATION_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
+import static org.briarproject.briar.introduction.MessageType.ABORT;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+import static org.briarproject.briar.introduction.MessageType.DECLINE;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 
 @NotNullByDefault
 class MessageEncoderImpl implements MessageEncoder {
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
similarity index 96%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageMetadata.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
index 67ba5b031992617fc41d73df1233e2139a088e1e..9b3ea54b53066b95852eca4f7d420fb9da36bad4 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageMetadata.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.client.SessionId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
similarity index 95%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParser.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
index 45526a09192977aa6a86f587f7c91b802438e01c..58f9dbfabd93f64b4c925af5df6dda3b0834e0a7 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.data.BdfDictionary;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
similarity index 86%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParserImpl.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
index 1c55be11d4f5f8b399b4974b875a299dd6f4fd9f..263af78442ec8cfb35f8832760054c303d9382d0 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -18,14 +18,14 @@ import java.util.Map;
 import javax.inject.Inject;
 
 import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_INVITATION_ACCEPTED;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_LOCAL;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_SESSION_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_INVITATION_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 
 @NotNullByDefault
 class MessageParserImpl implements MessageParser {
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java
deleted file mode 100644
index 7848aaf3dcd4fadba978f7cf576e763873c174d5..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataEncoder;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Metadata;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.MessageQueueManager;
-
-import javax.annotation.concurrent.Immutable;
-import javax.inject.Inject;
-
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-
-@Immutable
-@NotNullByDefault
-class MessageSender {
-
-	private final DatabaseComponent db;
-	private final ClientHelper clientHelper;
-	private final Clock clock;
-	private final MetadataEncoder metadataEncoder;
-	private final MessageQueueManager messageQueueManager;
-
-	@Inject
-	MessageSender(DatabaseComponent db, ClientHelper clientHelper, Clock clock,
-			MetadataEncoder metadataEncoder,
-			MessageQueueManager messageQueueManager) {
-
-		this.db = db;
-		this.clientHelper = clientHelper;
-		this.clock = clock;
-		this.metadataEncoder = metadataEncoder;
-		this.messageQueueManager = messageQueueManager;
-	}
-
-	void sendMessage(Transaction txn, BdfDictionary message)
-			throws DbException, FormatException {
-
-		BdfList bdfList = encodeMessage(message);
-		byte[] body = clientHelper.toByteArray(bdfList);
-		GroupId groupId = new GroupId(message.getRaw(GROUP_ID));
-		Group group = db.getGroup(txn, groupId);
-		long timestamp = clock.currentTimeMillis();
-
-		message.put(MESSAGE_TIME, timestamp);
-		Metadata metadata = metadataEncoder.encode(message);
-
-		messageQueueManager.sendMessage(txn, group, timestamp, body, metadata);
-	}
-
-	private BdfList encodeMessage(BdfDictionary d) throws FormatException {
-
-		BdfList body;
-		long type = d.getLong(TYPE);
-		if (type == TYPE_REQUEST) {
-			body = encodeRequest(d);
-		} else if (type == TYPE_RESPONSE) {
-			body = encodeResponse(d);
-		} else if (type == TYPE_ACK) {
-			body = encodeAck(d);
-		} else if (type == TYPE_ABORT) {
-			body = encodeAbort(d);
-		} else {
-			throw new FormatException();
-		}
-		return body;
-	}
-
-	private BdfList encodeRequest(BdfDictionary d) throws FormatException {
-		BdfList list = BdfList.of(TYPE_REQUEST, d.getRaw(SESSION_ID),
-				d.getString(NAME), d.getRaw(PUBLIC_KEY));
-
-		if (d.containsKey(MSG)) {
-			list.add(d.getString(MSG));
-		}
-		return list;
-	}
-
-	private BdfList encodeResponse(BdfDictionary d) throws FormatException {
-		BdfList list = BdfList.of(TYPE_RESPONSE, d.getRaw(SESSION_ID),
-				d.getBoolean(ACCEPT));
-
-		if (d.getBoolean(ACCEPT)) {
-			list.add(d.getLong(TIME));
-			list.add(d.getRaw(E_PUBLIC_KEY));
-			list.add(d.getDictionary(TRANSPORT));
-		}
-		return list;
-	}
-
-	private BdfList encodeAck(BdfDictionary d) throws FormatException {
-		return BdfList.of(TYPE_ACK, d.getRaw(SESSION_ID), d.getRaw(MAC),
-				d.getRaw(SIGNATURE));
-	}
-
-	private BdfList encodeAbort(BdfDictionary d) throws FormatException {
-		return BdfList.of(TYPE_ABORT, d.getRaw(SESSION_ID));
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java
similarity index 92%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/MessageType.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java
index bb34b7da3e8040c979ded40546b423dd46172dc1..67365399b0bc22a8627bdf6bba31355116be22ab 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/MessageType.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
similarity index 91%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
index e2f8b7c46fbb42f413a35c8a182a3de4e26c29c2..3c453d2fa9e8cc311623768c48362f65b197a2ad 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java
similarity index 96%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java
index 083296ca0cb00cc8e160ed36c12c821dc63e6c92..e3766c91a4556b3bde9e40c2371079a229ef4da7 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.db.DbException;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/RequestMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java
similarity index 88%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/RequestMessage.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java
index 470fb39662e3cce6f24d2c21bc3d7402f6657d7a..743e87af872d898f8535b9311d60eb9650622562 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/RequestMessage.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-class RequestMessage extends IntroductionMessage {
+class RequestMessage extends AbstractIntroductionMessage {
 
 	private final Author author;
 	@Nullable
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
similarity index 87%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
index ca2a89c190119dc9aae058d900318abc00b7a259..ddf04d044eb9e95522686d0c2113745a556ed91d 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java
@@ -1,8 +1,8 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.Role;
+import org.briarproject.briar.api.introduction.Role;
 
 import javax.annotation.concurrent.Immutable;
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java
similarity index 57%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java
index 4b45c39ca29b77057ac116e7bcc4aaee34a5f434..70cfff1bba1077426bc4e087cf60a6627829ae3b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java
@@ -1,11 +1,16 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 
 @NotNullByDefault
 interface SessionEncoder {
 
+	BdfDictionary getIntroduceeSessionsByIntroducerQuery(Author introducer);
+
+	BdfDictionary getIntroducerSessionsQuery();
+
 	BdfDictionary encodeIntroducerSession(IntroducerSession s);
 
 	BdfDictionary encodeIntroduceeSession(IntroduceeSession s);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
similarity index 54%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
index 3bce33d853d6cf57c8fc9cd7ade56e4718a2d501..9cf17a2d0fcb7dd181a438847ce23133eb2ca680 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
@@ -1,11 +1,13 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.transport.KeySetId;
-import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
 import java.util.Map;
 
@@ -14,28 +16,30 @@ import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_AUTHOR;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_GROUP_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCER;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_MASTER_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_SESSION_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_STATE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
 
 @Immutable
 @NotNullByDefault
@@ -48,6 +52,23 @@ class SessionEncoderImpl implements SessionEncoder {
 		this.clientHelper = clientHelper;
 	}
 
+	@Override
+	public BdfDictionary getIntroduceeSessionsByIntroducerQuery(
+			Author introducer) {
+		return BdfDictionary.of(
+				new BdfEntry(SESSION_KEY_ROLE, INTRODUCEE.getValue()),
+				new BdfEntry(SESSION_KEY_INTRODUCER,
+						clientHelper.toList(introducer))
+		);
+	}
+
+	@Override
+	public BdfDictionary getIntroducerSessionsQuery() {
+		return BdfDictionary.of(
+				new BdfEntry(SESSION_KEY_ROLE, INTRODUCER.getValue())
+		);
+	}
+
 	@Override
 	public BdfDictionary encodeIntroducerSession(IntroducerSession s) {
 		BdfDictionary d = encodeSession(s);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
similarity index 86%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
index dd3fba09cb69c17d94a0e04ec94ad921541471ae..c58cac3d951012cbd1298f2283b97e2747f13681 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java
@@ -1,11 +1,11 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.Role;
+import org.briarproject.briar.api.introduction.Role;
 
 @NotNullByDefault
 interface SessionParser {
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
similarity index 69%
rename from briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java
rename to briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
index dbb609fc1a49ec362630a08282b3a7857db8a7f1..f269b497301ce261335edcfbeb5170aa95f45c8b 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -12,8 +12,8 @@ import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.Role;
-import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+import org.briarproject.briar.api.introduction.Role;
+import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -22,30 +22,30 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_AUTHOR;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_GROUP_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCER;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_MASTER_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_SESSION_ID;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_STATE;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 
 @Immutable
 @NotNullByDefault
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/State.java b/briar-core/src/main/java/org/briarproject/briar/introduction/State.java
new file mode 100644
index 0000000000000000000000000000000000000000..3063f9bd83d1761fc6c4b5446a482b00f0329f80
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/State.java
@@ -0,0 +1,7 @@
+package org.briarproject.briar.introduction;
+
+interface State {
+
+	int getValue();
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java
deleted file mode 100644
index 55a6e552a5f3c672a754c99e3de09d4058dab631..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java
+++ /dev/null
@@ -1,459 +0,0 @@
-package org.briarproject.briar.introduction2;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.client.ContactGroupFactory;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataParser;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Metadata;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.IdentityManager;
-import org.briarproject.bramble.api.identity.LocalAuthor;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Client;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.sync.MessageStatus;
-import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction2.IntroductionManager;
-import org.briarproject.briar.api.introduction2.IntroductionMessage;
-import org.briarproject.briar.api.introduction2.IntroductionRequest;
-import org.briarproject.briar.api.introduction2.IntroductionResponse;
-import org.briarproject.briar.api.introduction2.Role;
-import org.briarproject.briar.client.ConversationClientImpl;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
-import javax.inject.Inject;
-
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
-import static org.briarproject.briar.introduction2.IntroductionConstants.GROUP_KEY_CONTACT_ID;
-import static org.briarproject.briar.introduction2.MessageType.ABORT;
-import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
-import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
-import static org.briarproject.briar.introduction2.MessageType.AUTH;
-import static org.briarproject.briar.introduction2.MessageType.DECLINE;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
-
-@Immutable
-@NotNullByDefault
-class IntroductionManagerImpl extends ConversationClientImpl
-		implements IntroductionManager, Client, ContactHook {
-
-	private final ContactGroupFactory contactGroupFactory;
-	private final MessageParser messageParser;
-	private final SessionEncoder sessionEncoder;
-	private final SessionParser sessionParser;
-	private final IntroducerProtocolEngine introducerEngine;
-	private final IntroduceeProtocolEngine introduceeEngine;
-	private final IntroductionCrypto crypto;
-	private final IdentityManager identityManager;
-
-	@Inject
-	IntroductionManagerImpl(
-			DatabaseComponent db,
-			ClientHelper clientHelper,
-			MetadataParser metadataParser,
-			MessageTracker messageTracker,
-			ContactGroupFactory contactGroupFactory,
-			MessageParser messageParser,
-			SessionEncoder sessionEncoder,
-			SessionParser sessionParser,
-			IntroducerProtocolEngine introducerEngine,
-			IntroduceeProtocolEngine introduceeEngine,
-			IntroductionCrypto crypto,
-			IdentityManager identityManager) {
-		super(db, clientHelper, metadataParser, messageTracker);
-		this.contactGroupFactory = contactGroupFactory;
-		this.messageParser = messageParser;
-		this.sessionEncoder = sessionEncoder;
-		this.sessionParser = sessionParser;
-		this.introducerEngine = introducerEngine;
-		this.introduceeEngine = introduceeEngine;
-		this.crypto = crypto;
-		this.identityManager = identityManager;
-	}
-
-	@Override
-	public void createLocalState(Transaction txn) throws DbException {
-		// Create a local group to store protocol sessions
-		Group localGroup = getLocalGroup();
-		if (db.containsGroup(txn, localGroup.getId())) return;
-		db.addGroup(txn, localGroup);
-		// Set up groups for communication with any pre-existing contacts
-		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
-	}
-
-	@Override
-	// TODO adapt to use upcoming ClientVersioning client
-	public void addingContact(Transaction txn, Contact c) throws DbException {
-		// Create a group to share with the contact
-		Group g = getContactGroup(c);
-		// Return if we've already set things up for this contact
-		if (db.containsGroup(txn, g.getId())) return;
-		// Store the group and share it with the contact
-		db.addGroup(txn, g);
-		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
-		// Attach the contact ID to the group
-		BdfDictionary meta = new BdfDictionary();
-		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
-		try {
-			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
-		} catch (FormatException e) {
-			throw new AssertionError(e);
-		}
-	}
-
-	@Override
-	public void removingContact(Transaction txn, Contact c) throws DbException {
-		// Remove the contact group (all messages will be removed with it)
-		db.removeGroup(txn, getContactGroup(c));
-		// TODO abort other sessions the contact is involved in
-	}
-
-	@Override
-	public Group getContactGroup(Contact c) {
-		return contactGroupFactory
-				.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
-	}
-
-	@Override
-	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
-			BdfDictionary bdfMeta) throws DbException, FormatException {
-		// Parse the metadata
-		MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
-		// Look up the session, if there is one
-		SessionId sessionId = meta.getSessionId();
-		IntroduceeSession newIntroduceeSession = null;
-		if (sessionId == null) {
-			if (meta.getMessageType() != REQUEST) throw new AssertionError();
-			newIntroduceeSession = createNewIntroduceeSession(txn, m, body);
-			sessionId = newIntroduceeSession.getSessionId();
-		}
-		StoredSession ss = getSession(txn, sessionId);
-		// Handle the message
-		Session session;
-		MessageId storageId;
-		if (ss == null) {
-			if (meta.getMessageType() != REQUEST) throw new FormatException();
-			if (newIntroduceeSession == null) throw new AssertionError();
-			storageId = createStorageId(txn);
-			session = handleMessage(txn, m, body, meta.getMessageType(),
-					newIntroduceeSession, introduceeEngine);
-		} else {
-			storageId = ss.storageId;
-			Role role = sessionParser.getRole(ss.bdfSession);
-			if (role == INTRODUCER) {
-				session = handleMessage(txn, m, body, meta.getMessageType(),
-						sessionParser.parseIntroducerSession(ss.bdfSession),
-						introducerEngine);
-			} else if (role == INTRODUCEE) {
-				session = handleMessage(txn, m, body, meta.getMessageType(),
-						sessionParser.parseIntroduceeSession(m.getGroupId(),
-								ss.bdfSession), introduceeEngine);
-			} else throw new AssertionError();
-		}
-		// Store the updated session
-		storeSession(txn, storageId, session);
-		return false;
-	}
-
-	private IntroduceeSession createNewIntroduceeSession(Transaction txn,
-			Message m, BdfList body) throws DbException, FormatException {
-		ContactId introducerId = getContactId(txn, m.getGroupId());
-		Author introducer = db.getContact(txn, introducerId).getAuthor();
-		Author alice = identityManager.getLocalAuthor(txn);
-		Author bob = messageParser.parseRequestMessage(m, body).getAuthor();
-		SessionId sessionId = crypto.getSessionId(introducer, alice, bob);
-		return IntroduceeSession
-				.getInitial(m.getGroupId(), sessionId, introducer, bob);
-	}
-
-	private <S extends Session> S handleMessage(Transaction txn, Message m,
-			BdfList body, MessageType type, S session, ProtocolEngine<S> engine)
-			throws DbException, FormatException {
-		if (type == REQUEST) {
-			RequestMessage request = messageParser.parseRequestMessage(m, body);
-			return engine.onRequestMessage(txn, session, request);
-		} else if (type == ACCEPT) {
-			AcceptMessage accept = messageParser.parseAcceptMessage(m, body);
-			return engine.onAcceptMessage(txn, session, accept);
-		} else if (type == DECLINE) {
-			DeclineMessage decline = messageParser.parseDeclineMessage(m, body);
-			return engine.onDeclineMessage(txn, session, decline);
-		} else if (type == AUTH) {
-			AuthMessage auth = messageParser.parseAuthMessage(m, body);
-			return engine.onAuthMessage(txn, session, auth);
-		} else if (type == ACTIVATE) {
-			ActivateMessage activate =
-					messageParser.parseActivateMessage(m, body);
-			return engine.onActivateMessage(txn, session, activate);
-		} else if (type == ABORT) {
-			AbortMessage abort = messageParser.parseAbortMessage(m, body);
-			return engine.onAbortMessage(txn, session, abort);
-		} else {
-			throw new AssertionError();
-		}
-	}
-
-	@Nullable
-	private StoredSession getSession(Transaction txn,
-			@Nullable SessionId sessionId) throws DbException, FormatException {
-		if (sessionId == null) return null;
-		BdfDictionary query = sessionParser.getSessionQuery(sessionId);
-		Map<MessageId, BdfDictionary> results = clientHelper
-				.getMessageMetadataAsDictionary(txn, getLocalGroup().getId(),
-						query);
-		if (results.size() > 1) throw new DbException();
-		if (results.isEmpty()) return null;
-		return new StoredSession(results.keySet().iterator().next(),
-				results.values().iterator().next());
-	}
-
-	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
-			throws DbException, FormatException {
-		BdfDictionary meta =
-				clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId);
-		return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
-	}
-
-	private MessageId createStorageId(Transaction txn) throws DbException {
-		Message m = clientHelper
-				.createMessageForStoringMetadata(getLocalGroup().getId());
-		db.addLocalMessage(txn, m, new Metadata(), false);
-		return m.getId();
-	}
-
-	private void storeSession(Transaction txn, MessageId storageId,
-			Session session) throws DbException, FormatException {
-		BdfDictionary d;
-		if (session.getRole() == INTRODUCER) {
-			d = sessionEncoder
-					.encodeIntroducerSession((IntroducerSession) session);
-		} else if (session.getRole() == INTRODUCEE) {
-			d = sessionEncoder
-					.encodeIntroduceeSession((IntroduceeSession) session);
-		} else {
-			throw new AssertionError();
-		}
-		clientHelper.mergeMessageMetadata(txn, storageId, d);
-	}
-
-	@Override
-	public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
-			long timestamp) throws DbException {
-		Transaction txn = db.startTransaction(false);
-		try {
-			// Look up the session, if there is one
-			Author introducer = identityManager.getLocalAuthor(txn);
-			SessionId sessionId =
-					crypto.getSessionId(introducer, c1.getAuthor(),
-							c2.getAuthor());
-			StoredSession ss = getSession(txn, sessionId);
-			// Create or parse the session
-			IntroducerSession session;
-			MessageId storageId;
-			if (ss == null) {
-				// This is the first request - create a new session
-				GroupId groupId1 = getContactGroup(c1).getId();
-				GroupId groupId2 = getContactGroup(c2).getId();
-				session = new IntroducerSession(sessionId, groupId1,
-						c1.getAuthor(), groupId2, c2.getAuthor());
-				storageId = createStorageId(txn);
-			} else {
-				// An earlier request exists, so we already have a session
-				session = sessionParser.parseIntroducerSession(ss.bdfSession);
-				storageId = ss.storageId;
-			}
-			// Handle the request action
-			session = introducerEngine
-					.onRequestAction(txn, session, msg, timestamp);
-			// Store the updated session
-			storeSession(txn, storageId, session);
-			db.commitTransaction(txn);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		} finally {
-			db.endTransaction(txn);
-		}
-	}
-
-	@Override
-	public void acceptIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException {
-		respondToRequest(contactId, sessionId, timestamp, true);
-	}
-
-	@Override
-	public void declineIntroduction(ContactId contactId, SessionId sessionId,
-			long timestamp) throws DbException {
-		respondToRequest(contactId, sessionId, timestamp, false);
-	}
-
-	private void respondToRequest(ContactId contactId, SessionId sessionId,
-			long timestamp, boolean accept) throws DbException {
-		Transaction txn = db.startTransaction(false);
-		try {
-			// Look up the session
-			StoredSession ss = getSession(txn, sessionId);
-			if (ss == null) throw new IllegalArgumentException();
-			// Parse the session
-			Contact contact = db.getContact(txn, contactId);
-			GroupId contactGroupId = getContactGroup(contact).getId();
-			IntroduceeSession session = sessionParser
-					.parseIntroduceeSession(contactGroupId, ss.bdfSession);
-			// Handle the join or leave action
-			if (accept) {
-				session = introduceeEngine
-						.onAcceptAction(txn, session, timestamp);
-			} else {
-				session = introduceeEngine
-						.onDeclineAction(txn, session, timestamp);
-			}
-			// Store the updated session
-			storeSession(txn, ss.storageId, session);
-			db.commitTransaction(txn);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		} finally {
-			db.endTransaction(txn);
-		}
-	}
-
-	@Override
-	public Collection<IntroductionMessage> getIntroductionMessages(ContactId c)
-			throws DbException {
-		List<IntroductionMessage> messages;
-		Transaction txn = db.startTransaction(true);
-		try {
-			Contact contact = db.getContact(txn, c);
-			GroupId contactGroupId = getContactGroup(contact).getId();
-			BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
-			Map<MessageId, BdfDictionary> results = clientHelper
-					.getMessageMetadataAsDictionary(txn, contactGroupId, query);
-			messages = new ArrayList<>(results.size());
-			for (Map.Entry<MessageId, BdfDictionary> e : results.entrySet()) {
-				MessageId m = e.getKey();
-				MessageMetadata meta =
-						messageParser.parseMetadata(e.getValue());
-				MessageStatus status = db.getMessageStatus(txn, c, m);
-				StoredSession ss = getSession(txn, meta.getSessionId());
-				if (ss == null) throw new AssertionError();
-				MessageType type = meta.getMessageType();
-				if (type == REQUEST) {
-					messages.add(
-							parseInvitationRequest(txn, contactGroupId, m,
-									meta, status, ss.bdfSession));
-				} else if (type == ACCEPT) {
-					messages.add(
-							parseInvitationResponse(txn, contactGroupId, m,
-									meta, status, ss.bdfSession, true));
-				} else if (type == DECLINE) {
-					messages.add(
-							parseInvitationResponse(txn, contactGroupId, m,
-									meta, status, ss.bdfSession, false));
-				}
-			}
-			db.commitTransaction(txn);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		} finally {
-			db.endTransaction(txn);
-		}
-		return messages;
-	}
-
-	private IntroductionRequest parseInvitationRequest(Transaction txn,
-			GroupId contactGroupId, MessageId m, MessageMetadata meta,
-			MessageStatus status, BdfDictionary bdfSession)
-			throws DbException, FormatException {
-		Role role = sessionParser.getRole(bdfSession);
-		SessionId sessionId;
-		Author author;
-		if (role == INTRODUCER) {
-			IntroducerSession session =
-					sessionParser.parseIntroducerSession(bdfSession);
-			sessionId = session.getSessionId();
-			LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
-			if (localAuthor.equals(session.getIntroducee1().author)) {
-				author = session.getIntroducee2().author;
-			} else {
-				author = session.getIntroducee1().author;
-			}
-		} else if (role == INTRODUCEE) {
-			IntroduceeSession session = sessionParser
-					.parseIntroduceeSession(contactGroupId, bdfSession);
-			sessionId = session.getSessionId();
-			author = session.getRemoteAuthor();
-		} else throw new AssertionError();
-		String message = ""; // TODO
-		boolean contactExists = false; // TODO
-
-		return new IntroductionRequest(sessionId, m, contactGroupId,
-				role, meta.getTimestamp(), meta.isLocal(),
-				status.isSent(), status.isSeen(), meta.isRead(),
-				author.getName(), false, message, !meta.isAvailableToAnswer(),
-				contactExists);
-	}
-
-	private IntroductionResponse parseInvitationResponse(Transaction txn,
-			GroupId contactGroupId, MessageId m, MessageMetadata meta,
-			MessageStatus status, BdfDictionary bdfSession, boolean accept)
-			throws FormatException, DbException {
-		Role role = sessionParser.getRole(bdfSession);
-		SessionId sessionId;
-		Author author;
-		if (role == INTRODUCER) {
-			IntroducerSession session =
-					sessionParser.parseIntroducerSession(bdfSession);
-			sessionId = session.getSessionId();
-			LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
-			if (localAuthor.equals(session.getIntroducee1().author)) {
-				author = session.getIntroducee2().author;
-			} else {
-				author = session.getIntroducee1().author;
-			}
-		} else if (role == INTRODUCEE) {
-			IntroduceeSession session = sessionParser
-					.parseIntroduceeSession(contactGroupId, bdfSession);
-			sessionId = session.getSessionId();
-			author = session.getRemoteAuthor();
-		} else throw new AssertionError();
-		return new IntroductionResponse(sessionId, m, contactGroupId,
-				role, meta.getTimestamp(), meta.isLocal(), status.isSent(),
-				status.isSeen(), meta.isRead(), author.getName(), accept);
-	}
-
-	private Group getLocalGroup() {
-		return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
-	}
-
-	private static class StoredSession {
-
-		private final MessageId storageId;
-		private final BdfDictionary bdfSession;
-
-		private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
-			this.storageId = storageId;
-			this.bdfSession = bdfSession;
-		}
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionModule.java
deleted file mode 100644
index 1a64538ea11db9ce614d96a50336a5e1e20007d5..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionModule.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.briarproject.briar.introduction2;
-
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.data.MetadataEncoder;
-import org.briarproject.bramble.api.sync.ValidationManager;
-import org.briarproject.bramble.api.system.Clock;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-import dagger.Module;
-import dagger.Provides;
-
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-
-@Module
-public class IntroductionModule {
-
-	public static class EagerSingletons {
-		@Inject
-		IntroductionValidator introductionValidator;
-	}
-
-	@Provides
-	@Singleton
-	IntroductionValidator provideValidator(ValidationManager validationManager,
-			MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
-			ClientHelper clientHelper, Clock clock) {
-
-		IntroductionValidator introductionValidator =
-				new IntroductionValidator(messageEncoder, clientHelper,
-						metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID,
-				introductionValidator);
-
-		return introductionValidator;
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionValidator.java
deleted file mode 100644
index 1bdf07ff640a0264385d535361aa5862e4238407..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionValidator.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package org.briarproject.briar.introduction2;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.UniqueId;
-import org.briarproject.bramble.api.client.BdfMessageContext;
-import org.briarproject.bramble.api.client.BdfMessageValidator;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataEncoder;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.SessionId;
-
-import java.util.Collections;
-
-import javax.annotation.concurrent.Immutable;
-
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
-import static org.briarproject.bramble.util.ValidationUtils.checkLength;
-import static org.briarproject.bramble.util.ValidationUtils.checkSize;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
-import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
-import static org.briarproject.briar.introduction2.MessageType.AUTH;
-
-
-@Immutable
-@NotNullByDefault
-class IntroductionValidator extends BdfMessageValidator {
-
-	private final MessageEncoder messageEncoder;
-
-	IntroductionValidator(MessageEncoder messageEncoder,
-			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
-			Clock clock) {
-		super(clientHelper, metadataEncoder, clock);
-		this.messageEncoder = messageEncoder;
-	}
-
-	@Override
-	protected BdfMessageContext validateMessage(Message m, Group g,
-			BdfList body) throws FormatException {
-		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
-
-		switch (type) {
-			case REQUEST:
-				return validateRequestMessage(m, body);
-			case ACCEPT:
-				return validateAcceptMessage(m, body);
-			case AUTH:
-				return validateAuthMessage(m, body);
-			case DECLINE:
-			case ACTIVATE:
-			case ABORT:
-				return validateOtherMessage(type, m, body);
-			default:
-				throw new FormatException();
-		}
-	}
-
-	private BdfMessageContext validateRequestMessage(Message m, BdfList body)
-			throws FormatException {
-		checkSize(body, 4);
-
-		byte[] previousMessageId = body.getOptionalRaw(1);
-		checkLength(previousMessageId, UniqueId.LENGTH);
-
-		BdfList authorList = body.getList(2);
-		clientHelper.parseAndValidateAuthor(authorList);
-
-		String msg = body.getOptionalString(3);
-		checkLength(msg, 1, MAX_REQUEST_MESSAGE_LENGTH);
-
-		BdfDictionary meta = messageEncoder
-				.encodeRequestMetadata(m.getTimestamp(), false, false,
-						false, false);
-		if (previousMessageId == null) {
-			return new BdfMessageContext(meta);
-		} else {
-			MessageId dependency = new MessageId(previousMessageId);
-			return new BdfMessageContext(meta,
-					Collections.singletonList(dependency));
-		}
-	}
-
-	private BdfMessageContext validateAcceptMessage(Message m, BdfList body)
-			throws FormatException {
-		checkSize(body, 6);
-
-		byte[] sessionIdBytes = body.getRaw(1);
-		checkLength(sessionIdBytes, UniqueId.LENGTH);
-
-		byte[] previousMessageId = body.getRaw(2);
-		checkLength(previousMessageId, UniqueId.LENGTH);
-
-		byte[] ephemeralPublicKey = body.getRaw(3);
-		checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
-
-		body.getLong(4);
-
-		BdfDictionary transportProperties = body.getDictionary(5);
-		if (transportProperties.size() < 1) throw new FormatException();
-		for (String tId : transportProperties.keySet()) {
-			checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH);
-			BdfDictionary tProps = transportProperties.getDictionary(tId);
-			clientHelper.parseAndValidateTransportProperties(tProps);
-		}
-
-		SessionId sessionId = new SessionId(sessionIdBytes);
-		BdfDictionary meta = messageEncoder
-				.encodeMetadata(ACCEPT, sessionId, m.getTimestamp(), false,
-						false, false);
-		MessageId dependency = new MessageId(previousMessageId);
-		return new BdfMessageContext(meta,
-				Collections.singletonList(dependency));
-	}
-
-	private BdfMessageContext validateAuthMessage(Message m, BdfList body)
-			throws FormatException {
-		checkSize(body, 5);
-
-		byte[] sessionIdBytes = body.getRaw(1);
-		checkLength(sessionIdBytes, UniqueId.LENGTH);
-
-		byte[] previousMessageId = body.getRaw(2);
-		checkLength(previousMessageId, UniqueId.LENGTH);
-
-		byte[] mac = body.getRaw(3);
-		checkLength(mac, MAC_BYTES);
-
-		byte[] signature = body.getRaw(4);
-		checkLength(signature, 1, MAX_SIGNATURE_BYTES);
-
-		SessionId sessionId = new SessionId(sessionIdBytes);
-		BdfDictionary meta = messageEncoder
-				.encodeMetadata(AUTH, sessionId, m.getTimestamp(), false, false,
-						false);
-		MessageId dependency = new MessageId(previousMessageId);
-		return new BdfMessageContext(meta,
-				Collections.singletonList(dependency));
-	}
-
-	private BdfMessageContext validateOtherMessage(MessageType type,
-			Message m, BdfList body) throws FormatException {
-		checkSize(body, 3);
-
-		byte[] sessionIdBytes = body.getRaw(1);
-		checkLength(sessionIdBytes, UniqueId.LENGTH);
-
-		byte[] previousMessageId = body.getRaw(2);
-		checkLength(previousMessageId, UniqueId.LENGTH);
-
-		SessionId sessionId = new SessionId(sessionIdBytes);
-		BdfDictionary meta = messageEncoder
-				.encodeMetadata(type, sessionId, m.getTimestamp(), false, false,
-						false);
-		MessageId dependency = new MessageId(previousMessageId);
-		return new BdfMessageContext(meta,
-				Collections.singletonList(dependency));
-	}
-
-}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java
deleted file mode 100644
index 1e1d46e0a6a352addf38f7cb524042a3b21fe08e..0000000000000000000000000000000000000000
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.briarproject.briar.introduction2;
-
-interface State {
-
-	int getValue();
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java
deleted file mode 100644
index 5f8391d9b8be793e3c53290c1b6ad3be1d18e3b5..0000000000000000000000000000000000000000
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java
+++ /dev/null
@@ -1,424 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.Bytes;
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.contact.ContactManager;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfEntry;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.AuthorFactory;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.identity.IdentityManager;
-import org.briarproject.bramble.api.properties.TransportPropertyManager;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.GroupId;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.introduction.IntroduceeProtocolState;
-import org.briarproject.briar.test.BriarTestCase;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.jmock.lib.legacy.ClassImposteriser;
-import org.junit.Test;
-
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
-import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getAuthor;
-import static org.briarproject.bramble.test.TestUtils.getGroup;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.bramble.test.TestUtils.getSecretKey;
-import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ADDED_CONTACT_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-import static org.hamcrest.Matchers.array;
-import static org.hamcrest.Matchers.samePropertyValuesAs;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class IntroduceeManagerTest extends BriarTestCase {
-
-	private final Mockery context;
-	private final IntroduceeManager introduceeManager;
-	private final DatabaseComponent db;
-	private final CryptoComponent cryptoComponent;
-	private final ClientHelper clientHelper;
-	private final IntroductionGroupFactory introductionGroupFactory;
-	private final AuthorFactory authorFactory;
-	private final ContactManager contactManager;
-	private final Clock clock;
-	private final Contact introducer;
-	private final Contact introducee1;
-	private final Contact introducee2;
-	private final Group localGroup1;
-	private final Group introductionGroup1;
-	private final Transaction txn;
-	private final long time = 42L;
-	private final Message localStateMessage;
-	private final SessionId sessionId;
-	private final Message message1;
-
-	public IntroduceeManagerTest() {
-		context = new Mockery();
-		context.setImposteriser(ClassImposteriser.INSTANCE);
-		MessageSender messageSender = context.mock(MessageSender.class);
-		db = context.mock(DatabaseComponent.class);
-		cryptoComponent = context.mock(CryptoComponent.class);
-		clientHelper = context.mock(ClientHelper.class);
-		clock = context.mock(Clock.class);
-		introductionGroupFactory =
-				context.mock(IntroductionGroupFactory.class);
-		TransportPropertyManager transportPropertyManager =
-				context.mock(TransportPropertyManager.class);
-		authorFactory = context.mock(AuthorFactory.class);
-		contactManager = context.mock(ContactManager.class);
-		IdentityManager identityManager = context.mock(IdentityManager.class);
-
-		introduceeManager = new IntroduceeManager(messageSender, db,
-				clientHelper, clock, cryptoComponent, transportPropertyManager,
-				authorFactory, contactManager, identityManager,
-				introductionGroupFactory);
-
-		Author author0 = getAuthor();
-		AuthorId localAuthorId = new AuthorId(getRandomId());
-		ContactId contactId0 = new ContactId(234);
-		introducer =
-				new Contact(contactId0, author0, localAuthorId, true, true);
-
-		Author author1 = getAuthor();
-		AuthorId localAuthorId1 = new AuthorId(getRandomId());
-		ContactId contactId1 = new ContactId(234);
-		introducee1 =
-				new Contact(contactId1, author1, localAuthorId1, true, true);
-
-		Author author2 = getAuthor();
-		ContactId contactId2 = new ContactId(235);
-		introducee2 =
-				new Contact(contactId2, author2, localAuthorId, true, true);
-
-		localGroup1 = getGroup(CLIENT_ID);
-		introductionGroup1 = getGroup(CLIENT_ID);
-
-		sessionId = new SessionId(getRandomId());
-		localStateMessage = new Message(
-				new MessageId(getRandomId()),
-				localGroup1.getId(),
-				time,
-				getRandomBytes(MESSAGE_HEADER_LENGTH + 1)
-		);
-		message1 = new Message(
-				new MessageId(getRandomId()),
-				introductionGroup1.getId(),
-				time,
-				getRandomBytes(MESSAGE_HEADER_LENGTH + 1)
-		);
-
-		txn = new Transaction(null, false);
-	}
-
-	@Test
-	public void testIncomingRequestMessage()
-			throws DbException, FormatException {
-
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_REQUEST);
-		msg.put(GROUP_ID, introductionGroup1.getId());
-		msg.put(SESSION_ID, sessionId);
-		msg.put(MESSAGE_ID, message1.getId());
-		msg.put(MESSAGE_TIME, time);
-		msg.put(NAME, introducee2.getAuthor().getName());
-		msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey());
-
-		BdfDictionary state =
-				initializeSessionState(txn, introductionGroup1.getId(), msg);
-
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).mergeMessageMetadata(txn,
-					localStateMessage.getId(), state);
-		}});
-
-		introduceeManager.incomingMessage(txn, state, msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-	@Test
-	public void testIncomingResponseMessage()
-			throws DbException, FormatException {
-
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_RESPONSE);
-		msg.put(GROUP_ID, introductionGroup1.getId());
-		msg.put(SESSION_ID, sessionId);
-		msg.put(MESSAGE_ID, message1.getId());
-		msg.put(MESSAGE_TIME, time);
-		msg.put(NAME, introducee2.getAuthor().getName());
-		msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey());
-
-		BdfDictionary state =
-				initializeSessionState(txn, introductionGroup1.getId(), msg);
-		state.put(STATE, IntroduceeProtocolState.AWAIT_RESPONSES.ordinal());
-
-		// turn request message into a response
-		msg.put(ACCEPT, true);
-		msg.put(TIME, time);
-		msg.put(E_PUBLIC_KEY, getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES));
-		msg.put(TRANSPORT, new BdfDictionary());
-
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).mergeMessageMetadata(txn,
-					localStateMessage.getId(), state);
-		}});
-
-		introduceeManager.incomingMessage(txn, state, msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-	@Test
-	public void testDetectReplacedEphemeralPublicKey()
-			throws DbException, FormatException, GeneralSecurityException {
-
-		// TODO MR !237 should use its new default initialization method here
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_RESPONSE);
-		msg.put(GROUP_ID, introductionGroup1.getId());
-		msg.put(SESSION_ID, sessionId);
-		msg.put(MESSAGE_ID, message1.getId());
-		msg.put(MESSAGE_TIME, time);
-		msg.put(NAME, introducee2.getAuthor().getName());
-		msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey());
-		BdfDictionary state =
-				initializeSessionState(txn, introductionGroup1.getId(), msg);
-
-		// prepare state for incoming ACK
-		state.put(STATE, IntroduceeProtocolState.AWAIT_ACK.ordinal());
-		state.put(ADDED_CONTACT_ID, 2);
-		byte[] nonce = getRandomBytes(42);
-		state.put(NONCE, nonce);
-		state.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey());
-
-		// create incoming ACK message
-		byte[] mac = getRandomBytes(MAC_LENGTH);
-		byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH);
-		BdfDictionary ack = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_ACK),
-				new BdfEntry(SESSION_ID, sessionId),
-				new BdfEntry(GROUP_ID, introductionGroup1.getId()),
-				new BdfEntry(MAC, mac),
-				new BdfEntry(SIGNATURE, sig)
-		);
-
-		context.checking(new Expectations() {{
-			oneOf(cryptoComponent).verifySignature(sig, SIGNING_LABEL, nonce,
-					introducee2.getAuthor().getPublicKey());
-			will(returnValue(false));
-		}});
-
-		try {
-			introduceeManager.incomingMessage(txn, state, ack);
-			fail();
-		} catch (DbException e) {
-			// expected
-			assertTrue(e.getCause() instanceof GeneralSecurityException);
-		}
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-	@Test
-	public void testSignatureVerification()
-			throws FormatException, DbException, GeneralSecurityException {
-
-		byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey();
-		byte[] nonce = getRandomBytes(MAC_LENGTH);
-		byte[] sig = getRandomBytes(MAC_LENGTH);
-
-		BdfDictionary state = new BdfDictionary();
-		state.put(PUBLIC_KEY, publicKeyBytes);
-		state.put(NONCE, nonce);
-		state.put(SIGNATURE, sig);
-
-		context.checking(new Expectations() {{
-			oneOf(cryptoComponent).verifySignature(sig, SIGNING_LABEL, nonce,
-					publicKeyBytes);
-			will(returnValue(true));
-		}});
-		introduceeManager.verifySignature(state);
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testMacVerification()
-			throws FormatException, DbException, GeneralSecurityException {
-
-		byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey();
-		BdfDictionary tp = BdfDictionary.of(new BdfEntry("fake", "fake"));
-		byte[] ePublicKeyBytes = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
-		byte[] mac = getRandomBytes(MAC_LENGTH);
-		SecretKey macKey = getSecretKey();
-
-		// move state to where it would be after an ACK arrived
-		BdfDictionary state = new BdfDictionary();
-		state.put(PUBLIC_KEY, publicKeyBytes);
-		state.put(TRANSPORT, tp);
-		state.put(TIME, time);
-		state.put(E_PUBLIC_KEY, ePublicKeyBytes);
-		state.put(MAC, mac);
-		state.put(MAC_KEY, macKey.getBytes());
-
-		byte[] signBytes = getRandomBytes(42);
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).toByteArray(
-					BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time));
-			will(returnValue(signBytes));
-			//noinspection unchecked
-			oneOf(cryptoComponent).mac(with(MAC_LABEL),
-					with(samePropertyValuesAs(macKey)),
-					with(array(equal(signBytes))));
-			will(returnValue(mac));
-		}});
-		introduceeManager.verifyMac(state);
-		context.assertIsSatisfied();
-
-		// now produce wrong MAC
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).toByteArray(
-					BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time));
-			will(returnValue(signBytes));
-			//noinspection unchecked
-			oneOf(cryptoComponent).mac(with(MAC_LABEL),
-					with(samePropertyValuesAs(macKey)),
-					with(array(equal(signBytes))));
-			will(returnValue(getRandomBytes(MAC_LENGTH)));
-		}});
-		try {
-			introduceeManager.verifyMac(state);
-			fail();
-		} catch (GeneralSecurityException e) {
-			// expected
-		}
-		context.assertIsSatisfied();
-	}
-
-	private BdfDictionary initializeSessionState(Transaction txn,
-			GroupId groupId, BdfDictionary msg)
-			throws DbException, FormatException {
-
-		SecureRandom secureRandom = context.mock(SecureRandom.class);
-		Bytes salt = new Bytes(new byte[64]);
-		BdfDictionary groupMetadata = BdfDictionary.of(
-				new BdfEntry(CONTACT, introducee1.getId().getInt())
-		);
-		boolean contactExists = false;
-		BdfDictionary state = new BdfDictionary();
-		state.put(STORAGE_ID, localStateMessage.getId());
-		state.put(STATE, AWAIT_REQUEST.getValue());
-		state.put(ROLE, ROLE_INTRODUCEE);
-		state.put(GROUP_ID, groupId);
-		state.put(INTRODUCER, introducer.getAuthor().getName());
-		state.put(CONTACT_ID_1, introducer.getId().getInt());
-		state.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes());
-		state.put(NOT_OUR_RESPONSE, localStateMessage.getId());
-		state.put(ANSWERED, false);
-		state.put(EXISTS, contactExists);
-		state.put(REMOTE_AUTHOR_ID, introducee2.getAuthor().getId());
-		state.put(REMOTE_AUTHOR_IS_US, false);
-
-		context.checking(new Expectations() {{
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(time));
-			oneOf(cryptoComponent).getSecureRandom();
-			will(returnValue(secureRandom));
-			oneOf(secureRandom).nextBytes(salt.getBytes());
-			oneOf(introductionGroupFactory).createLocalGroup();
-			will(returnValue(localGroup1));
-			oneOf(clientHelper)
-					.createMessage(localGroup1.getId(), time, BdfList.of(salt));
-			will(returnValue(localStateMessage));
-
-			// who is making the introduction? who is the introducer?
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					groupId);
-			will(returnValue(groupMetadata));
-			oneOf(db).getContact(txn, introducer.getId());
-			will(returnValue(introducer));
-
-			// create remote author to check if contact exists
-			oneOf(authorFactory).createAuthor(introducee2.getAuthor().getName(),
-					introducee2.getAuthor().getPublicKey());
-			will(returnValue(introducee2.getAuthor()));
-			oneOf(contactManager)
-					.contactExists(txn, introducee2.getAuthor().getId(),
-							introducer.getLocalAuthorId());
-			will(returnValue(contactExists));
-
-			// checks if remote author is one of our identities
-			oneOf(db).containsLocalAuthor(txn, introducee2.getAuthor().getId());
-			will(returnValue(false));
-
-			// store session state
-			oneOf(clientHelper)
-					.addLocalMessage(txn, localStateMessage, state, false);
-		}});
-
-		BdfDictionary result = introduceeManager.initialize(txn, groupId, msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-		return result;
-	}
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java
deleted file mode 100644
index 558b26ec9b2fdb9452676dc54e4b86ff8bf2418c..0000000000000000000000000000000000000000
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.Bytes;
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.test.BriarTestCase;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.jmock.lib.legacy.ClassImposteriser;
-import org.junit.Test;
-
-import java.security.SecureRandom;
-
-import static org.briarproject.bramble.test.TestUtils.getAuthor;
-import static org.briarproject.bramble.test.TestUtils.getGroup;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES;
-import static org.briarproject.briar.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-import static org.junit.Assert.assertFalse;
-
-public class IntroducerManagerTest extends BriarTestCase {
-
-	private final Mockery context;
-	private final IntroducerManager introducerManager;
-	private final CryptoComponent cryptoComponent;
-	private final ClientHelper clientHelper;
-	private final IntroductionGroupFactory introductionGroupFactory;
-	private final MessageSender messageSender;
-	private final Clock clock;
-	private final Contact introducee1;
-	private final Contact introducee2;
-	private final Group localGroup0;
-	private final Group introductionGroup1;
-	private final Group introductionGroup2;
-
-	public IntroducerManagerTest() {
-		context = new Mockery();
-		context.setImposteriser(ClassImposteriser.INSTANCE);
-		messageSender = context.mock(MessageSender.class);
-		cryptoComponent = context.mock(CryptoComponent.class);
-		clientHelper = context.mock(ClientHelper.class);
-		clock = context.mock(Clock.class);
-		introductionGroupFactory =
-				context.mock(IntroductionGroupFactory.class);
-
-		introducerManager =
-				new IntroducerManager(messageSender, clientHelper, clock,
-						cryptoComponent, introductionGroupFactory);
-
-		Author author1 = getAuthor();
-		AuthorId localAuthorId1 = new AuthorId(getRandomId());
-		ContactId contactId1 = new ContactId(234);
-		introducee1 =
-				new Contact(contactId1, author1, localAuthorId1, true, true);
-
-		Author author2 = getAuthor();
-		AuthorId localAuthorId2 = new AuthorId(getRandomId());
-		ContactId contactId2 = new ContactId(235);
-		introducee2 =
-				new Contact(contactId2, author2, localAuthorId2, true, true);
-
-		localGroup0 = getGroup(CLIENT_ID);
-		introductionGroup1 = getGroup(CLIENT_ID);
-		introductionGroup2 = getGroup(CLIENT_ID);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testMakeIntroduction() throws DbException, FormatException {
-		Transaction txn = new Transaction(null, false);
-		long time = 42L;
-		context.setImposteriser(ClassImposteriser.INSTANCE);
-		SecureRandom secureRandom = context.mock(SecureRandom.class);
-		Bytes salt = new Bytes(new byte[64]);
-		Message msg = new Message(new MessageId(getRandomId()),
-				localGroup0.getId(), time, getRandomBytes(64));
-		BdfDictionary state = new BdfDictionary();
-		state.put(SESSION_ID, msg.getId());
-		state.put(STORAGE_ID, msg.getId());
-		state.put(STATE, PREPARE_REQUESTS.getValue());
-		state.put(ROLE, ROLE_INTRODUCER);
-		state.put(GROUP_ID_1, introductionGroup1.getId());
-		state.put(GROUP_ID_2, introductionGroup2.getId());
-		state.put(CONTACT_1, introducee1.getAuthor().getName());
-		state.put(CONTACT_2, introducee2.getAuthor().getName());
-		state.put(CONTACT_ID_1, introducee1.getId().getInt());
-		state.put(CONTACT_ID_2, introducee2.getId().getInt());
-		state.put(AUTHOR_ID_1, introducee1.getAuthor().getId());
-		state.put(AUTHOR_ID_2, introducee2.getAuthor().getId());
-		BdfDictionary state2 = (BdfDictionary) state.clone();
-		state2.put(STATE, AWAIT_RESPONSES.getValue());
-
-		BdfDictionary msg1 = new BdfDictionary();
-		msg1.put(TYPE, TYPE_REQUEST);
-		msg1.put(SESSION_ID, state.getRaw(SESSION_ID));
-		msg1.put(GROUP_ID, state.getRaw(GROUP_ID_1));
-		msg1.put(NAME, state.getString(CONTACT_2));
-		msg1.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey());
-		BdfDictionary msg1send = (BdfDictionary) msg1.clone();
-		msg1send.put(MESSAGE_TIME, time);
-
-		BdfDictionary msg2 = new BdfDictionary();
-		msg2.put(TYPE, TYPE_REQUEST);
-		msg2.put(SESSION_ID, state.getRaw(SESSION_ID));
-		msg2.put(GROUP_ID, state.getRaw(GROUP_ID_2));
-		msg2.put(NAME, state.getString(CONTACT_1));
-		msg2.put(PUBLIC_KEY, introducee1.getAuthor().getPublicKey());
-		BdfDictionary msg2send = (BdfDictionary) msg2.clone();
-		msg2send.put(MESSAGE_TIME, time);
-
-		context.checking(new Expectations() {{
-			// initialize and store session state
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(time));
-			oneOf(cryptoComponent).getSecureRandom();
-			will(returnValue(secureRandom));
-			oneOf(secureRandom).nextBytes(salt.getBytes());
-			oneOf(introductionGroupFactory).createLocalGroup();
-			will(returnValue(localGroup0));
-			oneOf(clientHelper).createMessage(localGroup0.getId(), time,
-					BdfList.of(salt));
-			will(returnValue(msg));
-			oneOf(introductionGroupFactory)
-					.createIntroductionGroup(introducee1);
-			will(returnValue(introductionGroup1));
-			oneOf(introductionGroupFactory)
-					.createIntroductionGroup(introducee2);
-			will(returnValue(introductionGroup2));
-			oneOf(clientHelper).addLocalMessage(txn, msg, state, false);
-
-			// send message
-			oneOf(clientHelper).mergeMessageMetadata(txn, msg.getId(), state2);
-			oneOf(messageSender).sendMessage(txn, msg1send);
-			oneOf(messageSender).sendMessage(txn, msg2send);
-		}});
-
-		introducerManager
-				.makeIntroduction(txn, introducee1, introducee2, null, time);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoImplTest.java
similarity index 99%
rename from briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java
rename to briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoImplTest.java
index 57cadce0e267070243ed1e8e4cab363427cca8b5..b8c636400d1ee102312f0fea368315d22728a426 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoImplTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
similarity index 91%
rename from briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java
rename to briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
index 139c4ca40847ef5996471dfa9758e73a8be9d7ab..a28e8321e2ae8adf1904a7495f8a7a2b0f28d680 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -11,7 +11,7 @@ import org.junit.Test;
 
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_SESSION_ID;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID;
 import static org.junit.Assert.assertEquals;
 
 public class IntroductionCryptoTest extends BrambleMockTestCase {
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index 9b485e5c42b1ce13b9ab9414cb90eb48de20432d..6d13b80f197a56bb9ee04f3920a2bf53355afbad 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -6,23 +6,21 @@ import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.KeyPair;
-import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Metadata;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.test.TestDatabaseModule;
 import org.briarproject.briar.api.client.SessionId;
@@ -38,56 +36,35 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.concurrent.TimeoutException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getTransportId;
-import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.api.client.MessageQueueManager.QUEUE_STATE_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
+import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
+import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
 import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public class IntroductionIntegrationTest
 		extends BriarIntegrationTest<IntroductionIntegrationTestComponent> {
 
-	@Inject
-	IntroductionGroupFactory introductionGroupFactory;
-
 	// objects accessed from background threads need to be volatile
 	private volatile IntroductionManager introductionManager0;
 	private volatile IntroductionManager introductionManager1;
@@ -102,7 +79,7 @@ public class IntroductionIntegrationTest
 			Logger.getLogger(IntroductionIntegrationTest.class.getName());
 
 	interface StateVisitor {
-		boolean visit(BdfDictionary response);
+		AcceptMessage visit(AcceptMessage response);
 	}
 
 	@Before
@@ -151,50 +128,50 @@ public class IntroductionIntegrationTest
 				.makeIntroduction(introducee1, introducee2, "Hi!", time);
 
 		// check that messages are tracked properly
-		Group g1 = introductionGroupFactory
-				.createIntroductionGroup(introducee1);
-		Group g2 = introductionGroupFactory
-				.createIntroductionGroup(introducee2);
-		assertGroupCount(messageTracker0, g1.getId(), 1, 0, time);
-		assertGroupCount(messageTracker0, g2.getId(), 1, 0, time);
+		Group g1 = introductionManager0.getContactGroup(introducee1);
+		Group g2 = introductionManager0.getContactGroup(introducee2);
+		assertGroupCount(messageTracker0, g1.getId(), 1, 0);
+		assertGroupCount(messageTracker0, g2.getId(), 1, 0);
 
-		// sync first request message
+		// sync first REQUEST message
 		sync0To1(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener1.requestReceived);
 		assertGroupCount(messageTracker1, g1.getId(), 2, 1);
 
-		// sync second request message
+		// sync second REQUEST message
 		sync0To2(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener2.requestReceived);
 		assertGroupCount(messageTracker2, g2.getId(), 2, 1);
 
-		// sync first response
+		// sync first ACCEPT message
 		sync1To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response1Received);
 		assertGroupCount(messageTracker0, g1.getId(), 2, 1);
 
-		// sync second response
+		// sync second ACCEPT message
 		sync2To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response2Received);
 		assertGroupCount(messageTracker0, g2.getId(), 2, 1);
 
-		// sync forwarded responses to introducees
+		// sync forwarded ACCEPT messages to introducees
 		sync0To1(1, true);
 		sync0To2(1, true);
-		assertGroupCount(messageTracker1, g1.getId(), 2, 1);
-		assertGroupCount(messageTracker2, g2.getId(), 2, 1);
 
-		// sync first ACK and its forward
+		// sync first AUTH and its forward
 		sync1To0(1, true);
 		sync0To2(1, true);
 
-		// sync second ACK and its forward
-		sync2To0(1, true);
-		sync0To1(1, true);
+		// sync second AUTH and its forward as well as the following ACTIVATE
+		sync2To0(2, true);
+		sync0To1(2, true);
+
+		// sync first ACTIVATE and its forward
+		sync1To0(1, true);
+		sync0To2(1, true);
 
 		// wait for introduction to succeed
 		eventWaiter.await(TIMEOUT, 2);
@@ -269,10 +246,8 @@ public class IntroductionIntegrationTest
 		assertFalse(contactManager2
 				.contactExists(author1.getId(), author2.getId()));
 
-		Group g1 = introductionGroupFactory
-				.createIntroductionGroup(introducee1);
-		Group g2 = introductionGroupFactory
-				.createIntroductionGroup(introducee2);
+		Group g1 = introductionManager0.getContactGroup(introducee1);
+		Group g2 = introductionManager0.getContactGroup(introducee2);
 		assertEquals(2,
 				introductionManager0.getIntroductionMessages(contactId1From0)
 						.size());
@@ -290,6 +265,10 @@ public class IntroductionIntegrationTest
 				introductionManager2.getIntroductionMessages(contactId0From2)
 						.size());
 		assertGroupCount(messageTracker2, g2.getId(), 3, 2);
+
+		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertFalse(listener2.aborted);
 	}
 
 	@Test
@@ -342,6 +321,9 @@ public class IntroductionIntegrationTest
 		assertEquals(2,
 				introductionManager2.getIntroductionMessages(contactId0From2)
 						.size());
+		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertFalse(listener2.aborted);
 	}
 
 	@Test
@@ -393,6 +375,9 @@ public class IntroductionIntegrationTest
 		// since introducee2 was already in FINISHED state when
 		// introducee1's response arrived, she ignores and deletes it
 		assertDefaultUiMessages();
+		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertFalse(listener2.aborted);
 	}
 
 	@Test
@@ -432,6 +417,8 @@ public class IntroductionIntegrationTest
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response2Received);
 		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertFalse(listener2.aborted);
 	}
 
 	@Test
@@ -452,61 +439,11 @@ public class IntroductionIntegrationTest
 		// make really sure we don't have that request
 		assertTrue(introductionManager1.getIntroductionMessages(contactId0From1)
 				.isEmpty());
-	}
 
-	@Test
-	public void testSessionIdReuse() throws Exception {
-		addListeners(true, true);
-
-		// make introduction
-		long time = clock.currentTimeMillis();
-		introductionManager0
-				.makeIntroduction(contact1From0, contact2From0, "Hi!", time);
-
-		// sync first request message
-		sync0To1(1, true);
-		eventWaiter.await(TIMEOUT, 1);
-		assertTrue(listener1.requestReceived);
-
-		// get SessionId
-		List<IntroductionMessage> list = new ArrayList<>(
-				introductionManager1.getIntroductionMessages(contactId0From1));
-		assertEquals(2, list.size());
-		assertTrue(list.get(0) instanceof IntroductionRequest);
-		IntroductionRequest msg = (IntroductionRequest) list.get(0);
-		SessionId sessionId = msg.getSessionId();
-
-		// get contact group
-		Group group =
-				introductionGroupFactory.createIntroductionGroup(contact1From0);
-
-		// create new message with same SessionId
-		BdfDictionary d = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_REQUEST),
-				new BdfEntry(SESSION_ID, sessionId),
-				new BdfEntry(GROUP_ID, group.getId()),
-				new BdfEntry(NAME, getRandomString(42)),
-				new BdfEntry(PUBLIC_KEY, getRandomBytes(MAX_PUBLIC_KEY_LENGTH))
-		);
-
-		// reset request received state
-		listener1.requestReceived = false;
-
-		// add the message to the queue
-		MessageSender sender0 = c0.getMessageSender();
-		Transaction txn = db0.startTransaction(false);
-		try {
-			sender0.sendMessage(txn, d);
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
-
-		// actually send message
-		sync0To1(1, false);
-
-		// make sure it does not arrive
-		assertFalse(listener1.requestReceived);
+		// The message was invalid, so no abort message was sent
+		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertFalse(listener2.aborted);
 	}
 
 	@Test
@@ -523,34 +460,20 @@ public class IntroductionIntegrationTest
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener1.requestReceived);
 
-		// get database and local group for introducee
-		Group group1 = introductionGroupFactory.createLocalGroup();
+		// get local group for introducee1
+		Group group1 =
+				contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
 
-		// get local session state messages
-		Map<MessageId, Metadata> map;
-		Transaction txn = db1.startTransaction(false);
-		try {
-			map = db1.getMessageMetadata(txn, group1.getId());
-			db1.commitTransaction(txn);
-		} finally {
-			db1.endTransaction(txn);
-		}
 		// check that we have one session state
-		assertEquals(1, map.size());
+		assertEquals(1, c1.getClientHelper()
+				.getMessageMetadataAsDictionary(group1.getId()).size());
 
 		// introducee1 removes introducer
 		contactManager1.removeContact(contactId0From1);
 
-		// get local session state messages again
-		txn = db1.startTransaction(false);
-		try {
-			map = db1.getMessageMetadata(txn, group1.getId());
-			db1.commitTransaction(txn);
-		} finally {
-			db1.endTransaction(txn);
-		}
 		// make sure local state got deleted
-		assertEquals(0, map.size());
+		assertEquals(0, c1.getClientHelper()
+				.getMessageMetadataAsDictionary(group1.getId()).size());
 	}
 
 	@Test
@@ -567,48 +490,36 @@ public class IntroductionIntegrationTest
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener1.requestReceived);
 
-		// get database and local group for introducee
-		Group group1 = introductionGroupFactory.createLocalGroup();
+		// get local group for introducer
+		Group group0 =
+				contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
 
-		// get local session state messages
-		Map<MessageId, Metadata> map;
-		Transaction txn = db0.startTransaction(false);
-		try {
-			map = db0.getMessageMetadata(txn, group1.getId());
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
 		// check that we have one session state
-		assertEquals(1, map.size());
+		assertEquals(1, c0.getClientHelper()
+				.getMessageMetadataAsDictionary(group0.getId()).size());
 
 		// introducer removes introducee1
 		contactManager0.removeContact(contactId1From0);
 
-		// get local session state messages again
-		txn = db0.startTransaction(false);
-		try {
-			map = db0.getMessageMetadata(txn, group1.getId());
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
 		// make sure local state is still there
-		assertEquals(1, map.size());
+		assertEquals(1, c0.getClientHelper()
+				.getMessageMetadataAsDictionary(group0.getId()).size());
+
+		// ensure introducer has aborted the session
+		assertTrue(listener0.aborted);
+
+		// sync REQUEST and ABORT message
+		sync0To2(2, true);
+
+		// ensure introducee2 has aborted the session as well
+		assertTrue(listener2.aborted);
 
 		// introducer removes other introducee
 		contactManager0.removeContact(contactId2From0);
 
-		// get local session state messages again
-		txn = db0.startTransaction(false);
-		try {
-			map = db0.getMessageMetadata(txn, group1.getId());
-			db0.commitTransaction(txn);
-		} finally {
-			db0.endTransaction(txn);
-		}
 		// make sure local state is gone now
-		assertEquals(0, map.size());
+		assertEquals(0, c0.getClientHelper()
+				.getMessageMetadataAsDictionary(group0.getId()).size());
 	}
 
 	private void testModifiedResponse(StateVisitor visitor)
@@ -630,26 +541,36 @@ public class IntroductionIntegrationTest
 		eventWaiter.await(TIMEOUT, 1);
 
 		// get response to be forwarded
-		ClientHelper ch = c0.getClientHelper(); // need 0's ClientHelper here
-		Entry<MessageId, BdfDictionary> resp =
-				getMessageFor(ch, contact2From0, TYPE_RESPONSE);
-		MessageId responseId = resp.getKey();
-		BdfDictionary response = resp.getValue();
-
-		// adapt outgoing message queue to removed message
-		Group g2 = introductionGroupFactory
-				.createIntroductionGroup(contact2From0);
-		decreaseOutgoingMessageCounter(ch, g2.getId());
+		AcceptMessage message =
+				(AcceptMessage) getMessageFor(c0.getClientHelper(),
+						contact2From0, ACCEPT);
 
 		// allow visitor to modify response
-		boolean earlyAbort = visitor.visit(response);
+		AcceptMessage m = visitor.visit(message);
 
 		// replace original response with modified one
-		MessageSender sender0 = c0.getMessageSender();
 		Transaction txn = db0.startTransaction(false);
 		try {
-			db0.deleteMessage(txn, responseId);
-			sender0.sendMessage(txn, response);
+			db0.removeMessage(txn, message.getMessageId());
+			Message msg = c0.getMessageEncoder()
+					.encodeAcceptMessage(m.getGroupId(), m.getTimestamp(),
+							m.getPreviousMessageId(), m.getSessionId(),
+							m.getEphemeralPublicKey(), m.getAcceptTimestamp(),
+							m.getTransportProperties());
+			c0.getClientHelper()
+					.addLocalMessage(txn, msg, new BdfDictionary(), true);
+			Group group0 = contactGroupFactory
+					.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
+			BdfDictionary query = BdfDictionary.of(
+					new BdfEntry(SESSION_KEY_SESSION_ID, m.getSessionId())
+			);
+			Map.Entry<MessageId, BdfDictionary> session = c0.getClientHelper()
+					.getMessageMetadataAsDictionary(txn, group0.getId(), query)
+					.entrySet().iterator().next();
+			replacePreviousLocalMessageId(contact2From0.getAuthor(),
+					session.getValue(), msg.getId());
+			c0.getClientHelper().mergeMessageMetadata(txn, session.getKey(),
+					session.getValue());
 			db0.commitTransaction(txn);
 		} finally {
 			db0.endTransaction(txn);
@@ -663,21 +584,14 @@ public class IntroductionIntegrationTest
 		sync0To1(1, true);
 		sync0To2(1, true);
 
-		// sync first ACK and forward it
+		// sync first AUTH and forward it
 		sync1To0(1, true);
 		sync0To2(1, true);
 
 		// introducee2 should have detected the fake now
-		// and deleted introducee1 again
-		Collection<Contact> contacts2;
-		txn = db2.startTransaction(true);
-		try {
-			contacts2 = db2.getContacts(txn);
-			db2.commitTransaction(txn);
-		} finally {
-			db2.endTransaction(txn);
-		}
-		assertEquals(1, contacts2.size());
+		assertFalse(listener0.aborted);
+		assertFalse(listener1.aborted);
+		assertTrue(listener2.aborted);
 
 		// sync introducee2's ack and following abort
 		sync2To0(2, true);
@@ -687,144 +601,44 @@ public class IntroductionIntegrationTest
 
 		// sync abort messages to introducees
 		sync0To1(2, true);
-		sync0To2(1, true);
 
-		if (earlyAbort) {
-			assertTrue(listener1.aborted);
-			assertTrue(listener2.aborted);
-		} else {
-			assertTrue(listener2.aborted);
-			// when aborted late, introducee1 keeps the contact,
-			// so introducer can not make contacts disappear by aborting
-			Collection<Contact> contacts1;
-			txn = db1.startTransaction(true);
-			try {
-				contacts1 = db1.getContacts(txn);
-				db1.commitTransaction(txn);
-			} finally {
-				db1.endTransaction(txn);
-			}
-			assertEquals(2, contacts1.size());
-		}
+		// ensure everybody got the abort now
+		assertTrue(listener0.aborted);
+		assertTrue(listener1.aborted);
+		assertTrue(listener2.aborted);
 	}
 
 	@Test
 	public void testModifiedTransportProperties() throws Exception {
-		testModifiedResponse(response -> {
-			BdfDictionary tp = response.getDictionary(TRANSPORT, null);
-			tp.put("fakeId", BdfDictionary.of(new BdfEntry("fake", "fake")));
-			response.put(TRANSPORT, tp);
-			return false;
-		});
+		testModifiedResponse(
+				m -> new AcceptMessage(m.getMessageId(), m.getGroupId(),
+						m.getTimestamp(), m.getPreviousMessageId(),
+						m.getSessionId(), m.getEphemeralPublicKey(),
+						m.getAcceptTimestamp(),
+						getTransportPropertiesMap(2))
+		);
 	}
 
 	@Test
 	public void testModifiedTimestamp() throws Exception {
-		testModifiedResponse(response -> {
-			long timestamp = response.getLong(TIME, 0L);
-			response.put(TIME, timestamp + 1);
-			return false;
-		});
+		testModifiedResponse(
+				m -> new AcceptMessage(m.getMessageId(), m.getGroupId(),
+						m.getTimestamp(), m.getPreviousMessageId(),
+						m.getSessionId(), m.getEphemeralPublicKey(),
+						clock.currentTimeMillis(),
+						m.getTransportProperties())
+		);
 	}
 
 	@Test
 	public void testModifiedEphemeralPublicKey() throws Exception {
-		testModifiedResponse(response -> {
-			KeyPair keyPair = crypto.generateAgreementKeyPair();
-			response.put(E_PUBLIC_KEY, keyPair.getPublic().getEncoded());
-			return true;
-		});
-	}
-
-	@Test
-	public void testModifiedEphemeralPublicKeyWithFakeMac()
-			throws Exception {
-		// initialize a real introducee manager
-		MessageSender messageSender = c2.getMessageSender();
-		TransportPropertyManager tpManager = c2.getTransportPropertyManager();
-		IntroduceeManager manager2 =
-				new IntroduceeManager(messageSender, db2, clientHelper, clock,
-						crypto, tpManager, authorFactory, contactManager2,
-						identityManager2, introductionGroupFactory);
-
-		// create keys
-		KeyPair keyPair1 = crypto.generateSignatureKeyPair();
-		KeyPair eKeyPair1 = crypto.generateAgreementKeyPair();
-		KeyPair eKeyPair2 = crypto.generateAgreementKeyPair();
-
-		// Nonce 1
-		byte[][] inputs = {
-				new byte[] {CLIENT_VERSION},
-				eKeyPair1.getPublic().getEncoded(),
-				eKeyPair2.getPublic().getEncoded()
-		};
-		SecretKey sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
-				eKeyPair2.getPublic(), eKeyPair1, inputs);
-		byte[] nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret);
-
-		// Signature 1
-		byte[] sig1 = crypto.sign(SIGNING_LABEL, nonce1,
-				keyPair1.getPrivate().getEncoded());
-
-		// MAC 1
-		SecretKey macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret);
-		BdfDictionary tp1 = BdfDictionary.of(new BdfEntry("fake", "fake"));
-		long time1 = clock.currentTimeMillis();
-		BdfList toMacList = BdfList.of(keyPair1.getPublic().getEncoded(),
-				eKeyPair1.getPublic().getEncoded(), tp1, time1);
-		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] mac1 = crypto.mac(MAC_LABEL, macKey1, toMac);
-
-		// create only relevant part of state for introducee2
-		BdfDictionary state = new BdfDictionary();
-		state.put(PUBLIC_KEY, keyPair1.getPublic().getEncoded());
-		state.put(TRANSPORT, tp1);
-		state.put(TIME, time1);
-		state.put(E_PUBLIC_KEY, eKeyPair1.getPublic().getEncoded());
-		state.put(MAC, mac1);
-		state.put(MAC_KEY, macKey1.getBytes());
-		state.put(NONCE, nonce1);
-		state.put(SIGNATURE, sig1);
-
-		// MAC and signature verification should pass
-		manager2.verifyMac(state);
-		manager2.verifySignature(state);
-
-		// replace ephemeral key pair and recalculate matching keys and nonce
-		KeyPair eKeyPair1f = crypto.generateAgreementKeyPair();
-		byte[][] fakeInputs = {
-				new byte[] {CLIENT_VERSION},
-				eKeyPair1f.getPublic().getEncoded(),
-				eKeyPair2.getPublic().getEncoded()
-		};
-		sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
-				eKeyPair2.getPublic(), eKeyPair1f, fakeInputs);
-		nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret);
-
-		// recalculate MAC
-		macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret);
-		toMacList = BdfList.of(keyPair1.getPublic().getEncoded(),
-				eKeyPair1f.getPublic().getEncoded(), tp1, time1);
-		toMac = clientHelper.toByteArray(toMacList);
-		mac1 = crypto.mac(MAC_LABEL, macKey1, toMac);
-
-		// update state with faked information
-		state.put(E_PUBLIC_KEY, eKeyPair1f.getPublic().getEncoded());
-		state.put(MAC, mac1);
-		state.put(MAC_KEY, macKey1.getBytes());
-		state.put(NONCE, nonce1);
-
-		// MAC verification should still pass
-		manager2.verifyMac(state);
-
-		// Signature can not be verified, because we don't have private
-		// long-term key to fake it
-		try {
-			manager2.verifySignature(state);
-			fail();
-		} catch (GeneralSecurityException e) {
-			// expected
-		}
+		testModifiedResponse(
+				m -> new AcceptMessage(m.getMessageId(), m.getGroupId(),
+						m.getTimestamp(), m.getPreviousMessageId(),
+						m.getSessionId(),
+						getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+						m.getAcceptTimestamp(), m.getTransportProperties())
+		);
 	}
 
 	private void addTransportProperties()
@@ -832,17 +646,15 @@ public class IntroductionIntegrationTest
 		TransportPropertyManager tpm0 = c0.getTransportPropertyManager();
 		TransportPropertyManager tpm1 = c1.getTransportPropertyManager();
 		TransportPropertyManager tpm2 = c2.getTransportPropertyManager();
-		TransportProperties tp = new TransportProperties(
-				Collections.singletonMap("key", "value"));
 
-		tpm0.mergeLocalProperties(TRANSPORT_ID, tp);
+		tpm0.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
 		sync0To1(1, true);
 		sync0To2(1, true);
 
-		tpm1.mergeLocalProperties(TRANSPORT_ID, tp);
+		tpm1.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
 		sync1To0(1, true);
 
-		tpm2.mergeLocalProperties(TRANSPORT_ID, tp);
+		tpm2.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2));
 		sync2To0(1, true);
 	}
 
@@ -935,7 +747,7 @@ public class IntroductionIntegrationTest
 											time);
 						}
 					}
-				} catch (DbException | FormatException exception) {
+				} catch (DbException exception) {
 					eventWaiter.rethrow(exception);
 				} finally {
 					eventWaiter.resume();
@@ -945,7 +757,6 @@ public class IntroductionIntegrationTest
 				Contact contact = ((IntroductionSucceededEvent) e).getContact();
 				eventWaiter
 						.assertFalse(contact.getId().equals(contactId0From1));
-				eventWaiter.assertTrue(contact.isActive());
 				eventWaiter.resume();
 			} else if (e instanceof IntroductionAbortedEvent) {
 				aborted = true;
@@ -981,30 +792,41 @@ public class IntroductionIntegrationTest
 
 	}
 
-	private void decreaseOutgoingMessageCounter(ClientHelper ch, GroupId g)
-			throws FormatException, DbException {
-		BdfDictionary gD = ch.getGroupMetadataAsDictionary(g);
-		LOG.warning(gD.toString());
-		BdfDictionary queue = gD.getDictionary(QUEUE_STATE_KEY);
-		queue.put("nextOut", queue.getLong("nextOut") - 1);
-		gD.put(QUEUE_STATE_KEY, queue);
-		ch.mergeGroupMetadata(g, gD);
+	private void replacePreviousLocalMessageId(Author author,
+			BdfDictionary d, MessageId id) throws FormatException {
+		BdfDictionary i1 = d.getDictionary(SESSION_KEY_INTRODUCEE_1);
+		BdfDictionary i2 = d.getDictionary(SESSION_KEY_INTRODUCEE_2);
+		Author a1 = clientHelper
+				.parseAndValidateAuthor(i1.getList(SESSION_KEY_AUTHOR));
+		Author a2 = clientHelper
+				.parseAndValidateAuthor(i2.getList(SESSION_KEY_AUTHOR));
+
+		if (a1.equals(author)) {
+			i1.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
+			d.put(SESSION_KEY_INTRODUCEE_1, i1);
+		} else if (a2.equals(author)) {
+			i2.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
+			d.put(SESSION_KEY_INTRODUCEE_2, i2);
+		} else {
+			throw new AssertionError();
+		}
 	}
 
-	private Entry<MessageId, BdfDictionary> getMessageFor(ClientHelper ch,
-			Contact contact, long type) throws FormatException, DbException {
-		Entry<MessageId, BdfDictionary> response = null;
-		Group g = introductionGroupFactory
-				.createIntroductionGroup(contact);
+	private AbstractIntroductionMessage getMessageFor(ClientHelper ch,
+			Contact contact, MessageType type)
+			throws FormatException, DbException {
+		Group g = introductionManager0.getContactGroup(contact);
+		BdfDictionary query = BdfDictionary.of(
+				new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue())
+		);
 		Map<MessageId, BdfDictionary> map =
-				ch.getMessageMetadataAsDictionary(g.getId());
-		for (Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
-			if (entry.getValue().getLong(TYPE) == type) {
-				response = entry;
-			}
-		}
-		assertTrue(response != null);
-		return response;
+				ch.getMessageMetadataAsDictionary(g.getId(), query);
+		assertEquals(1, map.size());
+		MessageId id = map.entrySet().iterator().next().getKey();
+		Message m = ch.getMessage(id);
+		BdfList body = ch.getMessageAsList(id);
+		//noinspection ConstantConditions
+		return c0.getMessageParser().parseAcceptMessage(m, body);
 	}
 
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index bc0ea62410713a6ae9f4a3f5034c8e10c187111b..a46d37cbedd9836ac766eb9f8308c1df3d437777 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -59,6 +59,7 @@ interface IntroductionIntegrationTestComponent
 
 	void inject(IntroductionIntegrationTest init);
 
-	MessageSender getMessageSender();
+	MessageEncoder getMessageEncoder();
+	MessageParser getMessageParser();
 
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java
deleted file mode 100644
index e445910201364626e62d3d8b74b535d81ebd7d1c..0000000000000000000000000000000000000000
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfEntry;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataParser;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.sync.MessageStatus;
-import org.briarproject.briar.api.client.MessageTracker;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.test.BriarTestCase;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.jmock.lib.legacy.ClassImposteriser;
-import org.junit.Test;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-
-import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getAuthor;
-import static org.briarproject.bramble.test.TestUtils.getGroup;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
-import static org.junit.Assert.assertFalse;
-
-public class IntroductionManagerImplTest extends BriarTestCase {
-
-	private final Mockery context;
-	private final IntroductionManagerImpl introductionManager;
-	private final IntroducerManager introducerManager;
-	private final IntroduceeManager introduceeManager;
-	private final DatabaseComponent db;
-	private final ClientHelper clientHelper;
-	private final MessageTracker messageTracker;
-	private final IntroductionGroupFactory introductionGroupFactory;
-	private final SessionId sessionId = new SessionId(getRandomId());
-	private final MessageId storageId = new MessageId(sessionId.getBytes());
-	private final long time = 42L;
-	private final Contact introducee1;
-	private final Contact introducee2;
-	private final Group introductionGroup1;
-	private final Group introductionGroup2;
-	private final Message message1;
-	private Transaction txn;
-
-	public IntroductionManagerImplTest() {
-		Author author1 = getAuthor();
-		AuthorId localAuthorId1 = new AuthorId(getRandomId());
-		ContactId contactId1 = new ContactId(234);
-		introducee1 =
-				new Contact(contactId1, author1, localAuthorId1, true, true);
-
-		Author author2 = getAuthor();
-		AuthorId localAuthorId2 = new AuthorId(getRandomId());
-		ContactId contactId2 = new ContactId(235);
-		introducee2 =
-				new Contact(contactId2, author2, localAuthorId2, true, true);
-
-		introductionGroup1 = getGroup(CLIENT_ID);
-		introductionGroup2 = getGroup(CLIENT_ID);
-
-		message1 = new Message(
-				new MessageId(getRandomId()),
-				introductionGroup1.getId(),
-				time,
-				getRandomBytes(MESSAGE_HEADER_LENGTH + 1)
-		);
-
-		// mock ALL THE THINGS!!!
-		context = new Mockery();
-		context.setImposteriser(ClassImposteriser.INSTANCE);
-		introducerManager = context.mock(IntroducerManager.class);
-		introduceeManager = context.mock(IntroduceeManager.class);
-		db = context.mock(DatabaseComponent.class);
-		clientHelper = context.mock(ClientHelper.class);
-		MetadataParser metadataParser = context.mock(MetadataParser.class);
-		messageTracker = context.mock(MessageTracker.class);
-		introductionGroupFactory = context.mock(IntroductionGroupFactory.class);
-
-		introductionManager = new IntroductionManagerImpl(db, clientHelper,
-				metadataParser, messageTracker, introducerManager,
-				introduceeManager, introductionGroupFactory);
-	}
-
-	@Test
-	public void testMakeIntroduction() throws DbException, FormatException {
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(db).startTransaction(false);
-			will(returnValue(txn));
-			oneOf(introducerManager)
-					.makeIntroduction(txn, introducee1, introducee2, null,
-							time);
-			// get both introduction groups
-			oneOf(introductionGroupFactory)
-					.createIntroductionGroup(introducee1);
-			will(returnValue(introductionGroup1));
-			oneOf(introductionGroupFactory)
-					.createIntroductionGroup(introducee2);
-			will(returnValue(introductionGroup2));
-			// track message for group 1
-			oneOf(messageTracker).trackMessage(txn,
-					introductionGroup1.getId(), time, true);
-			// track message for group 2
-			oneOf(messageTracker).trackMessage(txn,
-					introductionGroup2.getId(), time, true);
-			oneOf(db).commitTransaction(txn);
-			oneOf(db).endTransaction(txn);
-		}});
-
-		introductionManager
-				.makeIntroduction(introducee1, introducee2, null, time);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testAcceptIntroduction() throws DbException, FormatException {
-		BdfDictionary state = BdfDictionary.of(
-				new BdfEntry(GROUP_ID_1, introductionGroup1.getId()),
-				new BdfEntry(GROUP_ID_2, introductionGroup2.getId())
-		);
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(db).startTransaction(false);
-			will(returnValue(txn));
-			oneOf(db).getContact(txn, introducee1.getId());
-			will(returnValue(introducee1));
-			oneOf(introductionGroupFactory).createIntroductionGroup(introducee1);
-			will(returnValue(introductionGroup1));
-			oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId);
-			will(returnValue(state));
-			oneOf(introduceeManager).acceptIntroduction(txn, state, time);
-			// track message
-			oneOf(messageTracker).trackMessage(txn,
-					introductionGroup1.getId(), time, true);
-			oneOf(db).commitTransaction(txn);
-			oneOf(db).endTransaction(txn);
-		}});
-
-		introductionManager
-				.acceptIntroduction(introducee1.getId(), sessionId, time);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testDeclineIntroduction() throws DbException, FormatException {
-		BdfDictionary state = BdfDictionary.of(
-				new BdfEntry(GROUP_ID_1, introductionGroup1.getId()),
-				new BdfEntry(GROUP_ID_2, introductionGroup2.getId())
-		);
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(db).startTransaction(false);
-			will(returnValue(txn));
-			oneOf(db).getContact(txn, introducee1.getId());
-			will(returnValue(introducee1));
-			oneOf(introductionGroupFactory).createIntroductionGroup(introducee1);
-			will(returnValue(introductionGroup1));
-			oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId);
-			will(returnValue(state));
-			oneOf(introduceeManager).declineIntroduction(txn, state, time);
-			// track message
-			oneOf(messageTracker).trackMessage(txn,
-					introductionGroup1.getId(), time, true);
-			oneOf(db).commitTransaction(txn);
-			oneOf(db).endTransaction(txn);
-		}});
-
-		introductionManager
-				.declineIntroduction(introducee1.getId(), sessionId, time);
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testGetIntroductionMessages()
-			throws DbException, FormatException {
-
-		Map<MessageId, BdfDictionary> metadata = Collections.emptyMap();
-		Collection<MessageStatus> statuses = Collections.emptyList();
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(db).startTransaction(true);
-			will(returnValue(txn));
-			oneOf(db).getContact(txn, introducee1.getId());
-			will(returnValue(introducee1));
-			oneOf(introductionGroupFactory).createIntroductionGroup(introducee1);
-			will(returnValue(introductionGroup1));
-			oneOf(clientHelper).getMessageMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadata));
-			oneOf(db).getMessageStatus(txn, introducee1.getId(),
-					introductionGroup1.getId());
-			will(returnValue(statuses));
-			oneOf(db).commitTransaction(txn);
-			oneOf(db).endTransaction(txn);
-		}});
-
-		introductionManager.getIntroductionMessages(introducee1.getId());
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testIncomingRequestMessage()
-			throws DbException, FormatException {
-
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_REQUEST);
-
-		BdfDictionary state = new BdfDictionary();
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(introduceeManager)
-					.initialize(txn, introductionGroup1.getId(), msg);
-			will(returnValue(state));
-			oneOf(introduceeManager)
-					.incomingMessage(txn, state, msg);
-			// track message
-			oneOf(messageTracker).trackIncomingMessage(txn, message1);
-		}});
-
-		introductionManager
-				.incomingMessage(txn, message1, new BdfList(), msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-	@Test
-	public void testIncomingResponseMessage()
-			throws DbException, FormatException {
-
-		BdfDictionary msg = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_RESPONSE),
-				new BdfEntry(SESSION_ID, sessionId)
-		);
-
-		BdfDictionary state = new BdfDictionary();
-		state.put(ROLE, ROLE_INTRODUCER);
-		state.put(GROUP_ID_1, introductionGroup1.getId());
-		state.put(GROUP_ID_2, introductionGroup2.getId());
-
-		txn = new Transaction(null, false);
-
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId);
-			will(returnValue(state));
-			oneOf(introducerManager).incomingMessage(txn, state, msg);
-			// track message
-			oneOf(messageTracker).trackIncomingMessage(txn, message1);
-		}});
-
-		introductionManager
-				.incomingMessage(txn, message1, new BdfList(), msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
index a2d481547c7c75e2e53c490f51ac4299bc10997f..4629a1f733c1cdd523b455a3546fea8f8d9e01b5 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java
@@ -1,361 +1,424 @@
 package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.client.BdfMessageContext;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfEntry;
 import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataEncoder;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.plugin.TransportId;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.bramble.system.SystemClock;
+import org.briarproject.bramble.test.ValidatorTestCase;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.test.BriarTestCase;
-import org.jmock.Mockery;
+import org.jmock.Expectations;
 import org.junit.Test;
 
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import javax.annotation.Nullable;
+
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
+import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
-import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getAuthor;
-import static org.briarproject.bramble.test.TestUtils.getClientId;
-import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.junit.Assert.assertArrayEquals;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
+import static org.briarproject.briar.introduction.MessageType.ABORT;
+import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
+import static org.briarproject.briar.introduction.MessageType.DECLINE;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 
-public class IntroductionValidatorTest extends BriarTestCase {
+public class IntroductionValidatorTest extends ValidatorTestCase {
+
+	private final MessageEncoder messageEncoder =
+			context.mock(MessageEncoder.class);
+	private final IntroductionValidator validator =
+			new IntroductionValidator(messageEncoder, clientHelper,
+					metadataEncoder, clock);
+
+	private final SessionId sessionId = new SessionId(getRandomId());
+	private final MessageId previousMsgId = new MessageId(getRandomId());
+	private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH);
+	private final BdfDictionary meta = new BdfDictionary();
+	private final long acceptTimestamp = 42;
+	private final BdfDictionary transportProperties = BdfDictionary.of(
+			new BdfEntry("transportId",  new BdfDictionary())
+	);
+	private final byte[] mac = getRandomBytes(MAC_BYTES);
+	private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES);
 
-	private final Mockery context = new Mockery();
-	private final Group group;
-	private final Message message;
-	private final IntroductionValidator validator;
-	private final Clock clock = new SystemClock();
+	//
+	// Introduction REQUEST
+	//
 
-	public IntroductionValidatorTest() {
-		group = getGroup(getClientId());
-		MessageId messageId = new MessageId(getRandomId());
-		long timestamp = System.currentTimeMillis();
-		byte[] raw = getRandomBytes(123);
-		message = new Message(messageId, group.getId(), timestamp, raw);
+	@Test
+	public void testAcceptsRequest() throws Exception {
+		BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(),
+				authorList, text);
 
+		expectParseAuthor(authorList, author);
+		expectEncodeRequestMetadata();
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		ClientHelper clientHelper = context.mock(ClientHelper.class);
-		MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
-		validator = new IntroductionValidator(clientHelper, metadataEncoder,
-				clock);
-		context.assertIsSatisfied();
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
-	//
-	// Introduction Requests
-	//
-
 	@Test
-	public void testValidateProperIntroductionRequest() throws Exception {
-		byte[] sessionId = getRandomId();
-		String name = getRandomString(MAX_AUTHOR_NAME_LENGTH);
-		byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
-		String text = getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
+	public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception {
+		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text);
 
-		BdfList body = BdfList.of(TYPE_REQUEST, sessionId,
-				name, publicKey, text);
+		expectParseAuthor(authorList, author);
+		expectEncodeRequestMetadata();
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		BdfDictionary result =
-				validator.validateMessage(message, group, body).getDictionary();
+		assertExpectedContext(messageContext, null);
+	}
+
+	@Test
+	public void testAcceptsRequestWithMessageNull() throws Exception {
+		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null);
+
+		expectParseAuthor(authorList, author);
+		expectEncodeRequestMetadata();
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		assertEquals(Long.valueOf(TYPE_REQUEST), result.getLong(TYPE));
-		assertEquals(sessionId, result.getRaw(SESSION_ID));
-		assertEquals(name, result.getString(NAME));
-		assertEquals(publicKey, result.getRaw(PUBLIC_KEY));
-		assertEquals(text, result.getString(MSG));
-		context.assertIsSatisfied();
+		assertExpectedContext(messageContext, null);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionRequestWithNoName() throws Exception {
-		BdfDictionary msg = getValidIntroductionRequest();
+	public void testRejectsTooShortBodyForRequest() throws Exception {
+		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList);
+		validator.validateMessage(message, group, body);
+	}
 
-		// no NAME is message
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getRaw(PUBLIC_KEY));
-		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBodyForRequest() throws Exception {
+		BdfList body =
+				BdfList.of(REQUEST.getValue(), null, authorList, text, null);
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsRawMessageForRequest() throws Exception {
+		BdfList body =
+				BdfList.of(REQUEST.getValue(), null, authorList, getRandomId());
+		expectParseAuthor(authorList, author);
 		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionRequestWithLongName() throws Exception {
-		// too long NAME in message
-		BdfDictionary msg = getValidIntroductionRequest();
-		msg.put(NAME, msg.get(NAME) + "x");
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
-		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
+	public void testRejectsStringMessageIdForRequest() throws Exception {
+		BdfList body =
+				BdfList.of(REQUEST.getValue(), "NoMessageId", authorList, null);
+		validator.validateMessage(message, group, body);
+	}
 
+	//
+	// Introduction ACCEPT
+	//
+
+	@Test
+	public void testAcceptsAccept() throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				acceptTimestamp, transportProperties);
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).parseAndValidateTransportProperties(
+					transportProperties.getDictionary("transportId"));
+		}});
+		expectEncodeMetadata(ACCEPT);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+
+		assertExpectedContext(messageContext, previousMsgId);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForAccept() throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(),
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp);
 		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionRequestWithWrongType()
-			throws Exception {
-		// wrong message type
-		BdfDictionary msg = getValidIntroductionRequest();
-		msg.put(TYPE, 324234);
+	public void testRejectsTooLongBodyForAccept() throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				acceptTimestamp, transportProperties, null);
+		validator.validateMessage(message, group, body);
+	}
 
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
-		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidSessionIdForAccept() throws Exception {
+		BdfList body =
+				BdfList.of(ACCEPT.getValue(), null, previousMsgId.getBytes(),
+						getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
+						transportProperties);
 		validator.validateMessage(message, group, body);
 	}
 
-	private BdfDictionary getValidIntroductionRequest() throws Exception {
-		byte[] sessionId = getRandomId();
-		Author author = getAuthor();
-		String text = getRandomString(MAX_MESSAGE_BODY_LENGTH);
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidPreviousMsgIdForAccept() throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), 1,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
+				transportProperties);
+		validator.validateMessage(message, group, body);
+	}
 
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_REQUEST);
-		msg.put(SESSION_ID, sessionId);
-		msg.put(NAME, author.getName());
-		msg.put(PUBLIC_KEY, author.getPublicKey());
-		msg.put(MSG, text);
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongPublicKeyForAccept() throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(),
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
+				transportProperties);
+		validator.validateMessage(message, group, body);
+	}
 
-		return msg;
+	@Test(expected = FormatException.class)
+	public void testRejectsEmptyTransportPropertiesForAccept()
+			throws Exception {
+		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(),
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
+				new BdfDictionary());
+		validator.validateMessage(message, group, body);
 	}
 
 	//
-	// Introduction Responses
+	// Introduction DECLINE
 	//
 
 	@Test
-	public void testValidateIntroductionAcceptResponse() throws Exception {
-		byte[] groupId = getRandomId();
-		byte[] sessionId = getRandomId();
-		long time = clock.currentTimeMillis();
-		byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
-		String transportId =
-				getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
-		BdfDictionary tProps = BdfDictionary.of(
-				new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH),
-						getRandomString(MAX_PROPERTY_LENGTH))
-		);
-		BdfDictionary tp = BdfDictionary.of(
-				new BdfEntry(transportId, tProps)
-		);
-
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_RESPONSE);
-		msg.put(GROUP_ID, groupId);
-		msg.put(SESSION_ID, sessionId);
-		msg.put(ACCEPT, true);
-		msg.put(TIME, time);
-		msg.put(E_PUBLIC_KEY, publicKey);
-		msg.put(TRANSPORT, tp);
-
-		BdfList body = BdfList.of(TYPE_RESPONSE, msg.getRaw(SESSION_ID),
-				msg.getBoolean(ACCEPT), msg.getLong(TIME),
-				msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
-
-		BdfDictionary result =
-				validator.validateMessage(message, group, body).getDictionary();
-
-		assertEquals(Long.valueOf(TYPE_RESPONSE), result.getLong(TYPE));
-		assertEquals(sessionId, result.getRaw(SESSION_ID));
-		assertEquals(true, result.getBoolean(ACCEPT));
-		assertEquals(publicKey, result.getRaw(E_PUBLIC_KEY));
-		assertEquals(tp, result.getDictionary(TRANSPORT));
-		context.assertIsSatisfied();
-	}
+	public void testAcceptsDecline() throws Exception {
+		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes());
 
-	@Test
-	public void testValidateIntroductionDeclineResponse() throws Exception {
-		BdfDictionary msg = getValidIntroductionResponse(false);
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getBoolean(ACCEPT));
+		expectEncodeMetadata(DECLINE);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		BdfDictionary result = validator.validateMessage(message, group, body)
-				.getDictionary();
+		assertExpectedContext(messageContext, previousMsgId);
+	}
 
-		assertFalse(result.getBoolean(ACCEPT));
-		context.assertIsSatisfied();
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForDecline() throws Exception {
+		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes());
+		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionResponseWithoutAccept()
-			throws Exception {
-		BdfDictionary msg = getValidIntroductionResponse(false);
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
+	public void testRejectsTooLongBodyForDecline() throws Exception {
+		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), null);
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidSessionIdForDecline() throws Exception {
+		BdfList body =
+				BdfList.of(DECLINE.getValue(), null, previousMsgId.getBytes());
 		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionResponseWithBrokenTp()
-			throws Exception {
-		BdfDictionary msg = getValidIntroductionResponse(true);
-		BdfDictionary tp = msg.getDictionary(TRANSPORT);
-		tp.put(
-				getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH), "X");
-		msg.put(TRANSPORT, tp);
+	public void testRejectsInvalidPreviousMsgIdForDecline() throws Exception {
+		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), 1);
+		validator.validateMessage(message, group, body);
+	}
+
+	//
+	// Introduction AUTH
+	//
+
+	@Test
+	public void testAcceptsAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac, signature);
 
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getBoolean(ACCEPT), msg.getLong(TIME),
-				msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
+		expectEncodeMetadata(AUTH);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
+		assertExpectedContext(messageContext, previousMsgId);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac);
 		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionResponseWithoutPublicKey()
-			throws Exception {
-		BdfDictionary msg = getValidIntroductionResponse(true);
+	public void testRejectsTooLongBodyForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac, signature, null);
+		validator.validateMessage(message, group, body);
+	}
 
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getBoolean(ACCEPT), msg.getLong(TIME),
-				msg.getDictionary(TRANSPORT));
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortMacForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1),
+				signature);
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongMacForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(),
+				getRandomBytes(MAC_BYTES + 1), signature);
 		validator.validateMessage(message, group, body);
 	}
 
-	private BdfDictionary getValidIntroductionResponse(boolean accept)
-			throws Exception {
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidMacForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), null, signature);
+		validator.validateMessage(message, group, body);
+	}
 
-		byte[] groupId = getRandomId();
-		byte[] sessionId = getRandomId();
-		long time = clock.currentTimeMillis();
-		byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
-		String transportId =
-				getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
-		BdfDictionary tProps = BdfDictionary.of(
-				new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH),
-						getRandomString(MAX_PROPERTY_LENGTH))
-		);
-		BdfDictionary tp = BdfDictionary.of(
-				new BdfEntry(transportId, tProps)
-		);
-
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_RESPONSE);
-		msg.put(GROUP_ID, groupId);
-		msg.put(SESSION_ID, sessionId);
-		msg.put(ACCEPT, accept);
-		if (accept) {
-			msg.put(TIME, time);
-			msg.put(E_PUBLIC_KEY, publicKey);
-			msg.put(TRANSPORT, tp);
-		}
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortSignatureForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac, getRandomBytes(0));
+		validator.validateMessage(message, group, body);
+	}
 
-		return msg;
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongSignatureForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac,
+				getRandomBytes(MAX_SIGNATURE_BYTES + 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidSignatureForAuth() throws Exception {
+		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), mac, null);
+		validator.validateMessage(message, group, body);
 	}
 
 	//
-	// Introduction ACK
+	// Introduction ACTIVATE
 	//
 
 	@Test
-	public void testValidateProperIntroductionAck() throws Exception {
-		byte[] sessionId = getRandomId();
-		byte[] mac = getRandomBytes(MAC_LENGTH);
-		byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH);
-		BdfList body = BdfList.of(TYPE_ACK, sessionId, mac, sig);
+	public void testAcceptsActivate() throws Exception {
+		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes());
 
-		BdfDictionary result =
-				validator.validateMessage(message, group, body).getDictionary();
+		expectEncodeMetadata(ACTIVATE);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		assertEquals(Long.valueOf(TYPE_ACK), result.getLong(TYPE));
-		assertArrayEquals(sessionId, result.getRaw(SESSION_ID));
-		assertArrayEquals(mac, result.getRaw(MAC));
-		assertArrayEquals(sig, result.getRaw(SIGNATURE));
-		context.assertIsSatisfied();
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateTooLongIntroductionAck() throws Exception {
-		BdfDictionary msg = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_ACK),
-				new BdfEntry(SESSION_ID, getRandomId()),
-				new BdfEntry("garbage", getRandomString(255))
-		);
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getString("garbage"));
+	public void testRejectsTooShortBodyForActivate() throws Exception {
+		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes());
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBodyForActivate() throws Exception {
+		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), null);
 		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateIntroductionAckWithLongSessionId()
-			throws Exception {
-		BdfDictionary msg = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_ACK),
-				new BdfEntry(SESSION_ID, new byte[SessionId.LENGTH + 1])
-		);
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
+	public void testRejectsInvalidSessionIdForActivate() throws Exception {
+		BdfList body =
+				BdfList.of(ACTIVATE.getValue(), null, previousMsgId.getBytes());
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidPreviousMsgIdForActivate() throws Exception {
+		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), 1);
 		validator.validateMessage(message, group, body);
 	}
 
 	//
-	// Introduction Abort
+	// Introduction ABORT
 	//
 
 	@Test
-	public void testValidateProperIntroductionAbort() throws Exception {
-		byte[] sessionId = getRandomId();
+	public void testAcceptsAbort() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes());
 
-		BdfDictionary msg = new BdfDictionary();
-		msg.put(TYPE, TYPE_ABORT);
-		msg.put(SESSION_ID, sessionId);
+		expectEncodeMetadata(ABORT);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
 
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
+		assertExpectedContext(messageContext, previousMsgId);
+	}
 
-		BdfDictionary result =
-				validator.validateMessage(message, group, body).getDictionary();
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForAbort() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes());
+		validator.validateMessage(message, group, body);
+	}
 
-		assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE));
-		assertEquals(sessionId, result.getRaw(SESSION_ID));
-		context.assertIsSatisfied();
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBodyForAbort() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
+				previousMsgId.getBytes(), null);
+		validator.validateMessage(message, group, body);
 	}
 
 	@Test(expected = FormatException.class)
-	public void testValidateTooLongIntroductionAbort() throws Exception {
-		BdfDictionary msg = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_ABORT),
-				new BdfEntry(SESSION_ID, getRandomId()),
-				new BdfEntry("garbage", getRandomString(255))
-		);
-		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
-				msg.getString("garbage"));
+	public void testRejectsInvalidSessionIdForAbort() throws Exception {
+		BdfList body =
+				BdfList.of(ABORT.getValue(), null, previousMsgId.getBytes());
+		validator.validateMessage(message, group, body);
+	}
 
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidPreviousMsgIdForAbort() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), 1);
 		validator.validateMessage(message, group, body);
 	}
 
+	//
+	// Introduction Helper Methods
+	//
+
+	private void expectEncodeRequestMetadata() {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder)
+					.encodeRequestMetadata(message.getTimestamp(), false, false,
+							false, false);
+			will(returnValue(meta));
+		}});
+	}
+
+	private void expectEncodeMetadata(MessageType type) {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder)
+					.encodeMetadata(type, sessionId, message.getTimestamp(),
+							false, false, false);
+			will(returnValue(meta));
+		}});
+	}
+
+	private void assertExpectedContext(BdfMessageContext c,
+			@Nullable MessageId dependency) {
+		assertEquals(meta, c.getDictionary());
+		if (dependency == null) {
+			assertEquals(0, c.getDependencies().size());
+		} else {
+			assertEquals(dependency, c.getDependencies().iterator().next());
+		}
+	}
+
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
similarity index 96%
rename from briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderParserIntegrationTest.java
rename to briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
index e623f814ac569809cbba77f6c2f0217787cd027c..f8c8fae3483774e8d830cd5f9ed97cd78608485b 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -28,9 +28,9 @@ import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.introduction2.MessageType.ABORT;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
+import static org.briarproject.briar.introduction.MessageType.ABORT;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 import static org.briarproject.briar.test.BriarTestUtils.getRealAuthor;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -55,8 +55,7 @@ public class MessageEncoderParserIntegrationTest extends BrambleTestCase {
 	private final SessionId sessionId = new SessionId(getRandomId());
 	private final MessageId previousMsgId = new MessageId(getRandomId());
 	private final Author author;
-	private final String text =
-			getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
+	private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH);
 	private final byte[] ephemeralPublicKey =
 			getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
 	private final byte[] mac = getRandomBytes(MAC_BYTES);
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java
similarity index 90%
rename from briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderTest.java
rename to briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java
index c7d60662d47664b2263a94236b4c87c054f4fce5..b1dae81c80b3f042f6ee3ab0e574648e65b1fd0f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/MessageEncoderTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -16,8 +16,8 @@ import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
+import static org.briarproject.briar.introduction.MessageType.REQUEST;
 
 public class MessageEncoderTest extends BrambleMockTestCase {
 
@@ -35,8 +35,7 @@ public class MessageEncoderTest extends BrambleMockTestCase {
 	private final byte[] body = getRandomBytes(42);
 	private final Author author = getAuthor();
 	private final BdfList authorList = new BdfList();
-	private final String text =
-			getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
+	private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH);
 
 	@Test
 	public void testEncodeRequestMessage() throws FormatException {
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java
deleted file mode 100644
index 1f922d70644fef119fa0b506dd25707ffa46b9d4..0000000000000000000000000000000000000000
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.briarproject.briar.introduction;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfEntry;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.data.MetadataEncoder;
-import org.briarproject.bramble.api.db.DatabaseComponent;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Metadata;
-import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.MessageQueueManager;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.test.BriarTestCase;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getClientId;
-import static org.briarproject.bramble.test.TestUtils.getGroup;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
-import static org.junit.Assert.assertFalse;
-
-public class MessageSenderTest extends BriarTestCase {
-
-	private final Mockery context;
-	private final MessageSender messageSender;
-	private final DatabaseComponent db;
-	private final ClientHelper clientHelper;
-	private final MetadataEncoder metadataEncoder;
-	private final MessageQueueManager messageQueueManager;
-	private final Clock clock;
-
-	public MessageSenderTest() {
-		context = new Mockery();
-		db = context.mock(DatabaseComponent.class);
-		clientHelper = context.mock(ClientHelper.class);
-		metadataEncoder =
-				context.mock(MetadataEncoder.class);
-		messageQueueManager =
-				context.mock(MessageQueueManager.class);
-		clock = context.mock(Clock.class);
-
-		messageSender = new MessageSender(db, clientHelper, clock,
-				metadataEncoder, messageQueueManager);
-	}
-
-	@Test
-	public void testSendMessage() throws DbException, FormatException {
-		Transaction txn = new Transaction(null, false);
-		Group privateGroup = getGroup(getClientId());
-		SessionId sessionId = new SessionId(getRandomId());
-		byte[] mac = getRandomBytes(42);
-		byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH);
-		long time = 42L;
-		BdfDictionary msg = BdfDictionary.of(
-				new BdfEntry(TYPE, TYPE_ACK),
-				new BdfEntry(GROUP_ID, privateGroup.getId()),
-				new BdfEntry(SESSION_ID, sessionId),
-				new BdfEntry(MAC, mac),
-				new BdfEntry(SIGNATURE, sig)
-		);
-		BdfList bodyList =
-				BdfList.of(TYPE_ACK, sessionId.getBytes(), mac, sig);
-		byte[] body = getRandomBytes(8);
-		Metadata metadata = new Metadata();
-
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).toByteArray(bodyList);
-			will(returnValue(body));
-			oneOf(db).getGroup(txn, privateGroup.getId());
-			will(returnValue(privateGroup));
-			oneOf(metadataEncoder).encode(msg);
-			will(returnValue(metadata));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(time));
-			oneOf(messageQueueManager)
-					.sendMessage(txn, privateGroup, time, body, metadata);
-		}});
-
-		messageSender.sendMessage(txn, msg);
-
-		context.assertIsSatisfied();
-		assertFalse(txn.isCommitted());
-	}
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
similarity index 95%
rename from briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java
rename to briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
index 24ddf208a3873ad9ac9b0c5642eb7f01c20a5d5d..00bec26a832f2e5cb3480fcb943d099fbc47369a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.introduction2;
+package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
@@ -13,7 +13,7 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.bramble.test.BrambleTestCase;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 import org.briarproject.briar.test.BriarIntegrationTestComponent;
 import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
 import org.junit.Test;
@@ -29,11 +29,11 @@ import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.test.TestUtils.getTransportId;
 import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.introduction2.IntroduceeState.LOCAL_ACCEPTED;
-import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTHS;
-import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
-import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTHS;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionValidatorTest.java
deleted file mode 100644
index 0fa094a5eaa5fed5466b8743d9d9dbe8d37ad649..0000000000000000000000000000000000000000
--- a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionValidatorTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-package org.briarproject.briar.introduction2;
-
-import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.BdfMessageContext;
-import org.briarproject.bramble.api.data.BdfDictionary;
-import org.briarproject.bramble.api.data.BdfEntry;
-import org.briarproject.bramble.api.data.BdfList;
-import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.bramble.test.ValidatorTestCase;
-import org.briarproject.briar.api.client.SessionId;
-import org.jmock.Expectations;
-import org.junit.Test;
-
-import javax.annotation.Nullable;
-
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
-import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
-import static org.briarproject.bramble.util.StringUtils.getRandomString;
-import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
-import static org.briarproject.briar.introduction2.MessageType.ABORT;
-import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
-import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
-import static org.briarproject.briar.introduction2.MessageType.AUTH;
-import static org.briarproject.briar.introduction2.MessageType.DECLINE;
-import static org.briarproject.briar.introduction2.MessageType.REQUEST;
-import static org.junit.Assert.assertEquals;
-
-public class IntroductionValidatorTest extends ValidatorTestCase {
-
-	private final MessageEncoder messageEncoder =
-			context.mock(MessageEncoder.class);
-	private final IntroductionValidator validator =
-			new IntroductionValidator(messageEncoder, clientHelper,
-					metadataEncoder, clock);
-
-	private final SessionId sessionId = new SessionId(getRandomId());
-	private final MessageId previousMsgId = new MessageId(getRandomId());
-	private final String text =
-			getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
-	private final BdfDictionary meta = new BdfDictionary();
-	private final long acceptTimestamp = 42;
-	private final BdfDictionary transportProperties = BdfDictionary.of(
-			new BdfEntry("transportId",  new BdfDictionary())
-	);
-	private final byte[] mac = getRandomBytes(MAC_BYTES);
-	private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES);
-
-	//
-	// Introduction REQUEST
-	//
-
-	@Test
-	public void testAcceptsRequest() throws Exception {
-		BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(),
-				authorList, text);
-
-		expectParseAuthor(authorList, author);
-		expectEncodeRequestMetadata();
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test
-	public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception {
-		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text);
-
-		expectParseAuthor(authorList, author);
-		expectEncodeRequestMetadata();
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, null);
-	}
-
-	@Test
-	public void testAcceptsRequestWithMessageNull() throws Exception {
-		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null);
-
-		expectParseAuthor(authorList, author);
-		expectEncodeRequestMetadata();
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, null);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForRequest() throws Exception {
-		BdfList body = BdfList.of(REQUEST.getValue(), null, authorList);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForRequest() throws Exception {
-		BdfList body =
-				BdfList.of(REQUEST.getValue(), null, authorList, text, null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsRawMessageForRequest() throws Exception {
-		BdfList body =
-				BdfList.of(REQUEST.getValue(), null, authorList, getRandomId());
-		expectParseAuthor(authorList, author);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsStringMessageIdForRequest() throws Exception {
-		BdfList body =
-				BdfList.of(REQUEST.getValue(), "NoMessageId", authorList, null);
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction ACCEPT
-	//
-
-	@Test
-	public void testAcceptsAccept() throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
-				acceptTimestamp, transportProperties);
-		context.checking(new Expectations() {{
-			oneOf(clientHelper).parseAndValidateTransportProperties(
-					transportProperties.getDictionary("transportId"));
-		}});
-		expectEncodeMetadata(ACCEPT);
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForAccept() throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(),
-				getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForAccept() throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
-				acceptTimestamp, transportProperties, null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidSessionIdForAccept() throws Exception {
-		BdfList body =
-				BdfList.of(ACCEPT.getValue(), null, previousMsgId.getBytes(),
-						getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
-						transportProperties);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidPreviousMsgIdForAccept() throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				null, getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
-				transportProperties);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongPublicKeyForAccept() throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(),
-				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
-				transportProperties);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsEmptyTransportPropertiesForAccept()
-			throws Exception {
-		BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(),
-				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
-				new BdfDictionary());
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction DECLINE
-	//
-
-	@Test
-	public void testAcceptsDecline() throws Exception {
-		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes());
-
-		expectEncodeMetadata(DECLINE);
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForDecline() throws Exception {
-		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForDecline() throws Exception {
-		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidSessionIdForDecline() throws Exception {
-		BdfList body =
-				BdfList.of(DECLINE.getValue(), null, previousMsgId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidPreviousMsgIdForDecline() throws Exception {
-		BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
-				null);
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction AUTH
-	//
-
-	@Test
-	public void testAcceptsAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac, signature);
-
-		expectEncodeMetadata(AUTH);
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac, signature, null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortMacForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1),
-				signature);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongMacForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(),
-				getRandomBytes(MAC_BYTES + 1), signature);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidMacForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), null, signature);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortSignatureForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac, getRandomBytes(0));
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongSignatureForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac,
-				getRandomBytes(MAX_SIGNATURE_BYTES + 1));
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidSignatureForAuth() throws Exception {
-		BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), mac, null);
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction ACTIVATE
-	//
-
-	@Test
-	public void testAcceptsActivate() throws Exception {
-		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes());
-
-		expectEncodeMetadata(ACTIVATE);
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForActivate() throws Exception {
-		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForActivate() throws Exception {
-		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidSessionIdForActivate() throws Exception {
-		BdfList body =
-				BdfList.of(ACTIVATE.getValue(), null, previousMsgId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidPreviousMsgIdForActivate() throws Exception {
-		BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
-				null);
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction ABORT
-	//
-
-	@Test
-	public void testAcceptsAbort() throws Exception {
-		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes());
-
-		expectEncodeMetadata(ABORT);
-		BdfMessageContext messageContext =
-				validator.validateMessage(message, group, body);
-
-		assertExpectedContext(messageContext, previousMsgId);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooShortBodyForAbort() throws Exception {
-		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsTooLongBodyForAbort() throws Exception {
-		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
-				previousMsgId.getBytes(), null);
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidSessionIdForAbort() throws Exception {
-		BdfList body =
-				BdfList.of(ABORT.getValue(), null, previousMsgId.getBytes());
-		validator.validateMessage(message, group, body);
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsInvalidPreviousMsgIdForAbort() throws Exception {
-		BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
-				null);
-		validator.validateMessage(message, group, body);
-	}
-
-	//
-	// Introduction Helper Methods
-	//
-
-	private void expectEncodeRequestMetadata() {
-		context.checking(new Expectations() {{
-			oneOf(messageEncoder)
-					.encodeRequestMetadata(message.getTimestamp(), false, false,
-							false, false);
-			will(returnValue(meta));
-		}});
-	}
-
-	private void expectEncodeMetadata(MessageType type) {
-		context.checking(new Expectations() {{
-			oneOf(messageEncoder)
-					.encodeMetadata(type, sessionId, message.getTimestamp(),
-							false, false, false);
-			will(returnValue(meta));
-		}});
-	}
-
-	private void assertExpectedContext(BdfMessageContext c,
-			@Nullable MessageId dependency) {
-		assertEquals(meta, c.getDictionary());
-		if (dependency == null) {
-			assertEquals(0, c.getDependencies().size());
-		} else {
-			assertEquals(dependency, c.getDependencies().iterator().next());
-		}
-	}
-
-}
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index fe2e745c0d7d92d0f7c3ee7bcbb71c6ce7b567e8..a76abd0570c8878dc87859ad913debed207199a7 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -37,9 +37,9 @@ import org.briarproject.briar.blog.BlogModule;
 import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
 import org.briarproject.briar.introduction.IntroductionModule;
-import org.briarproject.briar.introduction2.IntroductionCryptoImplTest;
-import org.briarproject.briar.introduction2.MessageEncoderParserIntegrationTest;
-import org.briarproject.briar.introduction2.SessionEncoderParserIntegrationTest;
+import org.briarproject.briar.introduction.IntroductionCryptoImplTest;
+import org.briarproject.briar.introduction.MessageEncoderParserIntegrationTest;
+import org.briarproject.briar.introduction.SessionEncoderParserIntegrationTest;
 import org.briarproject.briar.messaging.MessagingModule;
 import org.briarproject.briar.privategroup.PrivateGroupModule;
 import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;