[freenet-cvs] r19733 - branches/saltedhashstore/freenet/src/…

Top Page
Delete this message
Reply to this message
Author: devl
Date:  
To: cvs
Subject: [freenet-cvs] r19733 - branches/saltedhashstore/freenet/src/freenet/store
Author: j16sdiz
Date: 2008-05-04 13:11:55 +0000 (Sun, 04 May 2008)
New Revision: 19733

Added:
branches/saltedhashstore/freenet/src/freenet/store/SaltedHashFreenetStore.java
Log:
SaltedHashFreenetStore basic function (without locking/resizing)


Added: branches/saltedhashstore/freenet/src/freenet/store/SaltedHashFreenetStore.java
===================================================================
--- branches/saltedhashstore/freenet/src/freenet/store/SaltedHashFreenetStore.java     (rev 0)
+++ branches/saltedhashstore/freenet/src/freenet/store/SaltedHashFreenetStore.java    2008-05-04 13:11:55 UTC (rev 19733)
@@ -0,0 +1,694 @@
+/* This code is part of Freenet. It is distributed under the GNU General
+ * Public License, version 2 (or at your option any later version). See
+ * http://www.gnu.org/ for further details of the GPL. */
+package freenet.store;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.Random;
+
+import freenet.crypt.Digest;
+import freenet.crypt.SHA1;
+import freenet.keys.KeyVerifyException;
+import freenet.support.HexUtil;
+import freenet.support.Logger;
+
+/**
+ * Index-less data store based on salted hash
+ *
+ * @author sdiz
+ */
+public class SaltedHashFreenetStore implements FreenetStore {
+    private static boolean logMINOR;
+    private static boolean logDEBUG;
+
+    private final File baseDir;
+    private final StoreCallback callback;
+    private final boolean collisionPossible;
+    private final int headerBlockLength;
+    private final int routeKeyLength;
+    private final int fullKeyLength;
+    private final int dataBlockLength;
+    private final Random random;
+    private long storeSize;
+
+    public static SaltedHashFreenetStore construct(File baseDir, String name, StoreCallback callback, Random random,
+     long maxKeys) throws IOException {
+        return new SaltedHashFreenetStore(baseDir, name, callback, random, maxKeys);
+    }
+
+    SaltedHashFreenetStore(File baseDir, String name, StoreCallback callback, Random random, long maxKeys)
+     throws IOException {
+        logMINOR = Logger.shouldLog(Logger.MINOR, this);
+        logDEBUG = Logger.shouldLog(Logger.DEBUG, this);
+
+        this.baseDir = baseDir;
+
+        this.callback = callback;
+        collisionPossible = callback.collisionPossible();
+        routeKeyLength = callback.routingKeyLength();
+        headerBlockLength = callback.headerLength();
+        fullKeyLength = callback.fullKeyLength();
+        dataBlockLength = callback.dataLength();
+
+        this.random = random;
+        storeSize = maxKeys;
+
+        long length = ENTRY_HEADER_LENGTH + headerBlockLength + dataBlockLength;
+        entryPaddingLength = 0x200L - (length % 0x200L);
+        entryTotalLength = length + entryPaddingLength;
+
+        // Create a directory it not exist
+        this.baseDir.mkdirs();
+
+        configFile = new File(this.baseDir, name + ".config");
+        loadConfigFile();
+
+        openStoreFiles(baseDir, name);
+
+        callback.setStore(this);
+    }
+
+    public StorableBlock fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote) throws IOException {
+        if (logMINOR)
+            Logger.minor(this, "Fetch " + HexUtil.bytesToHex(routingKey) + " for " + callback);
+
+        Entry entry = probeEntry(routingKey);
+
+        if (entry == null) {
+            incMisses();
+            return null;
+        }
+
+        try {
+            StorableBlock block = entry.getStorableBlock(routingKey, fullKey);
+            incHits();
+            return block;
+        } catch (KeyVerifyException e) {
+            Logger.minor(this, "key verification exception", e);
+            incMisses();
+            return null;
+        }
+    }
+
+    private Entry probeEntry(byte[] routingKey) throws IOException {
+        // TODO probe store resize
+        return probeEntry0(routingKey, storeSize);
+    }
+
+    private Entry probeEntry0(byte[] routingKey, long probeStoreSize) throws IOException {
+        Entry entry;
+        long offset = getOffsetFromPlainKey(routingKey, probeStoreSize);
+
+        lockEntry(offset);
+        try {
+            entry = readEntry(offset, routingKey);
+        } catch (EOFException e) {
+            // may occur on resize, silent it a bit
+            //TODO store resize
+            Logger.error(this, "EOFException on probeEntry", e);
+            return null;
+        } finally {
+            unlockEntry(offset);
+        }
+        return entry;
+    }
+
+    public void put(StorableBlock block, byte[] routingKey, byte[] fullKey, byte[] data, byte[] header,
+     boolean overwrite) throws IOException, KeyCollisionException {
+        if (logMINOR)
+            Logger.minor(this, "Putting " + HexUtil.bytesToHex(routingKey) + " for " + callback);
+
+        StorableBlock oldBlock = fetch(routingKey, fullKey, false);
+
+        if (oldBlock != null) {
+            if (!collisionPossible)
+                return;
+            if (block.equals(oldBlock)) {
+                return; // already in store
+            } else {
+                if (!overwrite)
+                    throw new KeyCollisionException();
+            }
+        }
+
+        Entry entry = new Entry(routingKey, header, data);
+        lockEntry(entry.getOffset());
+        try {
+            writeEntry(entry);
+            incWrites();
+        } finally {
+            unlockEntry(entry.getOffset());
+        }
+    }
+
+    // ------------- Entry I/O
+
+    // split the files for better concurrency
+    // you may even some if you have lots of mount points =)
+    private final static int FILE_SPLIT = 0x20;
+    private File[] storeFiles;
+    private RandomAccessFile[] storeRAF;
+    private FileChannel[] storeFC;
+
+    /** Flag for occupied space */
+    private final long ENTRY_FLAG_OCCUPIED = 0x00000001L;
+
+    private static final long ENTRY_HEADER_LENGTH = 0x70L;
+    private final long entryPaddingLength;
+    private final long entryTotalLength;
+
+    /**
+     * Data entry
+     *
+     * <pre>
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|
+     * +----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |0000| |
+     * +----+ Digested Routing Key |
+     * |0010| |
+     * +----+-------------------------------+
+     * |0020| Data Encrypt Key |
+     * +----+---------------+---------------+
+     * |0030| Flag | Store Size |
+     * +----+---------------+---------------+
+     * |0040| Reserved |
+     * |0050| Reserved |
+     * |0060| Reserved |
+     * +----+-------------------------------+
+     * |0070| Encrypted Header |
+     * | . + - - - - - - - - - - - - - - - +
+     * | . | Encrypted Data |
+     * +----+-------------------------------+
+     * | | Padding |
+     * +----+-------------------------------+
+     * </pre>
+     *
+     * Total length is padded to multiple of 512bytes. All reserved bytes should be zero when
+     * written, ignored on read.
+     */
+    private class Entry {
+        private byte[] routingKey;
+        private byte[] dataEncryptKey;
+        private long flag;
+        private long storeSize;
+        private byte[] header;
+        private byte[] data;
+
+        private boolean isEncrypted;
+
+        /**
+         * Create a new entry
+         *
+         * @param routingKey
+         * @param header
+         * @param data
+         */
+        public Entry(byte[] routingKey, byte[] header, byte[] data) {
+            this.routingKey = routingKey;
+            flag = ENTRY_FLAG_OCCUPIED;
+            storeSize = SaltedHashFreenetStore.this.storeSize;
+
+            // header/data will be overwritten in flip(),
+            // let's make a copy here
+            this.header = new byte[headerBlockLength];
+            System.arraycopy(header, 0, this.header, 0, headerBlockLength);
+            this.data = new byte[dataBlockLength];
+            System.arraycopy(data, 0, this.data, 0, dataBlockLength);
+
+            isEncrypted = false;
+        }
+
+        public Entry(ByteBuffer in) {
+            assert in.remaining() == entryTotalLength;
+
+            routingKey = new byte[0x20];
+            in.get(routingKey);
+
+            dataEncryptKey = new byte[0x10];
+            in.get(dataEncryptKey);
+
+            flag = in.getLong();
+            storeSize = in.getLong();
+
+            // reserved bytes
+            in.position((int) ENTRY_HEADER_LENGTH);
+
+            header = new byte[headerBlockLength];
+            in.get(header);
+
+            data = new byte[dataBlockLength];
+            in.get(data);
+
+            assert in.remaining() == entryPaddingLength;
+
+            isEncrypted = true;
+        }
+
+        public ByteBuffer toByteBuffer() {
+            ByteBuffer out = ByteBuffer.allocate((int) entryTotalLength);
+            encrypt();
+            out.put(routingKey);
+            out.put(dataEncryptKey);
+
+            out.putLong(flag);
+            out.putLong(storeSize);
+
+            // reserved bytes
+            out.position((int) ENTRY_HEADER_LENGTH);
+
+            out.put(header);
+            out.put(data);
+
+            assert out.remaining() == entryPaddingLength;
+            out.position(0);
+
+            return out;
+        }
+
+        public StorableBlock getStorableBlock(byte[] routingKey, byte[] fullKey) throws KeyVerifyException {
+            if (!decrypt(routingKey))
+                return null;
+
+            StorableBlock block = callback.construct(data, header, routingKey, fullKey);
+            byte[] blockRoutingKey = block.getRoutingKey();
+
+            if (!Arrays.equals(blockRoutingKey, routingKey)) {
+                // either the data is corrupted or we have found a SHA-1 collision
+                // can't recover, as decrypt() depends on a correct route key
+                return null;
+            }
+
+            return block;
+        }
+
+        public long getOffset() {
+            BigInteger bi = new BigInteger(isEncrypted ? routingKey : getDigestedRoutingKey(routingKey));
+            return bi.mod(BigInteger.valueOf(storeSize)).longValue();
+        }
+
+        /**
+         * Verify and decrypt this entry
+         *
+         * @param routingKey
+         * @return <code>true</code> if the <code>routeKey</code> match and the entry is
+         * decrypted.
+         */
+        private boolean decrypt(byte[] routingKey) {
+            if (!isEncrypted) {
+                // Already decrypted
+                if (Arrays.equals(this.routingKey, routingKey))
+                    return true;
+                else
+                    return false;
+            }
+
+            // Does the digested routing key match?
+            if (!Arrays.equals(this.routingKey, getDigestedRoutingKey(routingKey)))
+                return false;
+
+            flip(routingKey);
+
+            this.routingKey = routingKey;
+            isEncrypted = false;
+
+            return true;
+        }
+
+        /**
+         * Encrypt this entry
+         */
+        private void encrypt() {
+            if (isEncrypted)
+                return;
+
+            dataEncryptKey = new byte[16];
+            random.nextBytes(dataEncryptKey);
+
+            flip(routingKey);
+
+            routingKey = getDigestedRoutingKey(routingKey);
+            isEncrypted = true;
+        }
+
+        /**
+         * Encrypt / Decrypt header and data
+         *
+         * @param routingKey
+         */
+        private void flip(byte[] routingKey) {
+            Digest digest = SHA1.getInstance();
+
+            int pos = 0;
+            for (byte i = 0; true; i++) {
+                digest.update(dataEncryptKey);
+                digest.update(routingKey);
+                digest.update(i);
+                byte[] otp = digest.digest();
+
+                for (int j = 0; j < otp.length && pos < header.length; j++, pos++)
+                    header[pos] ^= otp[j];
+
+                if (pos == header.length)
+                    break;
+            }
+
+            pos = 0;
+            for (byte i = 0; true; i++) {
+                digest.update(i); // reverse the order for data
+                digest.update(routingKey);
+                digest.update(dataEncryptKey);
+                byte[] otp = digest.digest();
+
+                for (int j = 0; j < otp.length && pos < data.length; j++, pos++)
+                    data[pos] ^= otp[j];
+
+                if (pos == data.length)
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Open all store files
+     *
+     * @param baseDir
+     * @param name
+     * @throws IOException
+     */
+    private void openStoreFiles(File baseDir, String name) throws IOException {
+        storeFiles = new File[FILE_SPLIT];
+        storeRAF = new RandomAccessFile[FILE_SPLIT];
+        storeFC = new FileChannel[FILE_SPLIT];
+
+        DecimalFormat fmt = new DecimalFormat("000");
+        for (int i = 0; i < FILE_SPLIT; i++) {
+            storeFiles[i] = new File(baseDir, name + ".data-" + fmt.format(i));
+
+            storeRAF[i] = new RandomAccessFile(storeFiles[i], "rw");
+            //TODO store resize
+            if (storeRAF[i].length() == 0) { // New file?
+                storeRAF[i].setLength(entryTotalLength * (storeSize / FILE_SPLIT + 1));
+            }
+            storeFC[i] = storeRAF[i].getChannel();
+        }
+    }
+
+    /**
+     * Flush all store files to disk
+     */
+    private void flushStoreFiles() {
+        for (int i = 0; i < FILE_SPLIT; i++) {
+            try {
+                storeFC[i].force(true);
+            } catch (Exception e) {
+                // TODO log this
+            }
+        }
+    }
+
+    /**
+     * Read entry from disk.
+     *
+     * Before calling this function, you should acquire all required locks.
+     */
+    private Entry readEntry(long offset, byte[] routingKey) throws IOException {
+        int split = (int) (offset % FILE_SPLIT);
+        long rawOffset = (offset / FILE_SPLIT) * entryTotalLength;
+
+        ByteBuffer bf = ByteBuffer.allocate((int) entryTotalLength);
+        do {
+            int status = storeFC[split].read(bf, rawOffset + bf.position());
+            if (status == -1)
+                throw new EOFException();
+        } while (bf.hasRemaining());
+        bf.flip();
+
+        Entry entry = new Entry(bf);
+
+        if (routingKey != null) {
+            boolean decrypted = entry.decrypt(routingKey);
+            if (!decrypted)
+                return null;
+        }
+
+        return entry;
+    }
+
+    /**
+     * Write entry to disk.
+     *
+     * Before calling this function, you should:
+     * <ul>
+     * <li>acquire all required locks</li>
+     * <li>update the entry with latest store size</li>
+     * </ul>
+     */
+    private void writeEntry(Entry entry) throws IOException {
+        entry.encrypt();
+
+        long offset = entry.getOffset();
+        int split = (int) (offset % FILE_SPLIT);
+        long rawOffset = (offset / FILE_SPLIT) * entryTotalLength;
+
+        if (logMINOR && !isFree(offset)) {
+            Logger.minor(this, "collision, overwritting an entry");
+        }
+
+        ByteBuffer bf = entry.toByteBuffer();
+        do {
+            int status = storeFC[split].write(bf, rawOffset + bf.position());
+            if (status == -1)
+                throw new EOFException();
+        } while (bf.hasRemaining());
+    }
+
+    /**
+     * Free an entry by zeroing the header
+     *
+     * @param offset
+     * @throws IOException
+     */
+    private void freeOffset(long offset) throws IOException {
+        int split = (int) (offset % FILE_SPLIT);
+        long rawOffset = (offset / FILE_SPLIT) * entryTotalLength;
+
+        ByteBuffer bf = ByteBuffer.allocate((int) ENTRY_HEADER_LENGTH);
+        do {
+            int status = storeFC[split].write(bf, rawOffset + bf.position());
+            if (status == -1)
+                throw new EOFException();
+        } while (bf.hasRemaining());
+    }
+
+    /**
+     * Check if a block is free
+     *
+     * @param offset
+     * @throws IOException
+     */
+    private boolean isFree(long offset) throws IOException {
+        int split = (int) (offset % FILE_SPLIT);
+        long rawOffset = (offset / FILE_SPLIT) * entryTotalLength;
+
+        ByteBuffer bf = ByteBuffer.allocate((int) ENTRY_HEADER_LENGTH);
+
+        do {
+            int status = storeFC[split].write(bf, rawOffset + bf.position());
+            if (status == -1)
+                throw new EOFException();
+        } while (bf.hasRemaining());
+
+        return (bf.getLong(0x30) & ENTRY_FLAG_OCCUPIED) == 0;
+    }
+
+    // ------------- Configuration
+    /**
+     * Configuration File
+     *
+     * <pre>
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|
+     * +----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |0000| Salt |
+     * +----+---------------+---------------+
+     * |0010| Store Size | prevStoreSize1|
+     * +----+---------------+---------------+
+     * |0010| prevStoreSize2| prevStoreSize3|
+     * +----+---------------+---------------+
+     * </pre>
+     */
+    private final File configFile;
+
+    /**
+     * Load config file
+     */
+    private void loadConfigFile() throws IOException {
+        assert salt == null; // never load the configuration twice
+
+        if (!configFile.exists()) {
+            // create new
+            salt = new byte[0x10];
+            random.nextBytes(salt);
+
+            writeConfigFile();
+        } else {
+            // try to load
+            RandomAccessFile raf = new RandomAccessFile(configFile, "r");
+            salt = new byte[0x10];
+            raf.read(salt);
+
+            // TODO store resize
+            storeSize = raf.readLong();
+            raf.readLong();
+            raf.readLong();
+            raf.readLong();
+
+            raf.close();
+        }
+
+    }
+
+    /**
+     * Write config file
+     */
+    private void writeConfigFile() throws IOException {
+        RandomAccessFile raf = new RandomAccessFile(configFile, "rw");
+        raf.seek(0);
+        raf.write(salt);
+        raf.writeLong(storeSize);
+
+        // TODO store resize
+        raf.writeLong(0);
+        raf.writeLong(0);
+        raf.writeLong(0);
+
+        raf.close();
+    }
+
+    // ------------- Store resizing
+    public void setMaxKeys(long maxStoreKeys, boolean shrinkNow) throws IOException {
+        // TODO
+        // NO-OP now
+    }
+
+    // ------------- Locking
+    /**
+     * Lock the entry // TODO locking
+     *
+     * This lock is <strong>not</strong> reentrance. No threads except Cleaner should hold more
+     * then one lock at a time (or deadlock may occur).
+     */
+    private boolean lockEntry(long offset) {
+        return true;
+    }
+
+    /**
+     * Unlock the entry // TODO locking
+     */
+    private void unlockEntry(long offset) {
+    }
+
+    // ------------- Hashing
+    /**
+     * <tt>0x10</tt> bytes of salt for better digestion, not too salty.
+     */
+    private byte[] salt;
+
+    /**
+     * Get hashed routing key
+     *
+     * @param routingKey
+     * @return
+     */
+    // TODO use a little cache?
+    private byte[] getDigestedRoutingKey(byte[] routingKey) {
+        Digest digest = SHA1.getInstance();
+        digest.update(routingKey);
+        digest.update(salt);
+
+        byte[] digestedKey = digest.digest();
+        byte[] hashedRoutingKey = new byte[0x20];
+
+        // SHA-1 is only 160-bits, must fill something on lower order bytes
+        System.arraycopy(digestedKey, 0, //
+         hashedRoutingKey, hashedRoutingKey.length - digestedKey.length,//
+         digestedKey.length);
+
+        return hashedRoutingKey;
+    }
+
+    /**
+     * Get offset in the hash table, given a plain routing key.
+     *
+     * @param routingKey
+     * @param storeSize
+     * @return
+     */
+    public long getOffsetFromPlainKey(byte[] routingKey, long storeSize) {
+        // Don't use NativeBigInteger, {@link net.i2p.util.NativeBigInteger#mod()} don't use native routine.
+        BigInteger bi = new BigInteger(getDigestedRoutingKey(routingKey));
+        return bi.mod(BigInteger.valueOf(storeSize)).longValue();
+    }
+
+    // ------------- Statistics (a.k.a. lies)
+    private final Object statLock = new Object();
+    private long hits;
+    private long misses;
+    private long writes;
+
+    public long hits() {
+        synchronized (statLock) {
+            return hits;
+        }
+    }
+
+    private void incHits() {
+        Logger.debug(this, "hit");
+        synchronized (statLock) {
+            hits++;
+        }
+    }
+
+    public long misses() {
+        synchronized (statLock) {
+            return misses;
+        }
+    }
+
+    private void incMisses() {
+        Logger.debug(this, "miss");
+        synchronized (statLock) {
+            misses++;
+        }
+    }
+
+    public long writes() {
+        synchronized (statLock) {
+            return writes;
+        }
+    }
+
+    private void incWrites() {
+        Logger.debug(this, "write");
+        synchronized (statLock) {
+            writes++;
+        }
+    }
+
+    public long keyCount() {
+        return 0;
+    }
+
+    public long getMaxKeys() {
+        return storeSize;
+    }
+}