Compare commits

...

5 Commits

Author SHA1 Message Date
akwizgran
5f36ba9014 Fix tests. 2018-12-06 18:05:03 +00:00
akwizgran
438d200afe Set paths on blocks after hashing. 2018-12-06 17:43:46 +00:00
akwizgran
bd9ebe75a0 Add initial stream hashing implementation. 2018-12-06 17:43:46 +00:00
akwizgran
4b04e6a21d Add basic hash tree API and implementation. 2018-12-06 17:43:46 +00:00
akwizgran
f915eb4d36 Add BlockSink interface, using temporary message ID. 2018-12-06 17:43:41 +00:00
17 changed files with 417 additions and 3 deletions

View File

@@ -0,0 +1,20 @@
package org.briarproject.bramble.api.io;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import java.util.List;
public interface BlockSink {
/**
* Stores a block of the message with the given temporary ID.
*/
void putBlock(HashingId h, int blockNumber, byte[] data) throws DbException;
/**
* Sets the hash tree path of a previously stored block.
*/
void setPath(HashingId h, int blockNumber, List<TreeHash> path)
throws DbException;
}

View File

@@ -0,0 +1,27 @@
package org.briarproject.bramble.api.io;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import javax.annotation.concurrent.ThreadSafe;
/**
* Type-safe wrapper for a byte array that uniquely identifies a
* {@link Message} while it's being hashed and the {@link MessageId} is not
* yet known.
*/
@ThreadSafe
@NotNullByDefault
public class HashingId extends UniqueId {
public HashingId(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof HashingId && super.equals(o);
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.api.sync; package org.briarproject.bramble.api.sync;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.TreeHash;
@NotNullByDefault @NotNullByDefault
public interface MessageFactory { public interface MessageFactory {
@@ -10,4 +11,6 @@ public interface MessageFactory {
Message createMessage(byte[] raw); Message createMessage(byte[] raw);
byte[] getRawMessage(Message m); byte[] getRawMessage(Message m);
MessageId getMessageId(GroupId g, long timestamp, TreeHash rootHash);
} }

View File

@@ -24,6 +24,12 @@ public class MessageId extends UniqueId {
public static final String BLOCK_LABEL = public static final String BLOCK_LABEL =
"org.briarproject.bramble/MESSAGE_BLOCK"; "org.briarproject.bramble/MESSAGE_BLOCK";
/**
* Label for hashing two tree hashes to produce a parent.
*/
public static final String TREE_LABEL =
"org.briarproject.bramble/MESSAGE_TREE";
public MessageId(byte[] id) { public MessageId(byte[] id) {
super(id); super(id);
} }

View File

@@ -35,4 +35,9 @@ public interface SyncConstants {
* The maximum number of message IDs in an ack, offer or request record. * The maximum number of message IDs in an ack, offer or request record.
*/ */
int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH; int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH;
/**
* The maximum length of a message block in bytes.
*/
int MAX_BLOCK_LENGTH = 32 * 2014; // 32 KiB
} }

View File

@@ -0,0 +1,21 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class LeafNode extends TreeNode {
public LeafNode(TreeHash hash, int blockNumber) {
super(hash, 0, blockNumber, blockNumber);
}
@Override
public TreeNode getLeftChild() {
throw new UnsupportedOperationException();
}
@Override
public TreeNode getRightChild() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,26 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class ParentNode extends TreeNode {
private final TreeNode left, right;
public ParentNode(TreeHash hash, TreeNode left, TreeNode right) {
super(hash, Math.max(left.getHeight(), right.getHeight()) + 1,
left.getFirstBlockNumber(), right.getLastBlockNumber());
this.left = left;
this.right = right;
}
@Override
public TreeNode getLeftChild() {
return left;
}
@Override
public TreeNode getRightChild() {
return right;
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.io.BlockSink;
import org.briarproject.bramble.api.io.HashingId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.io.InputStream;
@NotNullByDefault
public interface StreamHasher {
/**
* Reads the given input stream, divides the data into blocks, stores
* the blocks and the resulting hash tree using the given block sink and
* temporary ID, and returns the hash tree.
*/
TreeNode hash(InputStream in, BlockSink sink, HashingId h)
throws IOException, DbException;
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.ThreadSafe;
/**
* Type-safe wrapper for a byte array that uniquely identifies a sequence of
* one or more message blocks.
*/
@ThreadSafe
@NotNullByDefault
public class TreeHash extends UniqueId {
public TreeHash(byte[] id) {
super(id);
}
@Override
public boolean equals(Object o) {
return o instanceof TreeHash && super.equals(o);
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public interface TreeHasher {
LeafNode hashBlock(int blockNumber, byte[] data);
ParentNode mergeTrees(TreeNode left, TreeNode right);
}

View File

@@ -0,0 +1,38 @@
package org.briarproject.bramble.api.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public abstract class TreeNode {
private final TreeHash hash;
private final int height, firstBlockNumber, lastBlockNumber;
TreeNode(TreeHash hash, int height, int firstBlockNumber,
int lastBlockNumber) {
this.hash = hash;
this.height = height;
this.firstBlockNumber = firstBlockNumber;
this.lastBlockNumber = lastBlockNumber;
}
public TreeHash getHash() {
return hash;
}
public int getHeight() {
return height;
}
public int getFirstBlockNumber() {
return firstBlockNumber;
}
public int getLastBlockNumber() {
return lastBlockNumber;
}
public abstract TreeNode getLeftChild();
public abstract TreeNode getRightChild();
}

View File

@@ -7,6 +7,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory; import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.util.ByteUtils; import org.briarproject.bramble.util.ByteUtils;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -39,13 +40,19 @@ class MessageFactoryImpl implements MessageFactory {
if (body.length == 0) throw new IllegalArgumentException(); if (body.length == 0) throw new IllegalArgumentException();
if (body.length > MAX_MESSAGE_BODY_LENGTH) if (body.length > MAX_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException(); throw new IllegalArgumentException();
MessageId id = getMessageId(g, timestamp, body); MessageId id = getMessageIdFromBody(g, timestamp, body);
return new Message(id, g, timestamp, body); return new Message(id, g, timestamp, body);
} }
private MessageId getMessageId(GroupId g, long timestamp, byte[] body) { private MessageId getMessageIdFromBody(GroupId g, long timestamp,
byte[] body) {
// There's only one block, so the root hash is the hash of the block // There's only one block, so the root hash is the hash of the block
byte[] rootHash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, body); byte[] rootHash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, body);
return getMessageIdFromRootHash(g, timestamp, rootHash);
}
private MessageId getMessageIdFromRootHash(GroupId g, long timestamp,
byte[] rootHash) {
byte[] timeBytes = new byte[INT_64_BYTES]; byte[] timeBytes = new byte[INT_64_BYTES];
ByteUtils.writeUint64(timestamp, timeBytes, 0); ByteUtils.writeUint64(timestamp, timeBytes, 0);
byte[] idHash = crypto.hash(ID_LABEL, FORMAT_VERSION_BYTES, byte[] idHash = crypto.hash(ID_LABEL, FORMAT_VERSION_BYTES,
@@ -65,7 +72,7 @@ class MessageFactoryImpl implements MessageFactory {
long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH); long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH);
byte[] body = new byte[raw.length - MESSAGE_HEADER_LENGTH]; byte[] body = new byte[raw.length - MESSAGE_HEADER_LENGTH];
System.arraycopy(raw, MESSAGE_HEADER_LENGTH, body, 0, body.length); System.arraycopy(raw, MESSAGE_HEADER_LENGTH, body, 0, body.length);
MessageId id = getMessageId(g, timestamp, body); MessageId id = getMessageIdFromBody(g, timestamp, body);
return new Message(id, g, timestamp, body); return new Message(id, g, timestamp, body);
} }
@@ -78,4 +85,10 @@ class MessageFactoryImpl implements MessageFactory {
System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length); System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
return raw; return raw;
} }
@Override
public MessageId getMessageId(GroupId g, long timestamp,
TreeHash rootHash) {
return getMessageIdFromRootHash(g, timestamp, rootHash.getBytes());
}
} }

View File

@@ -0,0 +1,16 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
interface HashTree {
void addLeaf(LeafNode leaf);
TreeNode getRoot();
}

View File

@@ -0,0 +1,46 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import java.util.Deque;
import java.util.LinkedList;
import javax.inject.Inject;
@NotNullByDefault
class HashTreeImpl implements HashTree {
private final TreeHasher treeHasher;
private final Deque<TreeNode> nodes = new LinkedList<>();
@Inject
HashTreeImpl(TreeHasher treeHasher) {
this.treeHasher = treeHasher;
}
@Override
public void addLeaf(LeafNode leaf) {
TreeNode add = leaf;
int height = leaf.getHeight();
TreeNode last = nodes.peekLast();
while (last != null && last.getHeight() == height) {
add = treeHasher.mergeTrees(last, add);
height = add.getHeight();
nodes.removeLast();
last = nodes.peekLast();
}
nodes.addLast(add);
}
@Override
public TreeNode getRoot() {
TreeNode root = nodes.removeLast();
while (!nodes.isEmpty()) {
root = treeHasher.mergeTrees(nodes.removeLast(), root);
}
return root;
}
}

View File

@@ -0,0 +1,85 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.io.BlockSink;
import org.briarproject.bramble.api.io.HashingId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.StreamHasher;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Arrays.copyOfRange;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_BLOCK_LENGTH;
@Immutable
@NotNullByDefault
class StreamHasherImpl implements StreamHasher {
private final TreeHasher treeHasher;
private final Provider<HashTree> hashTreeProvider;
@Inject
StreamHasherImpl(TreeHasher treeHasher,
Provider<HashTree> hashTreeProvider) {
this.treeHasher = treeHasher;
this.hashTreeProvider = hashTreeProvider;
}
@Override
public TreeNode hash(InputStream in, BlockSink sink, HashingId h)
throws IOException, DbException {
HashTree tree = hashTreeProvider.get();
byte[] block = new byte[MAX_BLOCK_LENGTH];
int read;
for (int blockNumber = 0; (read = read(in, block)) > 0; blockNumber++) {
byte[] data;
if (read == block.length) data = block;
else data = copyOfRange(block, 0, read);
sink.putBlock(h, blockNumber, data);
tree.addLeaf(treeHasher.hashBlock(blockNumber, data));
}
TreeNode root = tree.getRoot();
setPaths(sink, h, root, new LinkedList<>());
return root;
}
/**
* Reads a block from the given input stream and returns the number of
* bytes read, or 0 if no bytes were read before reaching the end of the
* stream.
*/
private int read(InputStream in, byte[] block) throws IOException {
int offset = 0;
while (offset < block.length) {
int read = in.read(block, offset, block.length - offset);
if (read == -1) return offset;
offset += read;
}
return offset;
}
private void setPaths(BlockSink sink, HashingId h, TreeNode node,
LinkedList<TreeHash> path) throws DbException {
if (node.getHeight() == 0) {
// We've reached a leaf - store the path
sink.setPath(h, node.getFirstBlockNumber(), path);
} else {
// Add the right child's hash to the path and traverse the left
path.addFirst(node.getRightChild().getHash());
setPaths(sink, h, node.getLeftChild(), path);
// Add the left child's hash to the path and traverse the right
path.removeFirst();
path.addFirst(node.getLeftChild().getHash());
setPaths(sink, h, node.getRightChild(), path);
}
}
}

View File

@@ -0,0 +1,44 @@
package org.briarproject.bramble.sync.tree;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.tree.LeafNode;
import org.briarproject.bramble.api.sync.tree.ParentNode;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import org.briarproject.bramble.api.sync.tree.TreeHasher;
import org.briarproject.bramble.api.sync.tree.TreeNode;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static org.briarproject.bramble.api.sync.Message.FORMAT_VERSION;
import static org.briarproject.bramble.api.sync.MessageId.BLOCK_LABEL;
import static org.briarproject.bramble.api.sync.MessageId.TREE_LABEL;
@Immutable
@NotNullByDefault
class TreeHasherImpl implements TreeHasher {
private static final byte[] FORMAT_VERSION_BYTES =
new byte[] {FORMAT_VERSION};
private final CryptoComponent crypto;
@Inject
TreeHasherImpl(CryptoComponent crypto) {
this.crypto = crypto;
}
@Override
public LeafNode hashBlock(int blockNumber, byte[] data) {
byte[] hash = crypto.hash(BLOCK_LABEL, FORMAT_VERSION_BYTES, data);
return new LeafNode(new TreeHash(hash), blockNumber);
}
@Override
public ParentNode mergeTrees(TreeNode left, TreeNode right) {
byte[] hash = crypto.hash(TREE_LABEL, FORMAT_VERSION_BYTES,
left.getHash().getBytes(), right.getHash().getBytes());
return new ParentNode(new TreeHash(hash), left, right);
}
}

View File

@@ -4,6 +4,8 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory; import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.tree.TreeHash;
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@@ -27,4 +29,10 @@ public class TestMessageFactory implements MessageFactory {
System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length); System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length);
return raw; return raw;
} }
@Override
public MessageId getMessageId(GroupId g, long timestamp,
TreeHash rootHash) {
throw new UnsupportedOperationException();
}
} }