package freenet.store; import java.io.File; import java.io.IOException; import java.util.Random; import junit.framework.TestCase; import freenet.crypt.DummyRandomSource; import freenet.crypt.RandomSource; import freenet.keys.CHKBlock; import freenet.keys.CHKDecodeException; import freenet.keys.CHKEncodeException; import freenet.keys.CHKVerifyException; import freenet.keys.ClientCHK; import freenet.keys.ClientCHKBlock; import freenet.keys.Key; import freenet.node.SemiOrderedShutdownHook; import freenet.store.saltedhash.ResizablePersistentIntBuffer; import freenet.store.saltedhash.SaltedHashFreenetStore; import freenet.support.PooledExecutor; import freenet.support.SimpleReadOnlyArrayBucket; import freenet.support.Ticker; import freenet.support.TrivialTicker; import freenet.support.api.Bucket; import freenet.support.compress.Compressor; import freenet.support.io.ArrayBucketFactory; import freenet.support.io.BucketTools; import freenet.support.io.FileUtil; /** Test migration from a RAMFreenetStore to a SaltedHashFreenetStore */ public class RAMSaltMigrationTest extends TestCase { private RandomSource strongPRNG = new DummyRandomSource(43210); private Random weakPRNG = new Random(12340); private PooledExecutor exec = new PooledExecutor(); private Ticker ticker = new TrivialTicker(exec); private File tempDir; @Override protected void setUp() throws java.lang.Exception { tempDir = new File("tmp-slashdotstoretest"); tempDir.mkdir(); exec.start(); ResizablePersistentIntBuffer.setPersistenceTime(-1); } @Override protected void tearDown() { FileUtil.removeAll(tempDir); } public void testRAMStore() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { checkRAMStore(true); checkRAMStore(false); } private void checkRAMStore(boolean newFormat) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { CHKStore store = new CHKStore(); new RAMFreenetStore<CHKBlock>(store, 10); // Encode a block String test = "test"; ClientCHKBlock block = encodeBlock(test, newFormat); store.put(block.getBlock(), false); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test, data); } public void testRAMStoreOldBlocks() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { CHKStore store = new CHKStore(); new RAMFreenetStore<CHKBlock>(store, 10); // Encode a block String test = "test"; ClientCHKBlock block = encodeBlock(test, false); store.put(block.getBlock(), true); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test, data); // ignoreOldBlocks works. assertEquals(null, store.fetch(key.getNodeCHK(), false, true, null)); // Put it with oldBlock = false should unset the flag. store.put(block.getBlock(), false); verify = store.fetch(key.getNodeCHK(), false, true, null); data = decodeBlock(verify, key); assertEquals(test, data); } public void testSaltedStore() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { checkSaltedStore(false); checkSaltedStore(true); } public void checkSaltedStore(boolean newFormat) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { File f = new File(tempDir, "saltstore"); FileUtil.removeAll(f); CHKStore store = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, 10, false, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(null, true); for(int i=0;i<5;i++) { // Encode a block String test = "test" + i; ClientCHKBlock block = encodeBlock(test, newFormat); store.put(block.getBlock(), false); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test, data); } saltStore.close(); } private void innerTestSaltedStoreWithClose(int persistenceTime, int delay) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { ResizablePersistentIntBuffer.setPersistenceTime(persistenceTime); File f = new File(tempDir, "saltstore"); FileUtil.removeAll(f); CHKStore store = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, 10, false, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(null, true); checkBlocks(store, true, false); saltStore.close(); if(delay != 0) try { Thread.sleep(delay); } catch (InterruptedException e) { // Ignore } saltStore = SaltedHashFreenetStore.construct(new File(tempDir, "saltstore"), "teststore", store, weakPRNG, 10, false, SemiOrderedShutdownHook.get(), true, true, ticker, null); checkBlocks(store, false, false); saltStore.close(); } private void checkBlocks(CHKStore store, boolean write, boolean expectFailure) throws CHKEncodeException, IOException, CHKVerifyException, CHKDecodeException { for(int i=0;i<5;i++) { // Encode a block String test = "test" + i; // Use new format for every other block to ensure they are mixed in the same store. ClientCHKBlock block = encodeBlock(test, (i & 1) == 1); if(write) store.put(block.getBlock(), false); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); if(expectFailure) assertEquals(null, verify); else { String data = decodeBlock(verify, key); assertEquals(test, data); } } } private void innerTestSaltedStoreSlotFilterWithAbort(int persistenceTime, int delay, boolean expectFailure, boolean forceValidEmpty) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { ResizablePersistentIntBuffer.setPersistenceTime(persistenceTime); File f = new File(tempDir, "saltstore"); FileUtil.removeAll(f); CHKStore store = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, 10, true, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(ticker, true); // Make sure it's clear. checkBlocks(store, false, true); checkBlocks(store, true, false); if(delay != 0) try { Thread.sleep(delay); } catch (InterruptedException e) { // Ignore } saltStore.close(true); store = new CHKStore(); saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, 10, true, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(ticker, true); if(forceValidEmpty) saltStore.forceValidEmpty(); checkBlocks(store, false, expectFailure); saltStore.close(); } public void testSaltedStoreWithClose() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { // Write straight through should work. innerTestSaltedStoreWithClose(-1, 0); // Write on shutdown should work. innerTestSaltedStoreWithClose(0, 0); // Shorter interval than delay should work. innerTestSaltedStoreWithClose(1000, 2000); // Longer interval than delay should work (write on shutdown). innerTestSaltedStoreWithClose(5000, 0); // Write straight through should work even with abort. innerTestSaltedStoreSlotFilterWithAbort(-1, 0, false, false); // Write straight through should work innerTestSaltedStoreSlotFilterWithAbort(1000, 2000, false, false); // Even this should work, because the slots still say unknown. innerTestSaltedStoreSlotFilterWithAbort(5000, 0, false, false); // However if we set the unknown slots to known empty, it should fail. innerTestSaltedStoreSlotFilterWithAbort(5000, 0, true, true); // But if we do the same thing while giving it enough time to write, it should work. innerTestSaltedStoreSlotFilterWithAbort(-1, 0, false, true); innerTestSaltedStoreSlotFilterWithAbort(1000, 2000, false, true); } public void testSaltedStoreOldBlock() throws CHKEncodeException, CHKVerifyException, CHKDecodeException, IOException { checkSaltedStoreOldBlocks(5, 10, 0, false); checkSaltedStoreOldBlocks(5, 10, 50, false); checkSaltedStoreOldBlocks(5, 10, 0, true); } public void checkSaltedStoreOldBlocks(int keycount, int size, int bloomSize, boolean useSlotFilter) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { CHKStore store = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(new File(tempDir, "saltstore-"+keycount+"-"+size+"-"+bloomSize+"-"+useSlotFilter), "teststore", store, weakPRNG, size, useSlotFilter, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(null, true); ClientCHK[] keys = new ClientCHK[keycount]; String[] test = new String[keycount]; ClientCHKBlock[] block = new ClientCHKBlock[keycount]; for(int i=0;i<keycount;i++) { // Encode a block test[i] = "test" + i; block[i] = encodeBlock(test[i], true); store.put(block[i].getBlock(), true); keys[i] = block[i].getClientKey(); } for(int i=0;i<keycount;i++) { ClientCHK key = keys[i]; CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test[i], data); // ignoreOldBlocks works. assertEquals(null, store.fetch(key.getNodeCHK(), false, true, null)); // Put it with oldBlock = false should unset the flag. store.put(block[i].getBlock(), false); verify = store.fetch(key.getNodeCHK(), false, true, null); data = decodeBlock(verify, key); assertEquals(test[i], data); } saltStore.close(); } public void testSaltedStoreResize() throws CHKEncodeException, CHKVerifyException, CHKDecodeException, IOException { checkSaltedStoreResize(5, 10, 20, false, -1, false, true); checkSaltedStoreResize(5, 10, 20, true, -1, false, true); // Will write to disk on shutdown. checkSaltedStoreResize(5, 10, 20, true, 60*60*1000, false, true); // Using the old size causes it to resize on startup back to the old size. This needs testing too, and revealed some odd bugs. checkSaltedStoreResize(5, 10, 20, true, 60*60*1000, false, false); // It will force to disk after resizing, so should still work even with a long write time. checkSaltedStoreResize(5, 10, 20, true, 60*60*1000, true, true); checkSaltedStoreResize(5, 10, 20, true, 60*60*1000, true, false); } public void checkSaltedStoreResize(int keycount, int size, int newSize, boolean useSlotFilter, int persistenceTime, boolean abort, boolean openNewSize) throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { File f = new File(tempDir, "saltstore-"+keycount+"-"+size+"-"+useSlotFilter); FileUtil.removeAll(f); ResizablePersistentIntBuffer.setPersistenceTime(persistenceTime); CHKStore store = new CHKStore(); SaltedHashFreenetStore.NO_CLEANER_SLEEP = true; SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, size, useSlotFilter, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(ticker, true); ClientCHK[] keys = new ClientCHK[keycount]; String[] test = new String[keycount]; ClientCHKBlock[] block = new ClientCHKBlock[keycount]; for(int i=0;i<keycount;i++) { // Encode a block test[i] = "test" + i; block[i] = encodeBlock(test[i], true); store.put(block[i].getBlock(), true); keys[i] = block[i].getClientKey(); } saltStore.setMaxKeys(newSize, true); for(int i=0;i<keycount;i++) { ClientCHK key = keys[i]; CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); assert(verify != null); String data = decodeBlock(verify, key); assertEquals(test[i], data); } saltStore.close(abort); store = new CHKStore(); saltStore = SaltedHashFreenetStore.construct(f, "teststore", store, weakPRNG, openNewSize ? newSize : size, useSlotFilter, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(ticker, true); for(int i=0;i<keycount;i++) { ClientCHK key = keys[i]; CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); assert(verify != null); String data = decodeBlock(verify, key); assertEquals(test[i], data); } saltStore.close(); } public void testMigrate() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { CHKStore store = new CHKStore(); RAMFreenetStore<CHKBlock> ramStore = new RAMFreenetStore<CHKBlock>(store, 10); // Encode a block String test = "test"; ClientCHKBlock block = encodeBlock(test, true); store.put(block.getBlock(), false); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test, data); CHKStore newStore = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(new File(tempDir, "saltstore"), "teststore", newStore, weakPRNG, 10, false, SemiOrderedShutdownHook.get(), true, true, ticker, null); saltStore.start(null, true); ramStore.migrateTo(newStore, false); CHKBlock newVerify = store.fetch(key.getNodeCHK(), false, false, null); String newData = decodeBlock(newVerify, key); assertEquals(test, newData); saltStore.close(); } public void testMigrateKeyed() throws IOException, CHKEncodeException, CHKVerifyException, CHKDecodeException { CHKStore store = new CHKStore(); RAMFreenetStore<CHKBlock> ramStore = new RAMFreenetStore<CHKBlock>(store, 10); // Encode a block String test = "test"; ClientCHKBlock block = encodeBlock(test, true); store.put(block.getBlock(), false); ClientCHK key = block.getClientKey(); CHKBlock verify = store.fetch(key.getNodeCHK(), false, false, null); String data = decodeBlock(verify, key); assertEquals(test, data); byte[] storeKey = new byte[32]; strongPRNG.nextBytes(storeKey); CHKStore newStore = new CHKStore(); SaltedHashFreenetStore<CHKBlock> saltStore = SaltedHashFreenetStore.construct(new File(tempDir, "saltstore"), "teststore", newStore, weakPRNG, 10, false, SemiOrderedShutdownHook.get(), true, true, ticker, storeKey); saltStore.start(null, true); ramStore.migrateTo(newStore, false); CHKBlock newVerify = store.fetch(key.getNodeCHK(), false, false, null); String newData = decodeBlock(newVerify, key); assertEquals(test, newData); saltStore.close(); } private String decodeBlock(CHKBlock verify, ClientCHK key) throws CHKVerifyException, CHKDecodeException, IOException { ClientCHKBlock cb = new ClientCHKBlock(verify, key); Bucket output = cb.decode(new ArrayBucketFactory(), 32768, false); byte[] buf = BucketTools.toByteArray(output); return new String(buf, "UTF-8"); } private ClientCHKBlock encodeBlock(String test, boolean newFormat) throws CHKEncodeException, IOException { byte[] data = test.getBytes("UTF-8"); SimpleReadOnlyArrayBucket bucket = new SimpleReadOnlyArrayBucket(data); return ClientCHKBlock.encode(bucket, false, false, (short)-1, bucket.size(), Compressor.DEFAULT_COMPRESSORDESCRIPTOR, false, null, newFormat ? Key.ALGO_AES_CTR_256_SHA256 : Key.ALGO_AES_PCFB_256_SHA256); } }