diff --git a/bramble-core/build.gradle b/bramble-core/build.gradle
index 59361dc93eedde403d9044802caedaf426094e55..9307f5d583a86d0b921954bb40bfd512d5edc285 100644
--- a/bramble-core/build.gradle
+++ b/bramble-core/build.gradle
@@ -9,13 +9,14 @@ apply plugin: 'witness'
 dependencies {
 	implementation project(path: ':bramble-api', configuration: 'default')
 	implementation 'com.madgag.spongycastle:core:1.58.0.0'
-	implementation 'com.h2database:h2:1.4.192' // This is the last version that supports Java 1.6
+	implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
 	implementation 'org.bitlet:weupnp:0.1.4'
 	implementation 'net.i2p.crypto:eddsa:0.2.0'
 
 	apt 'com.google.dagger:dagger-compiler:2.0.2'
 
 	testImplementation project(path: ':bramble-api', configuration: 'testOutput')
+	testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6
 	testImplementation 'junit:junit:4.12'
 	testImplementation "org.jmock:jmock:2.8.2"
 	testImplementation "org.jmock:jmock-junit4:2.8.2"
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
index 32596eede3aa70fae92e160b0d4729d5b3029700..307f146e19dc6a819c4e53a760d1933c9e5df60a 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
@@ -22,16 +22,18 @@ import javax.inject.Inject;
 class H2Database extends JdbcDatabase {
 
 	private static final String HASH_TYPE = "BINARY(32)";
+	private static final String SECRET_TYPE = "BINARY(32)";
 	private static final String BINARY_TYPE = "BINARY";
 	private static final String COUNTER_TYPE = "INT NOT NULL AUTO_INCREMENT";
-	private static final String SECRET_TYPE = "BINARY(32)";
+	private static final String STRING_TYPE = "VARCHAR";
 
 	private final DatabaseConfig config;
 	private final String url;
 
 	@Inject
 	H2Database(DatabaseConfig config, Clock clock) {
-		super(HASH_TYPE, BINARY_TYPE, COUNTER_TYPE, SECRET_TYPE, clock);
+		super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE,
+				clock);
 		this.config = config;
 		File dir = config.getDatabaseDirectory();
 		String path = new File(dir, "db").getAbsolutePath();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
new file mode 100644
index 0000000000000000000000000000000000000000..1db9248cb28b6d67f7de3818b822480296dabf5e
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
@@ -0,0 +1,99 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DatabaseConfig;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.util.StringUtils;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.inject.Inject;
+
+/**
+ * Contains all the HSQLDB-specific code for the database.
+ */
+@NotNullByDefault
+class HyperSqlDatabase extends JdbcDatabase {
+
+	private static final String HASH_TYPE = "BINARY(32)";
+	private static final String SECRET_TYPE = "BINARY(32)";
+	private static final String BINARY_TYPE = "BINARY";
+	private static final String COUNTER_TYPE =
+			"INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(START WITH 1)";
+	private static final String STRING_TYPE = "VARCHAR";
+
+	private final DatabaseConfig config;
+	private final String url;
+
+	@Inject
+	HyperSqlDatabase(DatabaseConfig config, Clock clock) {
+		super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE,
+				clock);
+		this.config = config;
+		File dir = config.getDatabaseDirectory();
+		String path = new File(dir, "db").getAbsolutePath();
+		url = "jdbc:hsqldb:file:" + path
+				+ ";sql.enforce_size=false;allow_empty_batch=true"
+				+ ";encrypt_lobs=true;crypt_type=AES";
+	}
+
+	@Override
+	public boolean open() throws DbException {
+		boolean reopen = config.databaseExists();
+		if (!reopen) config.getDatabaseDirectory().mkdirs();
+		super.open("org.hsqldb.jdbc.JDBCDriver", reopen);
+		return reopen;
+	}
+
+	@Override
+	public void close() throws DbException {
+		try {
+			super.closeAllConnections();
+			Connection c = createConnection();
+			Statement s = c.createStatement();
+			s.executeQuery("SHUTDOWN");
+			s.close();
+			c.close();
+		} catch (SQLException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public long getFreeSpace() throws DbException {
+		File dir = config.getDatabaseDirectory();
+		long maxSize = config.getMaxSize();
+		long free = dir.getFreeSpace();
+		long used = getDiskSpace(dir);
+		long quota = maxSize - used;
+		return Math.min(free, quota);
+	}
+
+	private long getDiskSpace(File f) {
+		if (f.isDirectory()) {
+			long total = 0;
+			File[] children = f.listFiles();
+			if (children != null)
+				for (File child : children) total += getDiskSpace(child);
+			return total;
+		} else if (f.isFile()) {
+			return f.length();
+		} else {
+			return 0;
+		}
+	}
+
+	@Override
+	protected Connection createConnection() throws SQLException {
+		SecretKey key = config.getEncryptionKey();
+		if (key == null) throw new IllegalStateException();
+		String hex = StringUtils.toHexString(key.getBytes());
+		return DriverManager.getConnection(url + ";crypt_key=" + hex);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index 8b8c3562af85c0b92dce12b71056bc93198f0c82..bf863ade1d8692bf3794489f89f628bf2fa6f7f2 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -73,27 +73,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
-					+ " (namespace VARCHAR NOT NULL,"
-					+ " key VARCHAR NOT NULL,"
-					+ " value VARCHAR NOT NULL,"
-					+ " PRIMARY KEY (namespace, key))";
+					+ " (namespace _STRING NOT NULL,"
+					+ " \"key\" _STRING NOT NULL,"
+					+ " value _STRING NOT NULL,"
+					+ " PRIMARY KEY (namespace, \"key\"))";
 
 	private static final String CREATE_LOCAL_AUTHORS =
 			"CREATE TABLE localAuthors"
-					+ " (authorId HASH NOT NULL,"
-					+ " name VARCHAR NOT NULL,"
-					+ " publicKey BINARY NOT NULL,"
-					+ " privateKey BINARY NOT NULL,"
+					+ " (authorId _HASH NOT NULL,"
+					+ " name _STRING NOT NULL,"
+					+ " publicKey _BINARY NOT NULL,"
+					+ " privateKey _BINARY NOT NULL,"
 					+ " created BIGINT NOT NULL,"
 					+ " PRIMARY KEY (authorId))";
 
 	private static final String CREATE_CONTACTS =
 			"CREATE TABLE contacts"
-					+ " (contactId COUNTER,"
-					+ " authorId HASH NOT NULL,"
-					+ " name VARCHAR NOT NULL,"
-					+ " publicKey BINARY NOT NULL,"
-					+ " localAuthorId HASH NOT NULL,"
+					+ " (contactId _COUNTER,"
+					+ " authorId _HASH NOT NULL,"
+					+ " name _STRING NOT NULL,"
+					+ " publicKey _BINARY NOT NULL,"
+					+ " localAuthorId _HASH NOT NULL,"
 					+ " verified BOOLEAN NOT NULL,"
 					+ " active BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (contactId),"
@@ -103,17 +103,17 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_GROUPS =
 			"CREATE TABLE groups"
-					+ " (groupId HASH NOT NULL,"
-					+ " clientId VARCHAR NOT NULL,"
-					+ " descriptor BINARY NOT NULL,"
+					+ " (groupId _HASH NOT NULL,"
+					+ " clientId _STRING NOT NULL,"
+					+ " descriptor _BINARY NOT NULL,"
 					+ " PRIMARY KEY (groupId))";
 
 	private static final String CREATE_GROUP_METADATA =
 			"CREATE TABLE groupMetadata"
-					+ " (groupId HASH NOT NULL,"
-					+ " key VARCHAR NOT NULL,"
-					+ " value BINARY NOT NULL,"
-					+ " PRIMARY KEY (groupId, key),"
+					+ " (groupId _HASH NOT NULL,"
+					+ " \"key\" _STRING NOT NULL,"
+					+ " value _BINARY NOT NULL,"
+					+ " PRIMARY KEY (groupId, \"key\"),"
 					+ " FOREIGN KEY (groupId)"
 					+ " REFERENCES groups (groupId)"
 					+ " ON DELETE CASCADE)";
@@ -121,7 +121,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String CREATE_GROUP_VISIBILITIES =
 			"CREATE TABLE groupVisibilities"
 					+ " (contactId INT NOT NULL,"
-					+ " groupId HASH NOT NULL,"
+					+ " groupId _HASH NOT NULL,"
 					+ " shared BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (contactId, groupId),"
 					+ " FOREIGN KEY (contactId)"
@@ -133,8 +133,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_MESSAGES =
 			"CREATE TABLE messages"
-					+ " (messageId HASH NOT NULL,"
-					+ " groupId HASH NOT NULL,"
+					+ " (messageId _HASH NOT NULL,"
+					+ " groupId _HASH NOT NULL,"
 					+ " timestamp BIGINT NOT NULL,"
 					+ " state INT NOT NULL,"
 					+ " shared BOOLEAN NOT NULL,"
@@ -147,19 +147,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_MESSAGE_METADATA =
 			"CREATE TABLE messageMetadata"
-					+ " (messageId HASH NOT NULL,"
-					+ " key VARCHAR NOT NULL,"
-					+ " value BINARY NOT NULL,"
-					+ " PRIMARY KEY (messageId, key),"
+					+ " (messageId _HASH NOT NULL,"
+					+ " \"key\" _STRING NOT NULL,"
+					+ " value _BINARY NOT NULL,"
+					+ " PRIMARY KEY (messageId, \"key\"),"
 					+ " FOREIGN KEY (messageId)"
 					+ " REFERENCES messages (messageId)"
 					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_MESSAGE_DEPENDENCIES =
 			"CREATE TABLE messageDependencies"
-					+ " (groupId HASH NOT NULL,"
-					+ " messageId HASH NOT NULL,"
-					+ " dependencyId HASH NOT NULL," // Not a foreign key
+					+ " (groupId _HASH NOT NULL,"
+					+ " messageId _HASH NOT NULL,"
+					+ " dependencyId _HASH NOT NULL," // Not a foreign key
 					+ " FOREIGN KEY (groupId)"
 					+ " REFERENCES groups (groupId)"
 					+ " ON DELETE CASCADE,"
@@ -169,7 +169,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_OFFERS =
 			"CREATE TABLE offers"
-					+ " (messageId HASH NOT NULL," // Not a foreign key
+					+ " (messageId _HASH NOT NULL," // Not a foreign key
 					+ " contactId INT NOT NULL,"
 					+ " PRIMARY KEY (messageId, contactId),"
 					+ " FOREIGN KEY (contactId)"
@@ -178,7 +178,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_STATUSES =
 			"CREATE TABLE statuses"
-					+ " (messageId HASH NOT NULL,"
+					+ " (messageId _HASH NOT NULL,"
 					+ " contactId INT NOT NULL,"
 					+ " ack BOOLEAN NOT NULL,"
 					+ " seen BOOLEAN NOT NULL,"
@@ -195,20 +195,20 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_TRANSPORTS =
 			"CREATE TABLE transports"
-					+ " (transportId VARCHAR NOT NULL,"
+					+ " (transportId _STRING NOT NULL,"
 					+ " maxLatency INT NOT NULL,"
 					+ " PRIMARY KEY (transportId))";
 
 	private static final String CREATE_INCOMING_KEYS =
 			"CREATE TABLE incomingKeys"
 					+ " (contactId INT NOT NULL,"
-					+ " transportId VARCHAR NOT NULL,"
-					+ " period BIGINT NOT NULL,"
-					+ " tagKey SECRET NOT NULL,"
-					+ " headerKey SECRET NOT NULL,"
+					+ " transportId _STRING NOT NULL,"
+					+ " \"period\" BIGINT NOT NULL,"
+					+ " tagKey _SECRET NOT NULL,"
+					+ " headerKey _SECRET NOT NULL,"
 					+ " base BIGINT NOT NULL,"
-					+ " bitmap BINARY NOT NULL,"
-					+ " PRIMARY KEY (contactId, transportId, period),"
+					+ " bitmap _BINARY NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId, \"period\"),"
 					+ " FOREIGN KEY (contactId)"
 					+ " REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE,"
@@ -219,10 +219,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String CREATE_OUTGOING_KEYS =
 			"CREATE TABLE outgoingKeys"
 					+ " (contactId INT NOT NULL,"
-					+ " transportId VARCHAR NOT NULL,"
-					+ " period BIGINT NOT NULL,"
-					+ " tagKey SECRET NOT NULL,"
-					+ " headerKey SECRET NOT NULL,"
+					+ " transportId _STRING NOT NULL,"
+					+ " \"period\" BIGINT NOT NULL,"
+					+ " tagKey _SECRET NOT NULL,"
+					+ " headerKey _SECRET NOT NULL,"
 					+ " stream BIGINT NOT NULL,"
 					+ " PRIMARY KEY (contactId, transportId),"
 					+ " FOREIGN KEY (contactId)"
@@ -260,7 +260,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			Logger.getLogger(JdbcDatabase.class.getName());
 
 	// Different database libraries use different names for certain types
-	private final String hashType, binaryType, counterType, secretType;
+	private final String hashType, secretType, binaryType;
+	private final String counterType, stringType;
 	private final Clock clock;
 
 	// Locking: connectionsLock
@@ -275,12 +276,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private final Lock connectionsLock = new ReentrantLock();
 	private final Condition connectionsChanged = connectionsLock.newCondition();
 
-	JdbcDatabase(String hashType, String binaryType, String counterType,
-			String secretType, Clock clock) {
+	JdbcDatabase(String hashType, String secretType, String binaryType,
+			String counterType, String stringType, Clock clock) {
 		this.hashType = hashType;
+		this.secretType = secretType;
 		this.binaryType = binaryType;
 		this.counterType = counterType;
-		this.secretType = secretType;
+		this.stringType = stringType;
 		this.clock = clock;
 	}
 
@@ -383,10 +385,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	private String insertTypeNames(String s) {
-		s = s.replaceAll("HASH", hashType);
-		s = s.replaceAll("BINARY", binaryType);
-		s = s.replaceAll("COUNTER", counterType);
-		s = s.replaceAll("SECRET", secretType);
+		s = s.replaceAll("_HASH", hashType);
+		s = s.replaceAll("_SECRET", secretType);
+		s = s.replaceAll("_BINARY", binaryType);
+		s = s.replaceAll("_COUNTER", counterType);
+		s = s.replaceAll("_STRING", stringType);
 		return s;
 	}
 
@@ -500,7 +503,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Create a contact row
 			String sql = "INSERT INTO contacts"
-					+ " (authorId, name, publicKey, localAuthorId, verified, active)"
+					+ " (authorId, name, publicKey, localAuthorId,"
+					+ " verified, active)"
 					+ " VALUES (?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, remote.getId().getBytes());
@@ -719,7 +723,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Store the incoming keys
 			String sql = "INSERT INTO incomingKeys (contactId, transportId,"
-					+ " period, tagKey, headerKey, base, bitmap)"
+					+ " \"period\", tagKey, headerKey, base, bitmap)"
 					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
@@ -754,8 +758,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 				if (rows != 1) throw new DbStateException();
 			ps.close();
 			// Store the outgoing keys
-			sql = "INSERT INTO outgoingKeys (contactId, transportId, period,"
-					+ " tagKey, headerKey, stream)"
+			sql = "INSERT INTO outgoingKeys (contactId, transportId,"
+					+ " \"period\", tagKey, headerKey, stream)"
 					+ " VALUES (?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
@@ -1335,7 +1339,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " JOIN messageMetadata AS md"
 					+ " ON m.messageId = md.messageId"
 					+ " WHERE state = ? AND groupId = ?"
-					+ " AND key = ? AND value = ?";
+					+ " AND \"key\" = ? AND value = ?";
 			for (Entry<String, byte[]> e : query.entrySet()) {
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, DELIVERED.getValue());
@@ -1367,7 +1371,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT m.messageId, key, value"
+			String sql = "SELECT m.messageId, \"key\", value"
 					+ " FROM messages AS m"
 					+ " JOIN messageMetadata AS md"
 					+ " ON m.messageId = md.messageId"
@@ -1417,7 +1421,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM groupMetadata"
+			String sql = "SELECT \"key\", value FROM groupMetadata"
 					+ " WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
@@ -1440,7 +1444,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM messageMetadata AS md"
+			String sql = "SELECT \"key\", value FROM messageMetadata AS md"
 					+ " JOIN messages AS m"
 					+ " ON m.messageId = md.messageId"
 					+ " WHERE m.state = ? AND md.messageId = ?";
@@ -1466,7 +1470,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM messageMetadata AS md"
+			String sql = "SELECT \"key\", value FROM messageMetadata AS md"
 					+ " JOIN messages AS m"
 					+ " ON m.messageId = md.messageId"
 					+ " WHERE (m.state = ? OR m.state = ?)"
@@ -1904,7 +1908,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM settings WHERE namespace = ?";
+			String sql = "SELECT \"key\", value FROM settings"
+					+ " WHERE namespace = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setString(1, namespace);
 			rs = ps.executeQuery();
@@ -1927,10 +1932,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			// Retrieve the incoming keys
-			String sql = "SELECT period, tagKey, headerKey, base, bitmap"
+			String sql = "SELECT \"period\", tagKey, headerKey, base, bitmap"
 					+ " FROM incomingKeys"
 					+ " WHERE transportId = ?"
-					+ " ORDER BY contactId, period";
+					+ " ORDER BY contactId, \"period\"";
 			ps = txn.prepareStatement(sql);
 			ps.setString(1, t.getString());
 			rs = ps.executeQuery();
@@ -1947,10 +1952,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs.close();
 			ps.close();
 			// Retrieve the outgoing keys in the same order
-			sql = "SELECT contactId, period, tagKey, headerKey, stream"
+			sql = "SELECT contactId, \"period\", tagKey, headerKey, stream"
 					+ " FROM outgoingKeys"
 					+ " WHERE transportId = ?"
-					+ " ORDER BY contactId, period";
+					+ " ORDER BY contactId, \"period\"";
 			ps = txn.prepareStatement(sql);
 			ps.setString(1, t.getString());
 			rs = ps.executeQuery();
@@ -1987,7 +1992,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE outgoingKeys SET stream = stream + 1"
-					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
+					+ " WHERE contactId = ? AND transportId = ?"
+					+ " AND \"period\" = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setString(2, t.getString());
@@ -2081,7 +2087,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			// Delete any keys that are being removed
 			if (!removed.isEmpty()) {
 				String sql = "DELETE FROM " + tableName
-						+ " WHERE " + columnName + " = ? AND key = ?";
+						+ " WHERE " + columnName + " = ? AND \"key\" = ?";
 				ps = txn.prepareStatement(sql);
 				ps.setBytes(1, id);
 				for (String key : removed) {
@@ -2100,7 +2106,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (retained.isEmpty()) return;
 			// Update any keys that already exist
 			String sql = "UPDATE " + tableName + " SET value = ?"
-					+ " WHERE " + columnName + " = ? AND key = ?";
+					+ " WHERE " + columnName + " = ? AND \"key\" = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(2, id);
 			for (Entry<String, byte[]> e : retained.entrySet()) {
@@ -2117,7 +2123,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			// Insert any keys that don't already exist
 			sql = "INSERT INTO " + tableName
-					+ " (" + columnName + ", key, value)"
+					+ " (" + columnName + ", \"key\", value)"
 					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, id);
@@ -2149,7 +2155,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Update any settings that already exist
 			String sql = "UPDATE settings SET value = ?"
-					+ " WHERE namespace = ? AND key = ?";
+					+ " WHERE namespace = ? AND \"key\" = ?";
 			ps = txn.prepareStatement(sql);
 			for (Entry<String, String> e : s.entrySet()) {
 				ps.setString(1, e.getValue());
@@ -2164,7 +2170,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				if (rows > 1) throw new DbStateException();
 			}
 			// Insert any settings that don't already exist
-			sql = "INSERT INTO settings (namespace, key, value)"
+			sql = "INSERT INTO settings (namespace, \"key\", value)"
 					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			int updateIndex = 0, inserted = 0;
@@ -2528,7 +2534,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?"
-					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
+					+ " WHERE contactId = ? AND transportId = ?"
+					+ " AND \"period\" = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, base);
 			ps.setBytes(2, bitmap);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..dbdeca4dcc943745504bfece15c85313c616c385
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java
@@ -0,0 +1,380 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.util.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.sql.Types.BINARY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class BasicDatabaseTest extends BrambleTestCase {
+
+	private static final int BATCH_SIZE = 100;
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final File db = new File(testDir, "db");
+
+	protected abstract String getBinaryType();
+
+	protected abstract String getDriverName();
+
+	protected abstract Connection openConnection(File db, boolean encrypt)
+			throws SQLException;
+
+	protected abstract void shutdownDatabase(File db, boolean encrypt)
+			throws SQLException;
+
+	@Before
+	public void setUp() throws Exception {
+		testDir.mkdirs();
+		Class.forName(getDriverName());
+	}
+
+	@Test
+	public void testInsertUpdateAndDelete() throws Exception {
+		Connection connection = openConnection(db, false);
+		try {
+			// Create the table
+			createTable(connection);
+			// Generate an ID and two names
+			byte[] id = TestUtils.getRandomId();
+			String oldName = StringUtils.getRandomString(50);
+			String newName = StringUtils.getRandomString(50);
+			// Insert the ID and old name into the table
+			insertRow(connection, id, oldName);
+			// Check that the old name can be retrieved using the ID
+			assertTrue(rowExists(connection, id));
+			assertEquals(oldName, getName(connection, id));
+			// Update the name
+			updateRow(connection, id, newName);
+			// Check that the new name can be retrieved using the ID
+			assertTrue(rowExists(connection, id));
+			assertEquals(newName, getName(connection, id));
+			// Delete the row from the table
+			assertTrue(deleteRow(connection, id));
+			// Check that the row no longer exists
+			assertFalse(rowExists(connection, id));
+			// Deleting the row again should have no effect
+			assertFalse(deleteRow(connection, id));
+		} finally {
+			connection.close();
+			shutdownDatabase(db, false);
+		}
+	}
+
+	@Test
+	public void testBatchInsertUpdateAndDelete() throws Exception {
+		Connection connection = openConnection(db, false);
+		try {
+			// Create the table
+			createTable(connection);
+			// Generate some IDs and two sets of names
+			byte[][] ids = new byte[BATCH_SIZE][];
+			String[] oldNames = new String[BATCH_SIZE];
+			String[] newNames = new String[BATCH_SIZE];
+			for (int i = 0; i < BATCH_SIZE; i++) {
+				ids[i] = TestUtils.getRandomId();
+				oldNames[i] = StringUtils.getRandomString(50);
+				newNames[i] = StringUtils.getRandomString(50);
+			}
+			// Insert the IDs and old names into the table as a batch
+			insertBatch(connection, ids, oldNames);
+			// Update the names as a batch
+			updateBatch(connection, ids, newNames);
+			// Check that the new names can be retrieved using the IDs
+			for (int i = 0; i < BATCH_SIZE; i++) {
+				assertTrue(rowExists(connection, ids[i]));
+				assertEquals(newNames[i], getName(connection, ids[i]));
+			}
+			// Delete the rows as a batch
+			boolean[] deleted = deleteBatch(connection, ids);
+			// Check that the rows no longer exist
+			for (int i = 0; i < BATCH_SIZE; i++) {
+				assertTrue(deleted[i]);
+				assertFalse(rowExists(connection, ids[i]));
+			}
+			// Deleting the rows again should have no effect
+			deleted = deleteBatch(connection, ids);
+			for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]);
+		} finally {
+			connection.close();
+			shutdownDatabase(db, false);
+		}
+	}
+
+	@Test
+	public void testSortOrder() throws Exception {
+		byte[] first = new byte[] {
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0
+		};
+		byte[] second = new byte[] {
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 127
+		};
+		byte[] third = new byte[] {
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0, 0, 0, (byte) 255
+		};
+		Connection connection = openConnection(db, false);
+		try {
+			// Create the table
+			createTable(connection);
+			// Insert the rows
+			insertRow(connection, first, "first");
+			insertRow(connection, second, "second");
+			insertRow(connection, third, "third");
+			insertRow(connection, null, "null");
+			// Check the ordering of the < operator: null is not comparable
+			assertNull(getPredecessor(connection, first));
+			assertEquals("first", getPredecessor(connection, second));
+			assertEquals("second", getPredecessor(connection, third));
+			assertNull(getPredecessor(connection, null));
+			// Check the ordering of ORDER BY: nulls come first
+			List<String> names = getNames(connection);
+			assertEquals(4, names.size());
+			assertEquals("null", names.get(0));
+			assertEquals("first", names.get(1));
+			assertEquals("second", names.get(2));
+			assertEquals("third", names.get(3));
+		} finally {
+			connection.close();
+			shutdownDatabase(db, false);
+		}
+	}
+
+	@Test
+	public void testDataIsFoundWithoutEncryption() throws Exception {
+		testEncryption(false);
+	}
+
+	@Test
+	public void testDataIsNotFoundWithEncryption() throws Exception {
+		testEncryption(true);
+	}
+
+	private void testEncryption(boolean encrypt) throws Exception {
+		byte[] sequence = new byte[] {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
+		Connection connection = openConnection(db, encrypt);
+		try {
+			createTable(connection);
+			insertRow(connection, sequence, "abcdefg");
+		} finally {
+			connection.close();
+			shutdownDatabase(db, encrypt);
+		}
+		try {
+			if (findSequence(testDir, sequence) == encrypt) fail();
+		} finally {
+			shutdownDatabase(db, encrypt);
+		}
+	}
+
+	private void createTable(Connection connection) throws SQLException {
+		Statement s = connection.createStatement();
+		String sql = "CREATE TABLE foo (uniqueId " + getBinaryType() + ","
+				+ " name VARCHAR(100) NOT NULL)";
+		s.executeUpdate(sql);
+		s.close();
+	}
+
+	private void insertRow(Connection connection, byte[] id, String name)
+			throws SQLException {
+		String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		if (id == null) ps.setNull(1, BINARY);
+		else ps.setBytes(1, id);
+		ps.setString(2, name);
+		int affected = ps.executeUpdate();
+		assertEquals(1, affected);
+		ps.close();
+	}
+
+	private boolean rowExists(Connection connection, byte[] id)
+			throws SQLException {
+		assertNotNull(id);
+		String sql = "SELECT * FROM foo WHERE uniqueID = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		ps.setBytes(1, id);
+		ResultSet rs = ps.executeQuery();
+		boolean found = rs.next();
+		assertFalse(rs.next());
+		rs.close();
+		ps.close();
+		return found;
+	}
+
+	private String getName(Connection connection, byte[] id)
+			throws SQLException {
+		assertNotNull(id);
+		String sql = "SELECT name FROM foo WHERE uniqueID = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		ps.setBytes(1, id);
+		ResultSet rs = ps.executeQuery();
+		assertTrue(rs.next());
+		String name = rs.getString(1);
+		assertFalse(rs.next());
+		rs.close();
+		ps.close();
+		return name;
+	}
+
+	private void updateRow(Connection connection, byte[] id, String name)
+			throws SQLException {
+		String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		if (id == null) ps.setNull(2, BINARY);
+		else ps.setBytes(2, id);
+		ps.setString(1, name);
+		assertEquals(1, ps.executeUpdate());
+		ps.close();
+	}
+
+	private boolean deleteRow(Connection connection, byte[] id)
+			throws SQLException {
+		String sql = "DELETE FROM foo WHERE uniqueId = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		if (id == null) ps.setNull(1, BINARY);
+		else ps.setBytes(1, id);
+		int affected = ps.executeUpdate();
+		ps.close();
+		return affected == 1;
+	}
+
+	private void insertBatch(Connection connection, byte[][] ids,
+			String[] names) throws SQLException {
+		assertEquals(ids.length, names.length);
+		String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		for (int i = 0; i < ids.length; i++) {
+			if (ids[i] == null) ps.setNull(1, BINARY);
+			else ps.setBytes(1, ids[i]);
+			ps.setString(2, names[i]);
+			ps.addBatch();
+		}
+		int[] batchAffected = ps.executeBatch();
+		assertEquals(ids.length, batchAffected.length);
+		for (int affected : batchAffected) assertEquals(1, affected);
+		ps.close();
+	}
+
+	private void updateBatch(Connection connection, byte[][] ids,
+			String[] names) throws SQLException {
+		assertEquals(ids.length, names.length);
+		String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		for (int i = 0; i < ids.length; i++) {
+			if (ids[i] == null) ps.setNull(2, BINARY);
+			else ps.setBytes(2, ids[i]);
+			ps.setString(1, names[i]);
+			ps.addBatch();
+		}
+		int[] batchAffected = ps.executeBatch();
+		assertEquals(ids.length, batchAffected.length);
+		for (int affected : batchAffected) assertEquals(1, affected);
+		ps.close();
+	}
+
+	private boolean[] deleteBatch(Connection connection, byte[][] ids)
+			throws SQLException {
+		String sql = "DELETE FROM foo WHERE uniqueId = ?";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		for (byte[] id : ids) {
+			if (id == null) ps.setNull(1, BINARY);
+			else ps.setBytes(1, id);
+			ps.addBatch();
+		}
+		int[] batchAffected = ps.executeBatch();
+		assertEquals(ids.length, batchAffected.length);
+		boolean[] ret = new boolean[ids.length];
+		for (int i = 0; i < batchAffected.length; i++)
+			ret[i] = batchAffected[i] == 1;
+		ps.close();
+		return ret;
+	}
+
+	private String getPredecessor(Connection connection, byte[] id)
+			throws SQLException {
+		String sql = "SELECT name FROM foo WHERE uniqueId < ?"
+				+ " ORDER BY uniqueId DESC";
+		PreparedStatement ps = connection.prepareStatement(sql);
+		ps.setBytes(1, id);
+		ps.setMaxRows(1);
+		ResultSet rs = ps.executeQuery();
+		String name = rs.next() ? rs.getString(1) : null;
+		assertFalse(rs.next());
+		rs.close();
+		ps.close();
+		return name;
+	}
+
+	private List<String> getNames(Connection connection) throws SQLException {
+		String sql = "SELECT name FROM foo ORDER BY uniqueId NULLS FIRST";
+		List<String> names = new ArrayList<>();
+		PreparedStatement ps = connection.prepareStatement(sql);
+		ResultSet rs = ps.executeQuery();
+		while (rs.next()) names.add(rs.getString(1));
+		rs.close();
+		ps.close();
+		return names;
+	}
+
+	private boolean findSequence(File f, byte[] sequence) throws IOException {
+		if (f.isDirectory()) {
+			File[] children = f.listFiles();
+			if (children != null)
+				for (File child : children)
+					if (findSequence(child, sequence)) return true;
+			return false;
+		} else if (f.isFile()) {
+			FileInputStream in = new FileInputStream(f);
+			try {
+				int offset = 0;
+				while (true) {
+					int read = in.read();
+					if (read == -1) return false;
+					if (((byte) read) == sequence[offset]) {
+						offset++;
+						if (offset == sequence.length) return true;
+					} else {
+						offset = 0;
+					}
+				}
+			} finally {
+				in.close();
+			}
+		} else {
+			return false;
+		}
+	}
+
+	@After
+	public void tearDown() throws Exception {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java
index 5b62ba75071b94b5c8d163f5fa709f9d949d1f58..ebfe9879f2d23106629a5b23264c42a286420209 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java
@@ -1,345 +1,47 @@
 package org.briarproject.bramble.db;
 
-import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.bramble.util.StringUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
 
 import java.io.File;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Properties;
 
-import static java.sql.Types.BINARY;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+public class BasicH2Test extends BasicDatabaseTest {
 
-public class BasicH2Test extends BrambleTestCase {
+	private final SecretKey key = TestUtils.getSecretKey();
 
-	private static final String CREATE_TABLE =
-			"CREATE TABLE foo (uniqueId BINARY(32), name VARCHAR NOT NULL)";
-	private static final int BATCH_SIZE = 100;
-
-	private final File testDir = TestUtils.getTestDirectory();
-	private final File db = new File(testDir, "db");
-	private final String url = "jdbc:h2:" + db.getAbsolutePath();
-
-	private Connection connection = null;
-
-	@Before
-	public void setUp() throws Exception {
-		testDir.mkdirs();
-		Class.forName("org.h2.Driver");
-		connection = DriverManager.getConnection(url);
-	}
-
-	@Test
-	public void testInsertUpdateAndDelete() throws Exception {
-		// Create the table
-		createTable(connection);
-		// Generate an ID and two names
-		byte[] id = TestUtils.getRandomId();
-		String oldName = StringUtils.getRandomString(50);
-		String newName = StringUtils.getRandomString(50);
-		// Insert the ID and old name into the table
-		insertRow(id, oldName);
-		// Check that the old name can be retrieved using the ID
-		assertTrue(rowExists(id));
-		assertEquals(oldName, getName(id));
-		// Update the name
-		updateRow(id, newName);
-		// Check that the new name can be retrieved using the ID
-		assertTrue(rowExists(id));
-		assertEquals(newName, getName(id));
-		// Delete the row from the table
-		assertTrue(deleteRow(id));
-		// Check that the row no longer exists
-		assertFalse(rowExists(id));
-		// Deleting the row again should have no effect
-		assertFalse(deleteRow(id));
-	}
-
-	@Test
-	public void testBatchInsertUpdateAndDelete() throws Exception {
-		// Create the table
-		createTable(connection);
-		// Generate some IDs and two sets of names
-		byte[][] ids = new byte[BATCH_SIZE][];
-		String[] oldNames = new String[BATCH_SIZE];
-		String[] newNames = new String[BATCH_SIZE];
-		for (int i = 0; i < BATCH_SIZE; i++) {
-			ids[i] = TestUtils.getRandomId();
-			oldNames[i] = StringUtils.getRandomString(50);
-			newNames[i] = StringUtils.getRandomString(50);
-		}
-		// Insert the IDs and old names into the table as a batch
-		insertBatch(ids, oldNames);
-		// Update the names as a batch
-		updateBatch(ids, newNames);
-		// Check that the new names can be retrieved using the IDs
-		for (int i = 0; i < BATCH_SIZE; i++) {
-			assertTrue(rowExists(ids[i]));
-			assertEquals(newNames[i], getName(ids[i]));
-		}
-		// Delete the rows as a batch
-		boolean[] deleted = deleteBatch(ids);
-		// Check that the rows no longer exist
-		for (int i = 0; i < BATCH_SIZE; i++) {
-			assertTrue(deleted[i]);
-			assertFalse(rowExists(ids[i]));
-		}
-		// Deleting the rows again should have no effect
-		deleted = deleteBatch(ids);
-		for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]);
-	}
-
-	@Test
-	public void testSortOrder() throws Exception {
-		byte[] first = new byte[] {
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0
-		};
-		byte[] second = new byte[] {
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 127
-		};
-		byte[] third = new byte[] {
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, 0,
-				0, 0, 0, 0, 0, 0, 0, (byte) 255
-		};
-		// Create the table
-		createTable(connection);
-		// Insert the rows
-		insertRow(first, "first");
-		insertRow(second, "second");
-		insertRow(third, "third");
-		insertRow(null, "null");
-		// Check the ordering of the < operator: the null ID is not comparable
-		assertNull(getPredecessor(first));
-		assertEquals("first", getPredecessor(second));
-		assertEquals("second", getPredecessor(third));
-		assertNull(getPredecessor(null));
-		// Check the ordering of ORDER BY: nulls come first
-		List<String> names = getNames();
-		assertEquals(4, names.size());
-		assertEquals("null", names.get(0));
-		assertEquals("first", names.get(1));
-		assertEquals("second", names.get(2));
-		assertEquals("third", names.get(3));
-	}
-
-	private void createTable(Connection connection) throws SQLException {
-		try {
-			Statement s = connection.createStatement();
-			s.executeUpdate(CREATE_TABLE);
-			s.close();
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private void insertRow(byte[] id, String name) throws SQLException {
-		String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			if (id == null) ps.setNull(1, BINARY);
-			else ps.setBytes(1, id);
-			ps.setString(2, name);
-			int affected = ps.executeUpdate();
-			assertEquals(1, affected);
-			ps.close();
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private boolean rowExists(byte[] id) throws SQLException {
-		assertNotNull(id);
-		String sql = "SELECT NULL FROM foo WHERE uniqueID = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			ps.setBytes(1, id);
-			ResultSet rs = ps.executeQuery();
-			boolean found = rs.next();
-			assertFalse(rs.next());
-			rs.close();
-			ps.close();
-			return found;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private String getName(byte[] id) throws SQLException {
-		assertNotNull(id);
-		String sql = "SELECT name FROM foo WHERE uniqueID = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			ps.setBytes(1, id);
-			ResultSet rs = ps.executeQuery();
-			assertTrue(rs.next());
-			String name = rs.getString(1);
-			assertFalse(rs.next());
-			rs.close();
-			ps.close();
-			return name;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private void updateRow(byte[] id, String name) throws SQLException {
-		String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			if (id == null) ps.setNull(2, BINARY);
-			else ps.setBytes(2, id);
-			ps.setString(1, name);
-			assertEquals(1, ps.executeUpdate());
-			ps.close();
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private boolean deleteRow(byte[] id) throws SQLException {
-		String sql = "DELETE FROM foo WHERE uniqueId = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			if (id == null) ps.setNull(1, BINARY);
-			else ps.setBytes(1, id);
-			int affected = ps.executeUpdate();
-			ps.close();
-			return affected == 1;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
+	@Override
+	protected String getBinaryType() {
+		return "BINARY(32)";
 	}
 
-	private void insertBatch(byte[][] ids, String[] names) throws SQLException {
-		assertEquals(ids.length, names.length);
-		String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			for (int i = 0; i < ids.length; i++) {
-				if (ids[i] == null) ps.setNull(1, BINARY);
-				else ps.setBytes(1, ids[i]);
-				ps.setString(2, names[i]);
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			assertEquals(ids.length, batchAffected.length);
-			for (int affected : batchAffected) assertEquals(1, affected);
-			ps.close();
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private void updateBatch(byte[][] ids, String[] names) throws SQLException {
-		assertEquals(ids.length, names.length);
-		String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			for (int i = 0; i < ids.length; i++) {
-				if (ids[i] == null) ps.setNull(2, BINARY);
-				else ps.setBytes(2, ids[i]);
-				ps.setString(1, names[i]);
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			assertEquals(ids.length, batchAffected.length);
-			for (int affected : batchAffected) assertEquals(1, affected);
-			ps.close();
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private boolean[] deleteBatch(byte[][] ids) throws SQLException {
-		String sql = "DELETE FROM foo WHERE uniqueId = ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			for (byte[] id : ids) {
-				if (id == null) ps.setNull(1, BINARY);
-				else ps.setBytes(1, id);
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			assertEquals(ids.length, batchAffected.length);
-			boolean[] ret = new boolean[ids.length];
-			for (int i = 0; i < batchAffected.length; i++)
-				ret[i] = batchAffected[i] == 1;
-			ps.close();
-			return ret;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
-	}
-
-	private String getPredecessor(byte[] id) throws SQLException {
-		String sql = "SELECT name FROM foo WHERE uniqueId < ?"
-				+ " ORDER BY uniqueId DESC LIMIT ?";
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			ps.setBytes(1, id);
-			ps.setInt(2, 1);
-			ResultSet rs = ps.executeQuery();
-			String name = rs.next() ? rs.getString(1) : null;
-			assertFalse(rs.next());
-			rs.close();
-			ps.close();
-			return name;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
-		}
+	@Override
+	protected String getDriverName() {
+		return "org.h2.Driver";
 	}
 
-	private List<String> getNames() throws SQLException {
-		String sql = "SELECT name FROM foo ORDER BY uniqueId";
-		List<String> names = new ArrayList<>();
-		try {
-			PreparedStatement ps = connection.prepareStatement(sql);
-			ResultSet rs = ps.executeQuery();
-			while (rs.next()) names.add(rs.getString(1));
-			rs.close();
-			ps.close();
-			return names;
-		} catch (SQLException e) {
-			connection.close();
-			throw e;
+	@Override
+	protected Connection openConnection(File db, boolean encrypt)
+			throws SQLException {
+		String url = "jdbc:h2:split:" + db.getAbsolutePath()
+				+ ";DB_CLOSE_ON_EXIT=false";
+		Properties props = new Properties();
+		props.setProperty("user", "user");
+		if (encrypt) {
+			url += ";CIPHER=AES";
+			String hex = StringUtils.toHexString(key.getBytes());
+			props.setProperty("password", hex + " password");
 		}
+		return DriverManager.getConnection(url, props);
 	}
 
-	@After
-	public void tearDown() throws Exception {
-		if (connection != null) connection.close();
-		TestUtils.deleteTestDirectory(testDir);
+	@Override
+	protected void shutdownDatabase(File db, boolean encrypt)
+			throws SQLException {
+		// The DB is closed automatically when the connection is closed
 	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fae313cf36f2b9e474c36e92ae8583308692fac5
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java
@@ -0,0 +1,48 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.util.StringUtils;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class BasicHyperSqlTest extends BasicDatabaseTest {
+
+	private final SecretKey key = TestUtils.getSecretKey();
+
+	@Override
+	protected String getBinaryType() {
+		return "BINARY(32)";
+	}
+
+	@Override
+	protected String getDriverName() {
+		return "org.hsqldb.jdbc.JDBCDriver";
+	}
+
+	@Override
+	protected Connection openConnection(File db, boolean encrypt)
+			throws SQLException {
+		String url = "jdbc:hsqldb:file:" + db.getAbsolutePath() +
+				";sql.enforce_size=false;allow_empty_batch=true";
+		if (encrypt) {
+			String hex = StringUtils.toHexString(key.getBytes());
+			url += ";encrypt_lobs=true;crypt_type=AES;crypt_key=" + hex;
+		}
+		return DriverManager.getConnection(url);
+	}
+
+	@Override
+	protected void shutdownDatabase(File db, boolean encrypt)
+			throws SQLException {
+		Connection c = openConnection(db, encrypt);
+		Statement s = c.createStatement();
+		s.executeQuery("SHUTDOWN");
+		s.close();
+		c.close();
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java
index 4afea4bf42a6b39b0bcb5a9bea2f6142d999e026..fb0f6e4a3071be2ec9e50c0d6f6147cbf6ed8399 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java
@@ -1,1686 +1,16 @@
 package org.briarproject.bramble.db;
 
-import org.briarproject.bramble.api.contact.Contact;
-import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.db.Metadata;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.identity.LocalAuthor;
-import org.briarproject.bramble.api.plugin.TransportId;
-import org.briarproject.bramble.api.settings.Settings;
-import org.briarproject.bramble.api.sync.ClientId;
-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.bramble.api.sync.ValidationManager.State;
-import org.briarproject.bramble.api.transport.IncomingKeys;
-import org.briarproject.bramble.api.transport.OutgoingKeys;
-import org.briarproject.bramble.api.transport.TransportKeys;
-import org.briarproject.bramble.system.SystemClock;
-import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.bramble.test.TestDatabaseConfig;
-import org.briarproject.bramble.test.TestUtils;
-import org.briarproject.bramble.util.StringUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.briarproject.bramble.api.db.DatabaseConfig;
+import org.briarproject.bramble.api.system.Clock;
 
-import java.io.File;
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.briarproject.bramble.api.db.Metadata.REMOVE;
-import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
-import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
-import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class H2DatabaseTest extends BrambleTestCase {
-
-	private static final int ONE_MEGABYTE = 1024 * 1024;
-	private static final int MAX_SIZE = 5 * ONE_MEGABYTE;
-
-	private final File testDir = TestUtils.getTestDirectory();
-	private final GroupId groupId;
-	private final ClientId clientId;
-	private final Group group;
-	private final Author author;
-	private final AuthorId localAuthorId;
-	private final LocalAuthor localAuthor;
-	private final MessageId messageId;
-	private final long timestamp;
-	private final int size;
-	private final byte[] raw;
-	private final Message message;
-	private final TransportId transportId;
-	private final ContactId contactId;
+public class H2DatabaseTest extends JdbcDatabaseTest {
 
 	public H2DatabaseTest() throws Exception {
-		groupId = new GroupId(TestUtils.getRandomId());
-		clientId = new ClientId(StringUtils.getRandomString(5));
-		byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
-		group = new Group(groupId, clientId, descriptor);
-		AuthorId authorId = new AuthorId(TestUtils.getRandomId());
-		author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
-		localAuthorId = new AuthorId(TestUtils.getRandomId());
-		timestamp = System.currentTimeMillis();
-		localAuthor = new LocalAuthor(localAuthorId, "Bob",
-				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
-		messageId = new MessageId(TestUtils.getRandomId());
-		size = 1234;
-		raw = TestUtils.getRandomBytes(size);
-		message = new Message(messageId, groupId, timestamp, raw);
-		transportId = new TransportId("id");
-		contactId = new ContactId(1);
-	}
-
-	@Before
-	public void setUp() {
-		assertTrue(testDir.mkdirs());
-	}
-
-	@Test
-	public void testPersistence() throws Exception {
-		// Store some records
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-		assertFalse(db.containsContact(txn, contactId));
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		assertTrue(db.containsContact(txn, contactId));
-		assertFalse(db.containsGroup(txn, groupId));
-		db.addGroup(txn, group);
-		assertTrue(db.containsGroup(txn, groupId));
-		assertFalse(db.containsMessage(txn, messageId));
-		db.addMessage(txn, message, DELIVERED, true);
-		assertTrue(db.containsMessage(txn, messageId));
-		db.commitTransaction(txn);
-		db.close();
-
-		// Check that the records are still there
-		db = open(true);
-		txn = db.startTransaction();
-		assertTrue(db.containsContact(txn, contactId));
-		assertTrue(db.containsGroup(txn, groupId));
-		assertTrue(db.containsMessage(txn, messageId));
-		byte[] raw1 = db.getRawMessage(txn, messageId);
-		assertArrayEquals(raw, raw1);
-
-		// Delete the records
-		db.removeMessage(txn, messageId);
-		db.removeContact(txn, contactId);
-		db.removeGroup(txn, groupId);
-		db.commitTransaction(txn);
-		db.close();
-
-		// Check that the records are gone
-		db = open(true);
-		txn = db.startTransaction();
-		assertFalse(db.containsContact(txn, contactId));
-		assertFalse(db.containsGroup(txn, groupId));
-		assertFalse(db.containsMessage(txn, messageId));
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testRemovingGroupRemovesMessage() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and a message
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-
-		// Removing the group should remove the message
-		assertTrue(db.containsMessage(txn, messageId));
-		db.removeGroup(txn, groupId);
-		assertFalse(db.containsMessage(txn, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-
-		// The message has no status yet, so it should not be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Adding a status with seen = false should make the message sendable
-		db.addStatus(txn, contactId, messageId, false, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// Changing the status to seen = true should make the message unsendable
-		db.raiseSeenFlag(txn, contactId, messageId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSendableMessagesMustBeDelivered() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared but unvalidated message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, UNKNOWN, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The message has not been validated, so it should not be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Marking the message delivered should make it sendable
-		db.setMessageState(txn, messageId, DELIVERED);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// Marking the message invalid should make it unsendable
-		db.setMessageState(txn, messageId, INVALID);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Marking the message pending should make it unsendable
-		db.setMessageState(txn, messageId, PENDING);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSendableMessagesMustHaveSharedGroup() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, an invisible group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The group is invisible, so the message should not be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Making the group visible should not make the message sendable
-		db.addGroupVisibility(txn, contactId, groupId, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Sharing the group should make the message sendable
-		db.setGroupVisibility(txn, contactId, groupId, true);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// Unsharing the group should make the message unsendable
-		db.setGroupVisibility(txn, contactId, groupId, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Making the group invisible should make the message unsendable
-		db.removeGroupVisibility(txn, contactId, groupId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSendableMessagesMustBeShared() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and an unshared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, false);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The message is not shared, so it should not be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// Sharing the message should make it sendable
-		db.setMessageShared(txn, messageId);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSendableMessagesMustFitCapacity() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The message is sendable, but too large to send
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				size - 1);
-		assertTrue(ids.isEmpty());
-
-		// The message is just the right size to send
-		ids = db.getMessagesToSend(txn, contactId, size);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMessagesToAck() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact and a visible group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, false);
-
-		// Add some messages to ack
-		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
-		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, true);
-		db.raiseAckFlag(txn, contactId, messageId);
-		db.addMessage(txn, message1, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId1, false, true);
-		db.raiseAckFlag(txn, contactId, messageId1);
-
-		// Both message IDs should be returned
-		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
-		assertEquals(Arrays.asList(messageId, messageId1), ids);
-
-		// Remove both message IDs
-		db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
-
-		// Both message IDs should have been removed
-		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
-				contactId, 1234));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testOutstandingMessageAcked() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// Retrieve the message from the database and mark it as sent
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
-
-		// The message should no longer be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-
-		// Pretend that the message was acked
-		db.raiseSeenFlag(txn, contactId, messageId);
-
-		// The message still should not be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetFreeSpace() throws Exception {
-		byte[] largeBody = new byte[MAX_MESSAGE_LENGTH];
-		for (int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i;
-		Message message = new Message(messageId, groupId, timestamp, largeBody);
-		Database<Connection> db = open(false);
-
-		// Sanity check: there should be enough space on disk for this test
-		assertTrue(testDir.getFreeSpace() > MAX_SIZE);
-
-		// The free space should not be more than the allowed maximum size
-		long free = db.getFreeSpace();
-		assertTrue(free <= MAX_SIZE);
-		assertTrue(free > 0);
-
-		// Storing a message should reduce the free space
-		Connection txn = db.startTransaction();
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.commitTransaction(txn);
-		assertTrue(db.getFreeSpace() < free);
-
-		db.close();
-	}
-
-	@Test
-	public void testCloseWaitsForCommit() throws Exception {
-		CountDownLatch closing = new CountDownLatch(1);
-		CountDownLatch closed = new CountDownLatch(1);
-		AtomicBoolean transactionFinished = new AtomicBoolean(false);
-		AtomicBoolean error = new AtomicBoolean(false);
-		Database<Connection> db = open(false);
-
-		// Start a transaction
-		Connection txn = db.startTransaction();
-		// In another thread, close the database
-		Thread close = new Thread(() -> {
-			try {
-				closing.countDown();
-				db.close();
-				if (!transactionFinished.get()) error.set(true);
-				closed.countDown();
-			} catch (Exception e) {
-				error.set(true);
-			}
-		});
-		close.start();
-		closing.await();
-		// Do whatever the transaction needs to do
-		Thread.sleep(10);
-		transactionFinished.set(true);
-		// Commit the transaction
-		db.commitTransaction(txn);
-		// The other thread should now terminate
-		assertTrue(closed.await(5, SECONDS));
-		// Check that the other thread didn't encounter an error
-		assertFalse(error.get());
-	}
-
-	@Test
-	public void testCloseWaitsForAbort() throws Exception {
-		CountDownLatch closing = new CountDownLatch(1);
-		CountDownLatch closed = new CountDownLatch(1);
-		AtomicBoolean transactionFinished = new AtomicBoolean(false);
-		AtomicBoolean error = new AtomicBoolean(false);
-		Database<Connection> db = open(false);
-
-		// Start a transaction
-		Connection txn = db.startTransaction();
-		// In another thread, close the database
-		Thread close = new Thread(() -> {
-			try {
-				closing.countDown();
-				db.close();
-				if (!transactionFinished.get()) error.set(true);
-				closed.countDown();
-			} catch (Exception e) {
-				error.set(true);
-			}
-		});
-		close.start();
-		closing.await();
-		// Do whatever the transaction needs to do
-		Thread.sleep(10);
-		transactionFinished.set(true);
-		// Abort the transaction
-		db.abortTransaction(txn);
-		// The other thread should now terminate
-		assertTrue(closed.await(5, SECONDS));
-		// Check that the other thread didn't encounter an error
-		assertFalse(error.get());
-	}
-
-	@Test
-	public void testUpdateSettings() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Store some settings
-		Settings s = new Settings();
-		s.put("foo", "foo");
-		s.put("bar", "bar");
-		db.mergeSettings(txn, s, "test");
-		assertEquals(s, db.getSettings(txn, "test"));
-
-		// Update one of the settings and add another
-		Settings s1 = new Settings();
-		s1.put("bar", "baz");
-		s1.put("bam", "bam");
-		db.mergeSettings(txn, s1, "test");
-
-		// Check that the settings were merged
-		Settings merged = new Settings();
-		merged.put("foo", "foo");
-		merged.put("bar", "baz");
-		merged.put("bam", "bam");
-		assertEquals(merged, db.getSettings(txn, "test"));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testContainsVisibleMessageRequiresMessageInDatabase()
-			throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact and a shared group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-
-		// The message is not in the database
-		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testContainsVisibleMessageRequiresGroupInDatabase()
-			throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-
-		// The group is not in the database
-		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testContainsVisibleMessageRequiresVisibileGroup()
-			throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a group and a message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The group is not visible
-		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGroupVisibility() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact and a group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-
-		// The group should not be visible to the contact
-		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.emptyList(),
-				db.getGroupVisibility(txn, groupId));
-
-		// Make the group visible to the contact
-		db.addGroupVisibility(txn, contactId, groupId, false);
-		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
-				db.getGroupVisibility(txn, groupId));
-
-		// Share the group with the contact
-		db.setGroupVisibility(txn, contactId, groupId, true);
-		assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
-				db.getGroupVisibility(txn, groupId));
-
-		// Unshare the group with the contact
-		db.setGroupVisibility(txn, contactId, groupId, false);
-		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
-				db.getGroupVisibility(txn, groupId));
-
-		// Make the group invisible again
-		db.removeGroupVisibility(txn, contactId, groupId);
-		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.emptyList(),
-				db.getGroupVisibility(txn, groupId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testTransportKeys() throws Exception {
-		TransportKeys keys = createTransportKeys();
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Initially there should be no transport keys in the database
-		assertEquals(Collections.emptyMap(),
-				db.getTransportKeys(txn, transportId));
-
-		// Add the contact, the transport and the transport keys
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addTransport(txn, transportId, 123);
-		db.addTransportKeys(txn, contactId, keys);
-
-		// Retrieve the transport keys
-		Map<ContactId, TransportKeys> newKeys =
-				db.getTransportKeys(txn, transportId);
-		assertEquals(1, newKeys.size());
-		Entry<ContactId, TransportKeys> e =
-				newKeys.entrySet().iterator().next();
-		assertEquals(contactId, e.getKey());
-		TransportKeys k = e.getValue();
-		assertEquals(transportId, k.getTransportId());
-		assertKeysEquals(keys.getPreviousIncomingKeys(),
-				k.getPreviousIncomingKeys());
-		assertKeysEquals(keys.getCurrentIncomingKeys(),
-				k.getCurrentIncomingKeys());
-		assertKeysEquals(keys.getNextIncomingKeys(),
-				k.getNextIncomingKeys());
-		assertKeysEquals(keys.getCurrentOutgoingKeys(),
-				k.getCurrentOutgoingKeys());
-
-		// Removing the contact should remove the transport keys
-		db.removeContact(txn, contactId);
-		assertEquals(Collections.emptyMap(),
-				db.getTransportKeys(txn, transportId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) {
-		assertArrayEquals(expected.getTagKey().getBytes(),
-				actual.getTagKey().getBytes());
-		assertArrayEquals(expected.getHeaderKey().getBytes(),
-				actual.getHeaderKey().getBytes());
-		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
-		assertEquals(expected.getWindowBase(), actual.getWindowBase());
-		assertArrayEquals(expected.getWindowBitmap(), actual.getWindowBitmap());
-	}
-
-	private void assertKeysEquals(OutgoingKeys expected, OutgoingKeys actual) {
-		assertArrayEquals(expected.getTagKey().getBytes(),
-				actual.getTagKey().getBytes());
-		assertArrayEquals(expected.getHeaderKey().getBytes(),
-				actual.getHeaderKey().getBytes());
-		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
-		assertEquals(expected.getStreamCounter(), actual.getStreamCounter());
-	}
-
-	@Test
-	public void testIncrementStreamCounter() throws Exception {
-		TransportKeys keys = createTransportKeys();
-		long rotationPeriod = keys.getCurrentOutgoingKeys().getRotationPeriod();
-		long streamCounter = keys.getCurrentOutgoingKeys().getStreamCounter();
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add the contact, transport and transport keys
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addTransport(txn, transportId, 123);
-		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
-
-		// Increment the stream counter twice and retrieve the transport keys
-		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
-		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
-		Map<ContactId, TransportKeys> newKeys =
-				db.getTransportKeys(txn, transportId);
-		assertEquals(1, newKeys.size());
-		Entry<ContactId, TransportKeys> e =
-				newKeys.entrySet().iterator().next();
-		assertEquals(contactId, e.getKey());
-		TransportKeys k = e.getValue();
-		assertEquals(transportId, k.getTransportId());
-		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
-		assertEquals(rotationPeriod, outCurr.getRotationPeriod());
-		assertEquals(streamCounter + 2, outCurr.getStreamCounter());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSetReorderingWindow() throws Exception {
-		TransportKeys keys = createTransportKeys();
-		long rotationPeriod = keys.getCurrentIncomingKeys().getRotationPeriod();
-		long base = keys.getCurrentIncomingKeys().getWindowBase();
-		byte[] bitmap = keys.getCurrentIncomingKeys().getWindowBitmap();
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add the contact, transport and transport keys
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addTransport(txn, transportId, 123);
-		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
-
-		// Update the reordering window and retrieve the transport keys
-		new Random().nextBytes(bitmap);
-		db.setReorderingWindow(txn, contactId, transportId, rotationPeriod,
-				base + 1, bitmap);
-		Map<ContactId, TransportKeys> newKeys =
-				db.getTransportKeys(txn, transportId);
-		assertEquals(1, newKeys.size());
-		Entry<ContactId, TransportKeys> e =
-				newKeys.entrySet().iterator().next();
-		assertEquals(contactId, e.getKey());
-		TransportKeys k = e.getValue();
-		assertEquals(transportId, k.getTransportId());
-		IncomingKeys inCurr = k.getCurrentIncomingKeys();
-		assertEquals(rotationPeriod, inCurr.getRotationPeriod());
-		assertEquals(base + 1, inCurr.getWindowBase());
-		assertArrayEquals(bitmap, inCurr.getWindowBitmap());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetContactsByAuthorId() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a local author - no contacts should be associated
-		db.addLocalAuthor(txn, localAuthor);
-
-		// Add a contact associated with the local author
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-
-		// Ensure contact is returned from database by Author ID
-		Collection<Contact> contacts =
-				db.getContactsByAuthorId(txn, author.getId());
-		assertEquals(1, contacts.size());
-		assertEquals(contactId, contacts.iterator().next().getId());
-
-		// Ensure no contacts are returned after contact was deleted
-		db.removeContact(txn, contactId);
-		contacts = db.getContactsByAuthorId(txn, author.getId());
-		assertEquals(0, contacts.size());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetContactsByLocalAuthorId() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a local author - no contacts should be associated
-		db.addLocalAuthor(txn, localAuthor);
-		Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
-		assertEquals(Collections.emptyList(), contacts);
-
-		// Add a contact associated with the local author
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		contacts = db.getContacts(txn, localAuthorId);
-		assertEquals(Collections.singletonList(contactId), contacts);
-
-		// Remove the local author - the contact should be removed
-		db.removeLocalAuthor(txn, localAuthorId);
-		contacts = db.getContacts(txn, localAuthorId);
-		assertEquals(Collections.emptyList(), contacts);
-		assertFalse(db.containsContact(txn, contactId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testOfferedMessages() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact - initially there should be no offered messages
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		assertEquals(0, db.countOfferedMessages(txn, contactId));
-
-		// Add some offered messages and count them
-		List<MessageId> ids = new ArrayList<>();
-		for (int i = 0; i < 10; i++) {
-			MessageId m = new MessageId(TestUtils.getRandomId());
-			db.addOfferedMessage(txn, contactId, m);
-			ids.add(m);
-		}
-		assertEquals(10, db.countOfferedMessages(txn, contactId));
-
-		// Remove some of the offered messages and count again
-		List<MessageId> half = ids.subList(0, 5);
-		db.removeOfferedMessages(txn, contactId, half);
-		assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
-		assertEquals(4, db.countOfferedMessages(txn, contactId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGroupMetadata() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group
-		db.addGroup(txn, group);
-
-		// Attach some metadata to the group
-		Metadata metadata = new Metadata();
-		metadata.put("foo", new byte[]{'b', 'a', 'r'});
-		metadata.put("baz", new byte[]{'b', 'a', 'm'});
-		db.mergeGroupMetadata(txn, groupId, metadata);
-
-		// Retrieve the metadata for the group
-		Metadata retrieved = db.getGroupMetadata(txn, groupId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Update the metadata
-		metadata.put("foo", REMOVE);
-		metadata.put("baz", new byte[] {'q', 'u', 'x'});
-		db.mergeGroupMetadata(txn, groupId, metadata);
-
-		// Retrieve the metadata again
-		retrieved = db.getGroupMetadata(txn, groupId);
-		assertEquals(1, retrieved.size());
-		assertFalse(retrieved.containsKey("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMessageMetadata() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and a message
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-
-		// Attach some metadata to the message
-		Metadata metadata = new Metadata();
-		metadata.put("foo", new byte[]{'b', 'a', 'r'});
-		metadata.put("baz", new byte[]{'b', 'a', 'm'});
-		db.mergeMessageMetadata(txn, messageId, metadata);
-
-		// Retrieve the metadata for the message
-		Metadata retrieved = db.getMessageMetadata(txn, messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Retrieve the metadata for the group
-		Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId);
-		assertEquals(1, all.size());
-		assertTrue(all.containsKey(messageId));
-		retrieved = all.get(messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Update the metadata
-		metadata.put("foo", REMOVE);
-		metadata.put("baz", new byte[] {'q', 'u', 'x'});
-		db.mergeMessageMetadata(txn, messageId, metadata);
-
-		// Retrieve the metadata again
-		retrieved = db.getMessageMetadata(txn, messageId);
-		assertEquals(1, retrieved.size());
-		assertFalse(retrieved.containsKey("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Retrieve the metadata for the group again
-		all = db.getMessageMetadata(txn, groupId);
-		assertEquals(1, all.size());
-		assertTrue(all.containsKey(messageId));
-		retrieved = all.get(messageId);
-		assertEquals(1, retrieved.size());
-		assertFalse(retrieved.containsKey("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Delete the metadata
-		db.deleteMessageMetadata(txn, messageId);
-
-		// Retrieve the metadata again
-		retrieved = db.getMessageMetadata(txn, messageId);
-		assertTrue(retrieved.isEmpty());
-
-		// Retrieve the metadata for the group again
-		all = db.getMessageMetadata(txn, groupId);
-		assertTrue(all.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMessageMetadataOnlyForDeliveredMessages() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and a message
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-
-		// Attach some metadata to the message
-		Metadata metadata = new Metadata();
-		metadata.put("foo", new byte[]{'b', 'a', 'r'});
-		metadata.put("baz", new byte[]{'b', 'a', 'm'});
-		db.mergeMessageMetadata(txn, messageId, metadata);
-
-		// Retrieve the metadata for the message
-		Metadata retrieved = db.getMessageMetadata(txn, messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-		Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId);
-		assertEquals(1, map.size());
-		assertTrue(map.get(messageId).containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo"));
-		assertTrue(map.get(messageId).containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz"));
-
-		// No metadata for unknown messages
-		db.setMessageState(txn, messageId, UNKNOWN);
-		retrieved = db.getMessageMetadata(txn, messageId);
-		assertTrue(retrieved.isEmpty());
-		map = db.getMessageMetadata(txn, groupId);
-		assertTrue(map.isEmpty());
-
-		// No metadata for invalid messages
-		db.setMessageState(txn, messageId, INVALID);
-		retrieved = db.getMessageMetadata(txn, messageId);
-		assertTrue(retrieved.isEmpty());
-		map = db.getMessageMetadata(txn, groupId);
-		assertTrue(map.isEmpty());
-
-		// No metadata for pending messages
-		db.setMessageState(txn, messageId, PENDING);
-		retrieved = db.getMessageMetadata(txn, messageId);
-		assertTrue(retrieved.isEmpty());
-		map = db.getMessageMetadata(txn, groupId);
-		assertTrue(map.isEmpty());
-
-		// Validator can get metadata for pending messages
-		retrieved = db.getMessageMetadataForValidator(txn, messageId);
-		assertFalse(retrieved.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMetadataQueries() throws Exception {
-		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
-		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and two messages
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addMessage(txn, message1, DELIVERED, true);
-
-		// Attach some metadata to the messages
-		Metadata metadata = new Metadata();
-		metadata.put("foo", new byte[]{'b', 'a', 'r'});
-		metadata.put("baz", new byte[]{'b', 'a', 'm'});
-		db.mergeMessageMetadata(txn, messageId, metadata);
-		Metadata metadata1 = new Metadata();
-		metadata1.put("foo", new byte[]{'q', 'u', 'x'});
-		db.mergeMessageMetadata(txn, messageId1, metadata1);
-
-		// Retrieve all the metadata for the group
-		Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId);
-		assertEquals(2, all.size());
-		assertTrue(all.containsKey(messageId));
-		assertTrue(all.containsKey(messageId1));
-		Metadata retrieved = all.get(messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-		retrieved = all.get(messageId1);
-		assertEquals(1, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
-
-		// Query the metadata with an empty query
-		Metadata query = new Metadata();
-		all = db.getMessageMetadata(txn, groupId, query);
-		assertEquals(2, all.size());
-		assertTrue(all.containsKey(messageId));
-		assertTrue(all.containsKey(messageId1));
-		retrieved = all.get(messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-		retrieved = all.get(messageId1);
-		assertEquals(1, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
-
-		// Use a single-term query that matches the first message
-		query = new Metadata();
-		query.put("foo", metadata.get("foo"));
-		all = db.getMessageMetadata(txn, groupId, query);
-		assertEquals(1, all.size());
-		assertTrue(all.containsKey(messageId));
-		retrieved = all.get(messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Use a single-term query that matches the second message
-		query = new Metadata();
-		query.put("foo", metadata1.get("foo"));
-		all = db.getMessageMetadata(txn, groupId, query);
-		assertEquals(1, all.size());
-		assertTrue(all.containsKey(messageId1));
-		retrieved = all.get(messageId1);
-		assertEquals(1, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
-
-		// Use a multi-term query that matches the first message
-		query = new Metadata();
-		query.put("foo", metadata.get("foo"));
-		query.put("baz", metadata.get("baz"));
-		all = db.getMessageMetadata(txn, groupId, query);
-		assertEquals(1, all.size());
-		assertTrue(all.containsKey(messageId));
-		retrieved = all.get(messageId);
-		assertEquals(2, retrieved.size());
-		assertTrue(retrieved.containsKey("foo"));
-		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
-		assertTrue(retrieved.containsKey("baz"));
-		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
-
-		// Use a multi-term query that doesn't match any messages
-		query = new Metadata();
-		query.put("foo", metadata1.get("foo"));
-		query.put("baz", metadata.get("baz"));
-		all = db.getMessageMetadata(txn, groupId, query);
-		assertTrue(all.isEmpty());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception {
-		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
-		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and two messages
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addMessage(txn, message1, DELIVERED, true);
-
-		// Attach some metadata to the messages
-		Metadata metadata = new Metadata();
-		metadata.put("foo", new byte[]{'b', 'a', 'r'});
-		metadata.put("baz", new byte[]{'b', 'a', 'm'});
-		db.mergeMessageMetadata(txn, messageId, metadata);
-		Metadata metadata1 = new Metadata();
-		metadata1.put("foo", new byte[]{'b', 'a', 'r'});
-		db.mergeMessageMetadata(txn, messageId1, metadata1);
-
-		for (int i = 0; i < 2; i++) {
-			Metadata query;
-			if (i == 0) {
-				// Query the metadata with an empty query
-				query = new Metadata();
-			} else {
-				// Query for foo
-				query = new Metadata();
-				query.put("foo", new byte[]{'b', 'a', 'r'});
-			}
-
-			db.setMessageState(txn, messageId, DELIVERED);
-			db.setMessageState(txn, messageId1, DELIVERED);
-			Map<MessageId, Metadata> all =
-					db.getMessageMetadata(txn, groupId, query);
-			assertEquals(2, all.size());
-			assertMetadataEquals(metadata, all.get(messageId));
-			assertMetadataEquals(metadata1, all.get(messageId1));
-
-			// No metadata for unknown messages
-			db.setMessageState(txn, messageId, UNKNOWN);
-			all = db.getMessageMetadata(txn, groupId, query);
-			assertEquals(1, all.size());
-			assertMetadataEquals(metadata1, all.get(messageId1));
-
-			// No metadata for invalid messages
-			db.setMessageState(txn, messageId, INVALID);
-			all = db.getMessageMetadata(txn, groupId, query);
-			assertEquals(1, all.size());
-			assertMetadataEquals(metadata1, all.get(messageId1));
-
-			// No metadata for pending messages
-			db.setMessageState(txn, messageId, PENDING);
-			all = db.getMessageMetadata(txn, groupId, query);
-			assertEquals(1, all.size());
-			assertMetadataEquals(metadata1, all.get(messageId1));
-		}
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	private void assertMetadataEquals(Metadata m1, Metadata m2) {
-		assertEquals(m1.keySet(), m2.keySet());
-		for (Entry<String, byte[]> e : m1.entrySet()) {
-			assertArrayEquals(e.getValue(), m2.get(e.getKey()));
-		}
-	}
-
-	@Test
-	public void testMessageDependencies() throws Exception {
-		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
-		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
-		MessageId messageId3 = new MessageId(TestUtils.getRandomId());
-		MessageId messageId4 = new MessageId(TestUtils.getRandomId());
-		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-		Message message2 = new Message(messageId2, groupId, timestamp, raw);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and some messages
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, PENDING, true);
-		db.addMessage(txn, message1, DELIVERED, true);
-		db.addMessage(txn, message2, INVALID, true);
-
-		// Add dependencies
-		db.addMessageDependency(txn, groupId, messageId, messageId1);
-		db.addMessageDependency(txn, groupId, messageId, messageId2);
-		db.addMessageDependency(txn, groupId, messageId1, messageId3);
-		db.addMessageDependency(txn, groupId, messageId2, messageId4);
-
-		Map<MessageId, State> dependencies;
-
-		// Retrieve dependencies for root
-		dependencies = db.getMessageDependencies(txn, messageId);
-		assertEquals(2, dependencies.size());
-		assertEquals(DELIVERED, dependencies.get(messageId1));
-		assertEquals(INVALID, dependencies.get(messageId2));
-
-		// Retrieve dependencies for message 1
-		dependencies = db.getMessageDependencies(txn, messageId1);
-		assertEquals(1, dependencies.size());
-		assertEquals(UNKNOWN, dependencies.get(messageId3)); // Missing
-
-		// Retrieve dependencies for message 2
-		dependencies = db.getMessageDependencies(txn, messageId2);
-		assertEquals(1, dependencies.size());
-		assertEquals(UNKNOWN, dependencies.get(messageId4)); // Missing
-
-		// Make sure leaves have no dependencies
-		dependencies = db.getMessageDependencies(txn, messageId3);
-		assertEquals(0, dependencies.size());
-		dependencies = db.getMessageDependencies(txn, messageId4);
-		assertEquals(0, dependencies.size());
-
-		Map<MessageId, State> dependents;
-
-		// Root message does not have dependents
-		dependents = db.getMessageDependents(txn, messageId);
-		assertEquals(0, dependents.size());
-
-		// Messages 1 and 2 have the root as a dependent
-		dependents = db.getMessageDependents(txn, messageId1);
-		assertEquals(1, dependents.size());
-		assertEquals(PENDING, dependents.get(messageId));
-		dependents = db.getMessageDependents(txn, messageId2);
-		assertEquals(1, dependents.size());
-		assertEquals(PENDING, dependents.get(messageId));
-
-		// Message 3 has message 1 as a dependent
-		dependents = db.getMessageDependents(txn, messageId3);
-		assertEquals(1, dependents.size());
-		assertEquals(DELIVERED, dependents.get(messageId1));
-
-		// Message 4 has message 2 as a dependent
-		dependents = db.getMessageDependents(txn, messageId4);
-		assertEquals(1, dependents.size());
-		assertEquals(INVALID, dependents.get(messageId2));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMessageDependenciesAcrossGroups() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and a message
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, PENDING, true);
-
-		// Add a second group
-		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
-		Group group1 = new Group(groupId1, clientId,
-				TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
-		db.addGroup(txn, group1);
-
-		// Add a message to the second group
-		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
-		Message message1 = new Message(messageId1, groupId1, timestamp, raw);
-		db.addMessage(txn, message1, DELIVERED, true);
-
-		// Create an ID for a missing message
-		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
-
-		// Add another message to the first group
-		MessageId messageId3 = new MessageId(TestUtils.getRandomId());
-		Message message3 = new Message(messageId3, groupId, timestamp, raw);
-		db.addMessage(txn, message3, DELIVERED, true);
-
-		// Add dependencies between the messages
-		db.addMessageDependency(txn, groupId, messageId, messageId1);
-		db.addMessageDependency(txn, groupId, messageId, messageId2);
-		db.addMessageDependency(txn, groupId, messageId, messageId3);
-
-		// Retrieve the dependencies for the root
-		Map<MessageId, State> dependencies;
-		dependencies = db.getMessageDependencies(txn, messageId);
-
-		// The cross-group dependency should have state INVALID
-		assertEquals(INVALID, dependencies.get(messageId1));
-
-		// The missing dependency should have state UNKNOWN
-		assertEquals(UNKNOWN, dependencies.get(messageId2));
-
-		// The valid dependency should have its real state
-		assertEquals(DELIVERED, dependencies.get(messageId3));
-
-		// Retrieve the dependents for the message in the second group
-		Map<MessageId, State> dependents;
-		dependents = db.getMessageDependents(txn, messageId1);
-
-		// The cross-group dependent should have its real state
-		assertEquals(PENDING, dependents.get(messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetPendingMessagesForDelivery() throws Exception {
-		MessageId mId1 = new MessageId(TestUtils.getRandomId());
-		MessageId mId2 = new MessageId(TestUtils.getRandomId());
-		MessageId mId3 = new MessageId(TestUtils.getRandomId());
-		MessageId mId4 = new MessageId(TestUtils.getRandomId());
-		Message m1 = new Message(mId1, groupId, timestamp, raw);
-		Message m2 = new Message(mId2, groupId, timestamp, raw);
-		Message m3 = new Message(mId3, groupId, timestamp, raw);
-		Message m4 = new Message(mId4, groupId, timestamp, raw);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and some messages with different states
-		db.addGroup(txn, group);
-		db.addMessage(txn, m1, UNKNOWN, true);
-		db.addMessage(txn, m2, INVALID, true);
-		db.addMessage(txn, m3, PENDING, true);
-		db.addMessage(txn, m4, DELIVERED, true);
-
-		Collection<MessageId> result;
-
-		// Retrieve messages to be validated
-		result = db.getMessagesToValidate(txn, clientId);
-		assertEquals(1, result.size());
-		assertTrue(result.contains(mId1));
-
-		// Retrieve pending messages
-		result = db.getPendingMessages(txn, clientId);
-		assertEquals(1, result.size());
-		assertTrue(result.contains(mId3));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetMessagesToShare() throws Exception {
-		MessageId mId1 = new MessageId(TestUtils.getRandomId());
-		MessageId mId2 = new MessageId(TestUtils.getRandomId());
-		MessageId mId3 = new MessageId(TestUtils.getRandomId());
-		MessageId mId4 = new MessageId(TestUtils.getRandomId());
-		Message m1 = new Message(mId1, groupId, timestamp, raw);
-		Message m2 = new Message(mId2, groupId, timestamp, raw);
-		Message m3 = new Message(mId3, groupId, timestamp, raw);
-		Message m4 = new Message(mId4, groupId, timestamp, raw);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and some messages
-		db.addGroup(txn, group);
-		db.addMessage(txn, m1, DELIVERED, true);
-		db.addMessage(txn, m2, DELIVERED, false);
-		db.addMessage(txn, m3, DELIVERED, false);
-		db.addMessage(txn, m4, DELIVERED, true);
-
-		// Introduce dependencies between the messages
-		db.addMessageDependency(txn, groupId, mId1, mId2);
-		db.addMessageDependency(txn, groupId, mId3, mId1);
-		db.addMessageDependency(txn, groupId, mId4, mId3);
-
-		// Retrieve messages to be shared
-		Collection<MessageId> result =
-				db.getMessagesToShare(txn, clientId);
-		assertEquals(2, result.size());
-		assertTrue(result.contains(mId2));
-		assertTrue(result.contains(mId3));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testGetMessageStatus() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The message should not be sent or seen
-		MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertFalse(status.isSent());
-		assertFalse(status.isSeen());
-
-		// The same status should be returned when querying by group
-		Collection<MessageStatus> statuses = db.getMessageStatus(txn,
-				contactId, groupId);
-		assertEquals(1, statuses.size());
-		status = statuses.iterator().next();
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertFalse(status.isSent());
-		assertFalse(status.isSeen());
-
-		// Pretend the message was sent to the contact
-		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
-
-		// The message should be sent but not seen
-		status = db.getMessageStatus(txn, contactId, messageId);
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertTrue(status.isSent());
-		assertFalse(status.isSeen());
-
-		// The same status should be returned when querying by group
-		statuses = db.getMessageStatus(txn, contactId, groupId);
-		assertEquals(1, statuses.size());
-		status = statuses.iterator().next();
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertTrue(status.isSent());
-		assertFalse(status.isSeen());
-
-		// Pretend the message was acked by the contact
-		db.raiseSeenFlag(txn, contactId, messageId);
-
-		// The message should be sent and seen
-		status = db.getMessageStatus(txn, contactId, messageId);
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertTrue(status.isSent());
-		assertTrue(status.isSeen());
-
-		// The same status should be returned when querying by group
-		statuses = db.getMessageStatus(txn, contactId, groupId);
-		assertEquals(1, statuses.size());
-		status = statuses.iterator().next();
-		assertEquals(messageId, status.getMessageId());
-		assertEquals(contactId, status.getContactId());
-		assertTrue(status.isSent());
-		assertTrue(status.isSeen());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testDifferentLocalAuthorsCanHaveTheSameContact()
-			throws Exception {
-		AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
-		LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
-				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add two local authors
-		db.addLocalAuthor(txn, localAuthor);
-		db.addLocalAuthor(txn, localAuthor1);
-
-		// Add the same contact for each local author
-		ContactId contactId =
-				db.addContact(txn, author, localAuthorId, true, true);
-		ContactId contactId1 =
-				db.addContact(txn, author, localAuthorId1, true, true);
-
-		// The contacts should be distinct
-		assertNotEquals(contactId, contactId1);
-		assertEquals(2, db.getContacts(txn).size());
-		assertEquals(1, db.getContacts(txn, localAuthorId).size());
-		assertEquals(1, db.getContacts(txn, localAuthorId1).size());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testDeleteMessage() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, a shared group and a shared message
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
-
-		// The message should be visible to the contact
-		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
-
-		// The message should be sendable
-		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
-				ONE_MEGABYTE);
-		assertEquals(Collections.singletonList(messageId), ids);
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// The raw message should not be null
-		assertNotNull(db.getRawMessage(txn, messageId));
-
-		// Delete the message
-		db.deleteMessage(txn, messageId);
-
-		// The message should be visible to the contact
-		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
-
-		// The message should not be sendable
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertTrue(ids.isEmpty());
-		ids = db.getMessagesToOffer(txn, contactId, 100);
-		assertTrue(ids.isEmpty());
-
-		// The raw message should be null
-		assertNull(db.getRawMessage(txn, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSetContactActive() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-
-		// The contact should be active
-		Contact contact = db.getContact(txn, contactId);
-		assertTrue(contact.isActive());
-
-		// Set the contact inactive
-		db.setContactActive(txn, contactId, false);
-
-		// The contact should be inactive
-		contact = db.getContact(txn, contactId);
-		assertFalse(contact.isActive());
-
-		// Set the contact active
-		db.setContactActive(txn, contactId, true);
-
-		// The contact should be active
-		contact = db.getContact(txn, contactId);
-		assertTrue(contact.isActive());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testSetMessageState() throws Exception {
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a group and a message
-		db.addGroup(txn, group);
-		db.addMessage(txn, message, UNKNOWN, false);
-
-		// Walk the message through the validation and delivery states
-		assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
-		db.setMessageState(txn, messageId, INVALID);
-		assertEquals(INVALID, db.getMessageState(txn, messageId));
-		db.setMessageState(txn, messageId, PENDING);
-		assertEquals(PENDING, db.getMessageState(txn, messageId));
-		db.setMessageState(txn, messageId, DELIVERED);
-		assertEquals(DELIVERED, db.getMessageState(txn, messageId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testExceptionHandling() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-		try {
-			// Ask for a nonexistent message - an exception should be thrown
-			db.getRawMessage(txn, messageId);
-			fail();
-		} catch (DbException expected) {
-			// It should be possible to abort the transaction without error
-			db.abortTransaction(txn);
-		}
-		// It should be possible to close the database cleanly
-		db.close();
-	}
-
-	private Database<Connection> open(boolean resume) throws Exception {
-		Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir,
-				MAX_SIZE), new SystemClock());
-		if (!resume) TestUtils.deleteTestDirectory(testDir);
-		db.open();
-		return db;
-	}
-
-	private TransportKeys createTransportKeys() {
-		SecretKey inPrevTagKey = TestUtils.getSecretKey();
-		SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
-		IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
-				1, 123, new byte[4]);
-		SecretKey inCurrTagKey = TestUtils.getSecretKey();
-		SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
-		IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
-				2, 234, new byte[4]);
-		SecretKey inNextTagKey = TestUtils.getSecretKey();
-		SecretKey inNextHeaderKey = TestUtils.getSecretKey();
-		IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
-				3, 345, new byte[4]);
-		SecretKey outCurrTagKey = TestUtils.getSecretKey();
-		SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
-		OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
-				2, 456);
-		return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
+		super();
 	}
 
-	@After
-	public void tearDown() {
-		TestUtils.deleteTestDirectory(testDir);
+	@Override
+	protected JdbcDatabase createDatabase(DatabaseConfig config, Clock clock) {
+		return new H2Database(config, clock);
 	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java
similarity index 95%
rename from bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java
rename to bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java
index 6596861cc3fcab926386c5fa8a4a6dc5f6c62021..1192f9dd35b27c26aca5db558a25f0c16799528f 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java
@@ -19,7 +19,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-public class TransactionIsolationTest extends BrambleTestCase {
+public class H2TransactionIsolationTest extends BrambleTestCase {
 
 	private static final String DROP_TABLE = "DROP TABLE foo IF EXISTS";
 	private static final String CREATE_TABLE = "CREATE TABLE foo"
@@ -34,10 +34,10 @@ public class TransactionIsolationTest extends BrambleTestCase {
 
 	private final File testDir = TestUtils.getTestDirectory();
 	private final File db = new File(testDir, "db");
-	private final String withMvcc = "jdbc:h2:" + db.getAbsolutePath()
-			+ ";MV_STORE=TRUE;MVCC=TRUE";
-	private final String withoutMvcc = "jdbc:h2:" + db.getAbsolutePath()
-			+ ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1";
+	private final String withMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+			+ ";MV_STORE=TRUE;MVCC=TRUE;DB_CLOSE_ON_EXIT=false";
+	private final String withoutMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+			+ ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1;DB_CLOSE_ON_EXIT=false";
 
 	@Before
 	public void setUp() throws Exception {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..64f666fc426bae11cd8534b635adc331f03de07d
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java
@@ -0,0 +1,16 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.db.DatabaseConfig;
+import org.briarproject.bramble.api.system.Clock;
+
+public class HyperSqlDatabaseTest extends JdbcDatabaseTest {
+
+	public HyperSqlDatabaseTest() throws Exception {
+		super();
+	}
+
+	@Override
+	protected JdbcDatabase createDatabase(DatabaseConfig config, Clock clock) {
+		return new HyperSqlDatabase(config, clock);
+	}
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2812eda45a40641d392fba55cd601394c3b694fe
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -0,0 +1,1691 @@
+package org.briarproject.bramble.db;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.db.DatabaseConfig;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.settings.Settings;
+import org.briarproject.bramble.api.sync.ClientId;
+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.bramble.api.sync.ValidationManager.State;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.api.transport.IncomingKeys;
+import org.briarproject.bramble.api.transport.OutgoingKeys;
+import org.briarproject.bramble.api.transport.TransportKeys;
+import org.briarproject.bramble.system.SystemClock;
+import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.test.TestDatabaseConfig;
+import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.util.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.sql.Connection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.bramble.api.db.Metadata.REMOVE;
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
+import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
+import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class JdbcDatabaseTest extends BrambleTestCase {
+
+	private static final int ONE_MEGABYTE = 1024 * 1024;
+	private static final int MAX_SIZE = 5 * ONE_MEGABYTE;
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final GroupId groupId;
+	private final ClientId clientId;
+	private final Group group;
+	private final Author author;
+	private final AuthorId localAuthorId;
+	private final LocalAuthor localAuthor;
+	private final MessageId messageId;
+	private final long timestamp;
+	private final int size;
+	private final byte[] raw;
+	private final Message message;
+	private final TransportId transportId;
+	private final ContactId contactId;
+
+	JdbcDatabaseTest() throws Exception {
+		groupId = new GroupId(TestUtils.getRandomId());
+		clientId = new ClientId(StringUtils.getRandomString(5));
+		byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH];
+		group = new Group(groupId, clientId, descriptor);
+		AuthorId authorId = new AuthorId(TestUtils.getRandomId());
+		author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
+		localAuthorId = new AuthorId(TestUtils.getRandomId());
+		timestamp = System.currentTimeMillis();
+		localAuthor = new LocalAuthor(localAuthorId, "Bob",
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
+		messageId = new MessageId(TestUtils.getRandomId());
+		size = 1234;
+		raw = TestUtils.getRandomBytes(size);
+		message = new Message(messageId, groupId, timestamp, raw);
+		transportId = new TransportId("id");
+		contactId = new ContactId(1);
+	}
+
+	protected abstract JdbcDatabase createDatabase(DatabaseConfig config,
+			Clock clock);
+
+	@Before
+	public void setUp() {
+		assertTrue(testDir.mkdirs());
+	}
+
+	@Test
+	public void testPersistence() throws Exception {
+		// Store some records
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+		assertFalse(db.containsContact(txn, contactId));
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		assertTrue(db.containsContact(txn, contactId));
+		assertFalse(db.containsGroup(txn, groupId));
+		db.addGroup(txn, group);
+		assertTrue(db.containsGroup(txn, groupId));
+		assertFalse(db.containsMessage(txn, messageId));
+		db.addMessage(txn, message, DELIVERED, true);
+		assertTrue(db.containsMessage(txn, messageId));
+		db.commitTransaction(txn);
+		db.close();
+
+		// Check that the records are still there
+		db = open(true);
+		txn = db.startTransaction();
+		assertTrue(db.containsContact(txn, contactId));
+		assertTrue(db.containsGroup(txn, groupId));
+		assertTrue(db.containsMessage(txn, messageId));
+		byte[] raw1 = db.getRawMessage(txn, messageId);
+		assertArrayEquals(raw, raw1);
+
+		// Delete the records
+		db.removeMessage(txn, messageId);
+		db.removeContact(txn, contactId);
+		db.removeGroup(txn, groupId);
+		db.commitTransaction(txn);
+		db.close();
+
+		// Check that the records are gone
+		db = open(true);
+		txn = db.startTransaction();
+		assertFalse(db.containsContact(txn, contactId));
+		assertFalse(db.containsGroup(txn, groupId));
+		assertFalse(db.containsMessage(txn, messageId));
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testRemovingGroupRemovesMessage() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// Removing the group should remove the message
+		assertTrue(db.containsMessage(txn, messageId));
+		db.removeGroup(txn, groupId);
+		assertFalse(db.containsMessage(txn, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// The message has no status yet, so it should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Adding a status with seen = false should make the message sendable
+		db.addStatus(txn, contactId, messageId, false, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Changing the status to seen = true should make the message unsendable
+		db.raiseSeenFlag(txn, contactId, messageId);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeDelivered() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared but unvalidated message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, UNKNOWN, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message has not been validated, so it should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Marking the message delivered should make it sendable
+		db.setMessageState(txn, messageId, DELIVERED);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Marking the message invalid should make it unsendable
+		db.setMessageState(txn, messageId, INVALID);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Marking the message pending should make it unsendable
+		db.setMessageState(txn, messageId, PENDING);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustHaveSharedGroup() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, an invisible group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The group is invisible, so the message should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Making the group visible should not make the message sendable
+		db.addGroupVisibility(txn, contactId, groupId, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Sharing the group should make the message sendable
+		db.setGroupVisibility(txn, contactId, groupId, true);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Unsharing the group should make the message unsendable
+		db.setGroupVisibility(txn, contactId, groupId, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Making the group invisible should make the message unsendable
+		db.removeGroupVisibility(txn, contactId, groupId);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeShared() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and an unshared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, false);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message is not shared, so it should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Sharing the message should make it sendable
+		db.setMessageShared(txn, messageId);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustFitCapacity() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message is sendable, but too large to send
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				size - 1);
+		assertTrue(ids.isEmpty());
+
+		// The message is just the right size to send
+		ids = db.getMessagesToSend(txn, contactId, size);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessagesToAck() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact and a visible group
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, false);
+
+		// Add some messages to ack
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId, timestamp, raw);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, true);
+		db.raiseAckFlag(txn, contactId, messageId);
+		db.addMessage(txn, message1, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId1, false, true);
+		db.raiseAckFlag(txn, contactId, messageId1);
+
+		// Both message IDs should be returned
+		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
+		assertEquals(Arrays.asList(messageId, messageId1), ids);
+
+		// Remove both message IDs
+		db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
+
+		// Both message IDs should have been removed
+		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
+				contactId, 1234));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testOutstandingMessageAcked() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// Retrieve the message from the database and mark it as sent
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
+
+		// The message should no longer be sendable
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+
+		// Pretend that the message was acked
+		db.raiseSeenFlag(txn, contactId, messageId);
+
+		// The message still should not be sendable
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetFreeSpace() throws Exception {
+		byte[] largeBody = new byte[MAX_MESSAGE_LENGTH];
+		for (int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i;
+		Message message = new Message(messageId, groupId, timestamp, largeBody);
+		Database<Connection> db = open(false);
+
+		// Sanity check: there should be enough space on disk for this test
+		assertTrue(testDir.getFreeSpace() > MAX_SIZE);
+
+		// The free space should not be more than the allowed maximum size
+		long free = db.getFreeSpace();
+		assertTrue(free <= MAX_SIZE);
+		assertTrue(free > 0);
+
+		// Storing a message should reduce the free space
+		Connection txn = db.startTransaction();
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.commitTransaction(txn);
+		assertTrue(db.getFreeSpace() < free);
+
+		db.close();
+	}
+
+	@Test
+	public void testCloseWaitsForCommit() throws Exception {
+		CountDownLatch closing = new CountDownLatch(1);
+		CountDownLatch closed = new CountDownLatch(1);
+		AtomicBoolean transactionFinished = new AtomicBoolean(false);
+		AtomicBoolean error = new AtomicBoolean(false);
+		Database<Connection> db = open(false);
+
+		// Start a transaction
+		Connection txn = db.startTransaction();
+		// In another thread, close the database
+		Thread close = new Thread(() -> {
+			try {
+				closing.countDown();
+				db.close();
+				if (!transactionFinished.get()) error.set(true);
+				closed.countDown();
+			} catch (Exception e) {
+				error.set(true);
+			}
+		});
+		close.start();
+		closing.await();
+		// Do whatever the transaction needs to do
+		Thread.sleep(10);
+		transactionFinished.set(true);
+		// Commit the transaction
+		db.commitTransaction(txn);
+		// The other thread should now terminate
+		assertTrue(closed.await(5, SECONDS));
+		// Check that the other thread didn't encounter an error
+		assertFalse(error.get());
+	}
+
+	@Test
+	public void testCloseWaitsForAbort() throws Exception {
+		CountDownLatch closing = new CountDownLatch(1);
+		CountDownLatch closed = new CountDownLatch(1);
+		AtomicBoolean transactionFinished = new AtomicBoolean(false);
+		AtomicBoolean error = new AtomicBoolean(false);
+		Database<Connection> db = open(false);
+
+		// Start a transaction
+		Connection txn = db.startTransaction();
+		// In another thread, close the database
+		Thread close = new Thread(() -> {
+			try {
+				closing.countDown();
+				db.close();
+				if (!transactionFinished.get()) error.set(true);
+				closed.countDown();
+			} catch (Exception e) {
+				error.set(true);
+			}
+		});
+		close.start();
+		closing.await();
+		// Do whatever the transaction needs to do
+		Thread.sleep(10);
+		transactionFinished.set(true);
+		// Abort the transaction
+		db.abortTransaction(txn);
+		// The other thread should now terminate
+		assertTrue(closed.await(5, SECONDS));
+		// Check that the other thread didn't encounter an error
+		assertFalse(error.get());
+	}
+
+	@Test
+	public void testUpdateSettings() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Store some settings
+		Settings s = new Settings();
+		s.put("foo", "foo");
+		s.put("bar", "bar");
+		db.mergeSettings(txn, s, "test");
+		assertEquals(s, db.getSettings(txn, "test"));
+
+		// Update one of the settings and add another
+		Settings s1 = new Settings();
+		s1.put("bar", "baz");
+		s1.put("bam", "bam");
+		db.mergeSettings(txn, s1, "test");
+
+		// Check that the settings were merged
+		Settings merged = new Settings();
+		merged.put("foo", "foo");
+		merged.put("bar", "baz");
+		merged.put("bam", "bam");
+		assertEquals(merged, db.getSettings(txn, "test"));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testContainsVisibleMessageRequiresMessageInDatabase()
+			throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact and a shared group
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+
+		// The message is not in the database
+		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testContainsVisibleMessageRequiresGroupInDatabase()
+			throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+
+		// The group is not in the database
+		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testContainsVisibleMessageRequiresVisibileGroup()
+			throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a group and a message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The group is not visible
+		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGroupVisibility() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact and a group
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+
+		// The group should not be visible to the contact
+		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.emptyList(),
+				db.getGroupVisibility(txn, groupId));
+
+		// Make the group visible to the contact
+		db.addGroupVisibility(txn, contactId, groupId, false);
+		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getGroupVisibility(txn, groupId));
+
+		// Share the group with the contact
+		db.setGroupVisibility(txn, contactId, groupId, true);
+		assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getGroupVisibility(txn, groupId));
+
+		// Unshare the group with the contact
+		db.setGroupVisibility(txn, contactId, groupId, false);
+		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getGroupVisibility(txn, groupId));
+
+		// Make the group invisible again
+		db.removeGroupVisibility(txn, contactId, groupId);
+		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.emptyList(),
+				db.getGroupVisibility(txn, groupId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testTransportKeys() throws Exception {
+		TransportKeys keys = createTransportKeys();
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Initially there should be no transport keys in the database
+		assertEquals(Collections.emptyMap(),
+				db.getTransportKeys(txn, transportId));
+
+		// Add the contact, the transport and the transport keys
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addTransport(txn, transportId, 123);
+		db.addTransportKeys(txn, contactId, keys);
+
+		// Retrieve the transport keys
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		assertKeysEquals(keys.getPreviousIncomingKeys(),
+				k.getPreviousIncomingKeys());
+		assertKeysEquals(keys.getCurrentIncomingKeys(),
+				k.getCurrentIncomingKeys());
+		assertKeysEquals(keys.getNextIncomingKeys(),
+				k.getNextIncomingKeys());
+		assertKeysEquals(keys.getCurrentOutgoingKeys(),
+				k.getCurrentOutgoingKeys());
+
+		// Removing the contact should remove the transport keys
+		db.removeContact(txn, contactId);
+		assertEquals(Collections.emptyMap(),
+				db.getTransportKeys(txn, transportId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) {
+		assertArrayEquals(expected.getTagKey().getBytes(),
+				actual.getTagKey().getBytes());
+		assertArrayEquals(expected.getHeaderKey().getBytes(),
+				actual.getHeaderKey().getBytes());
+		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
+		assertEquals(expected.getWindowBase(), actual.getWindowBase());
+		assertArrayEquals(expected.getWindowBitmap(), actual.getWindowBitmap());
+	}
+
+	private void assertKeysEquals(OutgoingKeys expected, OutgoingKeys actual) {
+		assertArrayEquals(expected.getTagKey().getBytes(),
+				actual.getTagKey().getBytes());
+		assertArrayEquals(expected.getHeaderKey().getBytes(),
+				actual.getHeaderKey().getBytes());
+		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
+		assertEquals(expected.getStreamCounter(), actual.getStreamCounter());
+	}
+
+	@Test
+	public void testIncrementStreamCounter() throws Exception {
+		TransportKeys keys = createTransportKeys();
+		long rotationPeriod = keys.getCurrentOutgoingKeys().getRotationPeriod();
+		long streamCounter = keys.getCurrentOutgoingKeys().getStreamCounter();
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add the contact, transport and transport keys
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addTransport(txn, transportId, 123);
+		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
+
+		// Increment the stream counter twice and retrieve the transport keys
+		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
+		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
+		assertEquals(rotationPeriod, outCurr.getRotationPeriod());
+		assertEquals(streamCounter + 2, outCurr.getStreamCounter());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSetReorderingWindow() throws Exception {
+		TransportKeys keys = createTransportKeys();
+		long rotationPeriod = keys.getCurrentIncomingKeys().getRotationPeriod();
+		long base = keys.getCurrentIncomingKeys().getWindowBase();
+		byte[] bitmap = keys.getCurrentIncomingKeys().getWindowBitmap();
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add the contact, transport and transport keys
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addTransport(txn, transportId, 123);
+		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
+
+		// Update the reordering window and retrieve the transport keys
+		new Random().nextBytes(bitmap);
+		db.setReorderingWindow(txn, contactId, transportId, rotationPeriod,
+				base + 1, bitmap);
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		IncomingKeys inCurr = k.getCurrentIncomingKeys();
+		assertEquals(rotationPeriod, inCurr.getRotationPeriod());
+		assertEquals(base + 1, inCurr.getWindowBase());
+		assertArrayEquals(bitmap, inCurr.getWindowBitmap());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetContactsByAuthorId() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a local author - no contacts should be associated
+		db.addLocalAuthor(txn, localAuthor);
+
+		// Add a contact associated with the local author
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+
+		// Ensure contact is returned from database by Author ID
+		Collection<Contact> contacts =
+				db.getContactsByAuthorId(txn, author.getId());
+		assertEquals(1, contacts.size());
+		assertEquals(contactId, contacts.iterator().next().getId());
+
+		// Ensure no contacts are returned after contact was deleted
+		db.removeContact(txn, contactId);
+		contacts = db.getContactsByAuthorId(txn, author.getId());
+		assertEquals(0, contacts.size());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetContactsByLocalAuthorId() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a local author - no contacts should be associated
+		db.addLocalAuthor(txn, localAuthor);
+		Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.emptyList(), contacts);
+
+		// Add a contact associated with the local author
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.singletonList(contactId), contacts);
+
+		// Remove the local author - the contact should be removed
+		db.removeLocalAuthor(txn, localAuthorId);
+		contacts = db.getContacts(txn, localAuthorId);
+		assertEquals(Collections.emptyList(), contacts);
+		assertFalse(db.containsContact(txn, contactId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testOfferedMessages() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact - initially there should be no offered messages
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		assertEquals(0, db.countOfferedMessages(txn, contactId));
+
+		// Add some offered messages and count them
+		List<MessageId> ids = new ArrayList<>();
+		for (int i = 0; i < 10; i++) {
+			MessageId m = new MessageId(TestUtils.getRandomId());
+			db.addOfferedMessage(txn, contactId, m);
+			ids.add(m);
+		}
+		assertEquals(10, db.countOfferedMessages(txn, contactId));
+
+		// Remove some of the offered messages and count again
+		List<MessageId> half = ids.subList(0, 5);
+		db.removeOfferedMessages(txn, contactId, half);
+		assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
+		assertEquals(4, db.countOfferedMessages(txn, contactId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGroupMetadata() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group
+		db.addGroup(txn, group);
+
+		// Attach some metadata to the group
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeGroupMetadata(txn, groupId, metadata);
+
+		// Retrieve the metadata for the group
+		Metadata retrieved = db.getGroupMetadata(txn, groupId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Update the metadata
+		metadata.put("foo", REMOVE);
+		metadata.put("baz", new byte[] {'q', 'u', 'x'});
+		db.mergeGroupMetadata(txn, groupId, metadata);
+
+		// Retrieve the metadata again
+		retrieved = db.getGroupMetadata(txn, groupId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessageMetadata() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// Attach some metadata to the message
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+
+		// Retrieve the metadata for the message
+		Metadata retrieved = db.getMessageMetadata(txn, messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Retrieve the metadata for the group
+		Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId));
+		retrieved = all.get(messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Update the metadata
+		metadata.put("foo", REMOVE);
+		metadata.put("baz", new byte[] {'q', 'u', 'x'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+
+		// Retrieve the metadata again
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Retrieve the metadata for the group again
+		all = db.getMessageMetadata(txn, groupId);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId));
+		retrieved = all.get(messageId);
+		assertEquals(1, retrieved.size());
+		assertFalse(retrieved.containsKey("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Delete the metadata
+		db.deleteMessageMetadata(txn, messageId);
+
+		// Retrieve the metadata again
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+
+		// Retrieve the metadata for the group again
+		all = db.getMessageMetadata(txn, groupId);
+		assertTrue(all.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessageMetadataOnlyForDeliveredMessages() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// Attach some metadata to the message
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+
+		// Retrieve the metadata for the message
+		Metadata retrieved = db.getMessageMetadata(txn, messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+		Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId);
+		assertEquals(1, map.size());
+		assertTrue(map.get(messageId).containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo"));
+		assertTrue(map.get(messageId).containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz"));
+
+		// No metadata for unknown messages
+		db.setMessageState(txn, messageId, UNKNOWN);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// No metadata for invalid messages
+		db.setMessageState(txn, messageId, INVALID);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// No metadata for pending messages
+		db.setMessageState(txn, messageId, PENDING);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// Validator can get metadata for pending messages
+		retrieved = db.getMessageMetadataForValidator(txn, messageId);
+		assertFalse(retrieved.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMetadataQueries() throws Exception {
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and two messages
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message1, DELIVERED, true);
+
+		// Attach some metadata to the messages
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+		Metadata metadata1 = new Metadata();
+		metadata1.put("foo", new byte[]{'q', 'u', 'x'});
+		db.mergeMessageMetadata(txn, messageId1, metadata1);
+
+		// Retrieve all the metadata for the group
+		Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId);
+		assertEquals(2, all.size());
+		assertTrue(all.containsKey(messageId));
+		assertTrue(all.containsKey(messageId1));
+		Metadata retrieved = all.get(messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+		retrieved = all.get(messageId1);
+		assertEquals(1, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
+
+		// Query the metadata with an empty query
+		Metadata query = new Metadata();
+		all = db.getMessageMetadata(txn, groupId, query);
+		assertEquals(2, all.size());
+		assertTrue(all.containsKey(messageId));
+		assertTrue(all.containsKey(messageId1));
+		retrieved = all.get(messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+		retrieved = all.get(messageId1);
+		assertEquals(1, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
+
+		// Use a single-term query that matches the first message
+		query = new Metadata();
+		query.put("foo", metadata.get("foo"));
+		all = db.getMessageMetadata(txn, groupId, query);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId));
+		retrieved = all.get(messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Use a single-term query that matches the second message
+		query = new Metadata();
+		query.put("foo", metadata1.get("foo"));
+		all = db.getMessageMetadata(txn, groupId, query);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId1));
+		retrieved = all.get(messageId1);
+		assertEquals(1, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata1.get("foo"), retrieved.get("foo"));
+
+		// Use a multi-term query that matches the first message
+		query = new Metadata();
+		query.put("foo", metadata.get("foo"));
+		query.put("baz", metadata.get("baz"));
+		all = db.getMessageMetadata(txn, groupId, query);
+		assertEquals(1, all.size());
+		assertTrue(all.containsKey(messageId));
+		retrieved = all.get(messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+
+		// Use a multi-term query that doesn't match any messages
+		query = new Metadata();
+		query.put("foo", metadata1.get("foo"));
+		query.put("baz", metadata.get("baz"));
+		all = db.getMessageMetadata(txn, groupId, query);
+		assertTrue(all.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception {
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and two messages
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message1, DELIVERED, true);
+
+		// Attach some metadata to the messages
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+		Metadata metadata1 = new Metadata();
+		metadata1.put("foo", new byte[]{'b', 'a', 'r'});
+		db.mergeMessageMetadata(txn, messageId1, metadata1);
+
+		for (int i = 0; i < 2; i++) {
+			Metadata query;
+			if (i == 0) {
+				// Query the metadata with an empty query
+				query = new Metadata();
+			} else {
+				// Query for foo
+				query = new Metadata();
+				query.put("foo", new byte[]{'b', 'a', 'r'});
+			}
+
+			db.setMessageState(txn, messageId, DELIVERED);
+			db.setMessageState(txn, messageId1, DELIVERED);
+			Map<MessageId, Metadata> all =
+					db.getMessageMetadata(txn, groupId, query);
+			assertEquals(2, all.size());
+			assertMetadataEquals(metadata, all.get(messageId));
+			assertMetadataEquals(metadata1, all.get(messageId1));
+
+			// No metadata for unknown messages
+			db.setMessageState(txn, messageId, UNKNOWN);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
+
+			// No metadata for invalid messages
+			db.setMessageState(txn, messageId, INVALID);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
+
+			// No metadata for pending messages
+			db.setMessageState(txn, messageId, PENDING);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
+		}
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	private void assertMetadataEquals(Metadata m1, Metadata m2) {
+		assertEquals(m1.keySet(), m2.keySet());
+		for (Entry<String, byte[]> e : m1.entrySet()) {
+			assertArrayEquals(e.getValue(), m2.get(e.getKey()));
+		}
+	}
+
+	@Test
+	public void testMessageDependencies() throws Exception {
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
+		MessageId messageId3 = new MessageId(TestUtils.getRandomId());
+		MessageId messageId4 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId, timestamp, raw);
+		Message message2 = new Message(messageId2, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and some messages
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, PENDING, true);
+		db.addMessage(txn, message1, DELIVERED, true);
+		db.addMessage(txn, message2, INVALID, true);
+
+		// Add dependencies
+		db.addMessageDependency(txn, groupId, messageId, messageId1);
+		db.addMessageDependency(txn, groupId, messageId, messageId2);
+		db.addMessageDependency(txn, groupId, messageId1, messageId3);
+		db.addMessageDependency(txn, groupId, messageId2, messageId4);
+
+		Map<MessageId, State> dependencies;
+
+		// Retrieve dependencies for root
+		dependencies = db.getMessageDependencies(txn, messageId);
+		assertEquals(2, dependencies.size());
+		assertEquals(DELIVERED, dependencies.get(messageId1));
+		assertEquals(INVALID, dependencies.get(messageId2));
+
+		// Retrieve dependencies for message 1
+		dependencies = db.getMessageDependencies(txn, messageId1);
+		assertEquals(1, dependencies.size());
+		assertEquals(UNKNOWN, dependencies.get(messageId3)); // Missing
+
+		// Retrieve dependencies for message 2
+		dependencies = db.getMessageDependencies(txn, messageId2);
+		assertEquals(1, dependencies.size());
+		assertEquals(UNKNOWN, dependencies.get(messageId4)); // Missing
+
+		// Make sure leaves have no dependencies
+		dependencies = db.getMessageDependencies(txn, messageId3);
+		assertEquals(0, dependencies.size());
+		dependencies = db.getMessageDependencies(txn, messageId4);
+		assertEquals(0, dependencies.size());
+
+		Map<MessageId, State> dependents;
+
+		// Root message does not have dependents
+		dependents = db.getMessageDependents(txn, messageId);
+		assertEquals(0, dependents.size());
+
+		// Messages 1 and 2 have the root as a dependent
+		dependents = db.getMessageDependents(txn, messageId1);
+		assertEquals(1, dependents.size());
+		assertEquals(PENDING, dependents.get(messageId));
+		dependents = db.getMessageDependents(txn, messageId2);
+		assertEquals(1, dependents.size());
+		assertEquals(PENDING, dependents.get(messageId));
+
+		// Message 3 has message 1 as a dependent
+		dependents = db.getMessageDependents(txn, messageId3);
+		assertEquals(1, dependents.size());
+		assertEquals(DELIVERED, dependents.get(messageId1));
+
+		// Message 4 has message 2 as a dependent
+		dependents = db.getMessageDependents(txn, messageId4);
+		assertEquals(1, dependents.size());
+		assertEquals(INVALID, dependents.get(messageId2));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessageDependenciesAcrossGroups() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, PENDING, true);
+
+		// Add a second group
+		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
+		Group group1 = new Group(groupId1, clientId,
+				TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
+		db.addGroup(txn, group1);
+
+		// Add a message to the second group
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId1, timestamp, raw);
+		db.addMessage(txn, message1, DELIVERED, true);
+
+		// Create an ID for a missing message
+		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
+
+		// Add another message to the first group
+		MessageId messageId3 = new MessageId(TestUtils.getRandomId());
+		Message message3 = new Message(messageId3, groupId, timestamp, raw);
+		db.addMessage(txn, message3, DELIVERED, true);
+
+		// Add dependencies between the messages
+		db.addMessageDependency(txn, groupId, messageId, messageId1);
+		db.addMessageDependency(txn, groupId, messageId, messageId2);
+		db.addMessageDependency(txn, groupId, messageId, messageId3);
+
+		// Retrieve the dependencies for the root
+		Map<MessageId, State> dependencies;
+		dependencies = db.getMessageDependencies(txn, messageId);
+
+		// The cross-group dependency should have state INVALID
+		assertEquals(INVALID, dependencies.get(messageId1));
+
+		// The missing dependency should have state UNKNOWN
+		assertEquals(UNKNOWN, dependencies.get(messageId2));
+
+		// The valid dependency should have its real state
+		assertEquals(DELIVERED, dependencies.get(messageId3));
+
+		// Retrieve the dependents for the message in the second group
+		Map<MessageId, State> dependents;
+		dependents = db.getMessageDependents(txn, messageId1);
+
+		// The cross-group dependent should have its real state
+		assertEquals(PENDING, dependents.get(messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetPendingMessagesForDelivery() throws Exception {
+		MessageId mId1 = new MessageId(TestUtils.getRandomId());
+		MessageId mId2 = new MessageId(TestUtils.getRandomId());
+		MessageId mId3 = new MessageId(TestUtils.getRandomId());
+		MessageId mId4 = new MessageId(TestUtils.getRandomId());
+		Message m1 = new Message(mId1, groupId, timestamp, raw);
+		Message m2 = new Message(mId2, groupId, timestamp, raw);
+		Message m3 = new Message(mId3, groupId, timestamp, raw);
+		Message m4 = new Message(mId4, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and some messages with different states
+		db.addGroup(txn, group);
+		db.addMessage(txn, m1, UNKNOWN, true);
+		db.addMessage(txn, m2, INVALID, true);
+		db.addMessage(txn, m3, PENDING, true);
+		db.addMessage(txn, m4, DELIVERED, true);
+
+		Collection<MessageId> result;
+
+		// Retrieve messages to be validated
+		result = db.getMessagesToValidate(txn, clientId);
+		assertEquals(1, result.size());
+		assertTrue(result.contains(mId1));
+
+		// Retrieve pending messages
+		result = db.getPendingMessages(txn, clientId);
+		assertEquals(1, result.size());
+		assertTrue(result.contains(mId3));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetMessagesToShare() throws Exception {
+		MessageId mId1 = new MessageId(TestUtils.getRandomId());
+		MessageId mId2 = new MessageId(TestUtils.getRandomId());
+		MessageId mId3 = new MessageId(TestUtils.getRandomId());
+		MessageId mId4 = new MessageId(TestUtils.getRandomId());
+		Message m1 = new Message(mId1, groupId, timestamp, raw);
+		Message m2 = new Message(mId2, groupId, timestamp, raw);
+		Message m3 = new Message(mId3, groupId, timestamp, raw);
+		Message m4 = new Message(mId4, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and some messages
+		db.addGroup(txn, group);
+		db.addMessage(txn, m1, DELIVERED, true);
+		db.addMessage(txn, m2, DELIVERED, false);
+		db.addMessage(txn, m3, DELIVERED, false);
+		db.addMessage(txn, m4, DELIVERED, true);
+
+		// Introduce dependencies between the messages
+		db.addMessageDependency(txn, groupId, mId1, mId2);
+		db.addMessageDependency(txn, groupId, mId3, mId1);
+		db.addMessageDependency(txn, groupId, mId4, mId3);
+
+		// Retrieve messages to be shared
+		Collection<MessageId> result =
+				db.getMessagesToShare(txn, clientId);
+		assertEquals(2, result.size());
+		assertTrue(result.contains(mId2));
+		assertTrue(result.contains(mId3));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetMessageStatus() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message should not be sent or seen
+		MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertFalse(status.isSent());
+		assertFalse(status.isSeen());
+
+		// The same status should be returned when querying by group
+		Collection<MessageStatus> statuses = db.getMessageStatus(txn,
+				contactId, groupId);
+		assertEquals(1, statuses.size());
+		status = statuses.iterator().next();
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertFalse(status.isSent());
+		assertFalse(status.isSeen());
+
+		// Pretend the message was sent to the contact
+		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
+
+		// The message should be sent but not seen
+		status = db.getMessageStatus(txn, contactId, messageId);
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertTrue(status.isSent());
+		assertFalse(status.isSeen());
+
+		// The same status should be returned when querying by group
+		statuses = db.getMessageStatus(txn, contactId, groupId);
+		assertEquals(1, statuses.size());
+		status = statuses.iterator().next();
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertTrue(status.isSent());
+		assertFalse(status.isSeen());
+
+		// Pretend the message was acked by the contact
+		db.raiseSeenFlag(txn, contactId, messageId);
+
+		// The message should be sent and seen
+		status = db.getMessageStatus(txn, contactId, messageId);
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertTrue(status.isSent());
+		assertTrue(status.isSeen());
+
+		// The same status should be returned when querying by group
+		statuses = db.getMessageStatus(txn, contactId, groupId);
+		assertEquals(1, statuses.size());
+		status = statuses.iterator().next();
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertTrue(status.isSent());
+		assertTrue(status.isSeen());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testDifferentLocalAuthorsCanHaveTheSameContact()
+			throws Exception {
+		AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
+		LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add two local authors
+		db.addLocalAuthor(txn, localAuthor);
+		db.addLocalAuthor(txn, localAuthor1);
+
+		// Add the same contact for each local author
+		ContactId contactId =
+				db.addContact(txn, author, localAuthorId, true, true);
+		ContactId contactId1 =
+				db.addContact(txn, author, localAuthorId1, true, true);
+
+		// The contacts should be distinct
+		assertNotEquals(contactId, contactId1);
+		assertEquals(2, db.getContacts(txn).size());
+		assertEquals(1, db.getContacts(txn, localAuthorId).size());
+		assertEquals(1, db.getContacts(txn, localAuthorId1).size());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testDeleteMessage() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, a shared group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addGroupVisibility(txn, contactId, groupId, true);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message should be visible to the contact
+		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
+
+		// The message should be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// The raw message should not be null
+		assertNotNull(db.getRawMessage(txn, messageId));
+
+		// Delete the message
+		db.deleteMessage(txn, messageId);
+
+		// The message should be visible to the contact
+		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
+
+		// The message should not be sendable
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// The raw message should be null
+		assertNull(db.getRawMessage(txn, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSetContactActive() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+
+		// The contact should be active
+		Contact contact = db.getContact(txn, contactId);
+		assertTrue(contact.isActive());
+
+		// Set the contact inactive
+		db.setContactActive(txn, contactId, false);
+
+		// The contact should be inactive
+		contact = db.getContact(txn, contactId);
+		assertFalse(contact.isActive());
+
+		// Set the contact active
+		db.setContactActive(txn, contactId, true);
+
+		// The contact should be active
+		contact = db.getContact(txn, contactId);
+		assertTrue(contact.isActive());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSetMessageState() throws Exception {
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, UNKNOWN, false);
+
+		// Walk the message through the validation and delivery states
+		assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
+		db.setMessageState(txn, messageId, INVALID);
+		assertEquals(INVALID, db.getMessageState(txn, messageId));
+		db.setMessageState(txn, messageId, PENDING);
+		assertEquals(PENDING, db.getMessageState(txn, messageId));
+		db.setMessageState(txn, messageId, DELIVERED);
+		assertEquals(DELIVERED, db.getMessageState(txn, messageId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testExceptionHandling() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+		try {
+			// Ask for a nonexistent message - an exception should be thrown
+			db.getRawMessage(txn, messageId);
+			fail();
+		} catch (DbException expected) {
+			// It should be possible to abort the transaction without error
+			db.abortTransaction(txn);
+		}
+		// It should be possible to close the database cleanly
+		db.close();
+	}
+
+	private Database<Connection> open(boolean resume) throws Exception {
+		Database<Connection> db = createDatabase(
+				new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock());
+		if (!resume) TestUtils.deleteTestDirectory(testDir);
+		db.open();
+		return db;
+	}
+
+	private TransportKeys createTransportKeys() {
+		SecretKey inPrevTagKey = TestUtils.getSecretKey();
+		SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
+		IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
+				1, 123, new byte[4]);
+		SecretKey inCurrTagKey = TestUtils.getSecretKey();
+		SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
+		IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
+				2, 234, new byte[4]);
+		SecretKey inNextTagKey = TestUtils.getSecretKey();
+		SecretKey inNextHeaderKey = TestUtils.getSecretKey();
+		IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
+				3, 345, new byte[4]);
+		SecretKey outCurrTagKey = TestUtils.getSecretKey();
+		SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
+		OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
+				2, 456);
+		return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
+	}
+
+	@After
+	public void tearDown() {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+}