diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java
index 05c3f2b2eb844622ccf44219f7f6f7b47e27e1a6..6e10805f39824bf6a8ac7ec80ed739f69388a5b2 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java
@@ -1,5 +1,6 @@
 package net.i2p.client.streaming;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.Arrays;
 
@@ -580,12 +581,15 @@ class Packet {
             cur += 2;
         }
         if (isFlagSet(FLAG_FROM_INCLUDED)) {
-            Destination optionFrom = new Destination();
+            ByteArrayInputStream bais = new ByteArrayInputStream(buffer, cur, length - cur);
             try {
-                cur += optionFrom.readBytes(buffer, cur);
+                Destination optionFrom = Destination.create(bais);
+                cur += optionFrom.size();
                 setOptionalFrom(optionFrom);
+            } catch (IOException ioe) {
+                throw new IllegalArgumentException("Bad from field", ioe);
             } catch (DataFormatException dfe) {
-                throw new IllegalArgumentException("Bad from field: " + dfe.getMessage());
+                throw new IllegalArgumentException("Bad from field", dfe);
             }
         }
         if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED)) {
@@ -631,10 +635,10 @@ class Packet {
             Log l = ctx.logManager().getLog(Packet.class);
             if (l.shouldLog(Log.WARN))
                 l.warn("Signature failed on " + toString(), new Exception("moo"));
-            if (false) {
-                l.error(Base64.encode(buffer, 0, size));
-                l.error("Signature: " + Base64.encode(_optionSignature.getData()));
-            }
+            //if (false) {
+            //    l.error(Base64.encode(buffer, 0, size));
+            //    l.error("Signature: " + Base64.encode(_optionSignature.getData()));
+            //}
         }
         return ok;
     }
diff --git a/core/java/src/net/i2p/client/datagram/I2PDatagramDissector.java b/core/java/src/net/i2p/client/datagram/I2PDatagramDissector.java
index 945043ea6c145ade9fb7f8c0331fac40ddd1bbb8..00c897a6f5d4a637afe0c8eb1f6795d593294835 100644
--- a/core/java/src/net/i2p/client/datagram/I2PDatagramDissector.java
+++ b/core/java/src/net/i2p/client/datagram/I2PDatagramDissector.java
@@ -65,12 +65,9 @@ public final class I2PDatagramDissector {
         this.valid = false;
         
         try {
-            rxDest = new Destination();
-            rxSign = new Signature();
-            
             // read destination
-            rxDest.readBytes(dgStream);
-            
+            rxDest = Destination.create(dgStream);
+            rxSign = new Signature();
             // read signature
             rxSign.readBytes(dgStream);
             
@@ -155,6 +152,7 @@ public final class I2PDatagramDissector {
      * @return The Destination of the I2P repliable datagram sender
      */
     public Destination extractSender() {
+      /****
         if (this.rxDest == null)
             return null;
         Destination retDest = new Destination();
@@ -167,6 +165,9 @@ public final class I2PDatagramDissector {
         }
         
         return retDest;
+      ****/
+        // dests are no longer modifiable
+        return rxDest;
     }
     
     /**
diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
index ff92edf0834a28ff59d8f6b13b9204a5f4cf182e..d1fd7530a3054bae6e35f03c70fd00f199da4371 100644
--- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
@@ -1162,12 +1162,12 @@ public class BlockfileNamingService extends DummyNamingService {
         /** returns null on error */
         public Object construct(byte[] b) {
             DestEntry rv = new DestEntry();
-            Destination dest = new Destination();
-            rv.dest = dest;
             ByteArrayInputStream bais = new ByteArrayInputStream(b);
             try {
                 rv.props = DataHelper.readProperties(bais);
-                dest.readBytes(bais);
+                //dest.readBytes(bais);
+                // Will this flush the dest cache too much?
+                rv.dest = Destination.create(bais);
             } catch (IOException ioe) {
                 logError("DB Read Fail", ioe);
                 return null;
diff --git a/core/java/src/net/i2p/data/Certificate.java b/core/java/src/net/i2p/data/Certificate.java
index 5c61df0472ea473634877ed8a1142933ab817ba8..ef6484f31ed4d147e47fb06562ad6e1aa7b75c12 100644
--- a/core/java/src/net/i2p/data/Certificate.java
+++ b/core/java/src/net/i2p/data/Certificate.java
@@ -45,7 +45,7 @@ public class Certificate extends DataStructureImpl {
 
     /**
      * If null cert, return immutable static instance, else create new
-     * @throws AIOOBE if not enough bytes
+     * @throws AIOOBE if not enough bytes, FIXME should throw DataFormatException
      * @since 0.8.3
      */
     public static Certificate create(byte[] data, int off) {
diff --git a/core/java/src/net/i2p/data/DataStructure.java b/core/java/src/net/i2p/data/DataStructure.java
index 0b9e65ba2e744b5ad41f7e8942f12b2c7e57592d..ab7086492a45c040cc09f44e7d53f82549c1dd9d 100644
--- a/core/java/src/net/i2p/data/DataStructure.java
+++ b/core/java/src/net/i2p/data/DataStructure.java
@@ -32,10 +32,13 @@ import java.io.OutputStream;
  * @author jrandom
  */
 public interface DataStructure /* extends Serializable */ {
+
     /**
      * Load up the current object with data from the given stream.  Data loaded 
      * this way must match the I2P data structure specification.
      *
+     * Warning - many classes will throw IllegalStateException if data is already set.
+     *
      * @param in stream to read from
      * @throws DataFormatException if the data is improperly formatted
      * @throws IOException if there was a problem reading the stream
@@ -61,11 +64,24 @@ public interface DataStructure /* extends Serializable */ {
     /**
      * Load the structure from the base 64 encoded data provided
      *
+     * Warning - many classes will throw IllegalStateException if data is already set.
+     * Warning - many classes will throw IllegalArgumentException if data is the wrong size.
+     *
      */
     public void fromBase64(String data) throws DataFormatException;
 
+    /**
+     *  @return may be null if data is not set
+     */
     public byte[] toByteArray();
 
+    /**
+     * Load the structure from the data provided
+     *
+     * Warning - many classes will throw IllegalStateException if data is already set.
+     * Warning - many classes will throw IllegalArgumentException if data is the wrong size.
+     *
+     */
     public void fromByteArray(byte data[]) throws DataFormatException;
 
     /**
diff --git a/core/java/src/net/i2p/data/DataStructureImpl.java b/core/java/src/net/i2p/data/DataStructureImpl.java
index 1ca043672ef2ae14f818bdc1921d113e0ae40b52..264f5918893f109d6c2f157c6c56c34943de619d 100644
--- a/core/java/src/net/i2p/data/DataStructureImpl.java
+++ b/core/java/src/net/i2p/data/DataStructureImpl.java
@@ -32,16 +32,19 @@ public abstract class DataStructureImpl implements DataStructure {
 
         return Base64.encode(data);
     }
+
     public void fromBase64(String data) throws DataFormatException {
         if (data == null) throw new DataFormatException("Null data passed in");
         byte bytes[] = Base64.decode(data);
         fromByteArray(bytes);
     }
+
     public Hash calculateHash() {
         byte data[] = toByteArray();
         if (data != null) return SHA256Generator.getInstance().calculateHash(data);
         return null;
     }
+
     public byte[] toByteArray() {
         try {
             ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
@@ -57,6 +60,7 @@ public abstract class DataStructureImpl implements DataStructure {
             return null;
         }
     }
+
     public void fromByteArray(byte data[]) throws DataFormatException {
         if (data == null) throw new DataFormatException("Null data passed in");
         try {
diff --git a/core/java/src/net/i2p/data/Destination.java b/core/java/src/net/i2p/data/Destination.java
index 6a1764805fa58cd0e3b3484edb16750de4b294db..716140edde30204911130dc27a81442684aecc44 100644
--- a/core/java/src/net/i2p/data/Destination.java
+++ b/core/java/src/net/i2p/data/Destination.java
@@ -9,6 +9,14 @@ package net.i2p.data;
  *
  */
 
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.Map;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.LHMCache;
+import net.i2p.util.SystemVersion;
+
 /**
  * Defines an end point in the I2P network.  The Destination may move around
  * in the network, but messages sent to the Destination will find it
@@ -20,13 +28,56 @@ package net.i2p.data;
  * The first bytes of the public key are used for the IV for leaseset encryption,
  * but that encryption is poorly designed and should be deprecated.
  *
+ * As of 0.9.9 this data structure is immutable after the two keys and the certificate
+ * are set; attempts to change them will throw an IllegalStateException.
+ *
  * @author jrandom
  */
 public class Destination extends KeysAndCert {
 
-    public Destination() {
+    private String _cachedB64;
+
+    //private static final boolean STATS = true;
+    private static final int CACHE_SIZE;
+    private static final int MIN_CACHE_SIZE = 32;
+    private static final int MAX_CACHE_SIZE = 512;
+    static {
+        long maxMemory = SystemVersion.getMaxMemory();
+        CACHE_SIZE = (int) Math.min(MAX_CACHE_SIZE, Math.max(MIN_CACHE_SIZE, maxMemory / 512*1024));
+        //if (STATS)
+        //    I2PAppContext.getGlobalContext().statManager().createRateStat("DestCache", "Hit rate", "Router", new long[] { 10*60*1000 });
+    }
+
+    private static final Map<SigningPublicKey, Destination> _cache = new LHMCache(CACHE_SIZE);
+
+    /**
+     * Pull from cache or return new
+     * @since 0.9.9
+     */
+    public static Destination create(InputStream in) throws DataFormatException, IOException {
+        PublicKey pk = PublicKey.create(in);
+        SigningPublicKey sk = SigningPublicKey.create(in);
+        Certificate c = Certificate.create(in);
+        Destination rv;
+        synchronized(_cache) {
+            rv = _cache.get(sk);
+        }
+        if (rv != null && rv.getPublicKey().equals(pk) && rv.getCertificate().equals(c)) {
+            //if (STATS)
+            //    I2PAppContext.getGlobalContext().statManager().addRateData("DestCache", 1);
+            return rv;
+        }
+        //if (STATS)
+        //    I2PAppContext.getGlobalContext().statManager().addRateData("DestCache", 0);
+        rv = new Destination(pk, sk, c);
+        synchronized(_cache) {
+            _cache.put(sk, rv);
+        }
+        return rv;
     }
 
+    public Destination() {}
+
     /**
      * alternative constructor which takes a base64 string representation
      * @param s a Base64 representation of the destination, as (eg) is used in hosts.txt
@@ -35,6 +86,15 @@ public class Destination extends KeysAndCert {
         fromBase64(s);
     }
 
+    /**
+     * @since 0.9.9
+     */
+    private Destination(PublicKey pk, SigningPublicKey sk, Certificate c) {
+        _publicKey = pk;
+        _signingKey = sk;
+        _certificate = c;
+    }
+
     /**
      *  deprecated, used only by Packet.java in streaming
      *  @return the written length (NOT the new offset)    
@@ -49,11 +109,17 @@ public class Destination extends KeysAndCert {
         return cur - offset;
     }
     
-    /** deprecated, used only by Packet.java in streaming */
+    /**
+     * @deprecated, was used only by Packet.java in streaming, now unused
+     *
+     * @throws IllegalStateException if data already set
+     */
     public int readBytes(byte source[], int offset) throws DataFormatException {
         if (source == null) throw new DataFormatException("Null source");
         if (source.length <= offset + PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES) 
             throw new DataFormatException("Not enough data (len=" + source.length + " off=" + offset + ")");
+        if (_publicKey != null || _signingKey != null || _certificate != null)
+            throw new IllegalStateException();
         int cur = offset;
         
         _publicKey = PublicKey.create(source, cur);
@@ -71,4 +137,26 @@ public class Destination extends KeysAndCert {
     public int size() {
         return PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES + _certificate.size();
     }
+
+    /**
+     *  Cache it.
+     *  Useful in I2PTunnelHTTPServer where it is added to the headers
+     *  @since 0.9.9
+     */
+    @Override
+    public String toBase64() {
+        if (_cachedB64 == null)
+            _cachedB64 = super.toBase64();
+        return _cachedB64;
+    }
+
+    /**
+     *  Clear the cache.
+     *  @since 0.9.9
+     */
+    public static void clearCache() {
+        synchronized(_cache) {
+            _cache.clear();
+        }
+    }
 }
diff --git a/core/java/src/net/i2p/data/KeysAndCert.java b/core/java/src/net/i2p/data/KeysAndCert.java
index 933eaa55e9dd031f3f1c91c283c02a9fa1c88a10..429d89be7c56bbeef7d195e9cbf1d12351ead5b8 100644
--- a/core/java/src/net/i2p/data/KeysAndCert.java
+++ b/core/java/src/net/i2p/data/KeysAndCert.java
@@ -24,6 +24,9 @@ import net.i2p.crypto.SHA256Generator;
  * Implemented in 0.8.2 and retrofitted over Destination and RouterIdentity.
  * There's actually no difference between the two of them.
  *
+ * As of 0.9.9 this data structure is immutable after the two keys and the certificate
+ * are set; attempts to change them will throw an IllegalStateException.
+ *
  * @since 0.8.2
  * @author zzz
  */
@@ -37,7 +40,12 @@ public class KeysAndCert extends DataStructureImpl {
         return _certificate;
     }
 
+    /**
+     * @throws IllegalStateException if was already set
+     */
     public void setCertificate(Certificate cert) {
+        if (_certificate != null)
+            throw new IllegalStateException();
         _certificate = cert;
         __calculatedHash = null;
     }
@@ -46,7 +54,12 @@ public class KeysAndCert extends DataStructureImpl {
         return _publicKey;
     }
 
+    /**
+     * @throws IllegalStateException if was already set
+     */
     public void setPublicKey(PublicKey key) {
+        if (_publicKey != null)
+            throw new IllegalStateException();
         _publicKey = key;
         __calculatedHash = null;
     }
@@ -55,20 +68,24 @@ public class KeysAndCert extends DataStructureImpl {
         return _signingKey;
     }
 
+    /**
+     * @throws IllegalStateException if was already set
+     */
     public void setSigningPublicKey(SigningPublicKey key) {
+        if (_signingKey != null)
+            throw new IllegalStateException();
         _signingKey = key;
         __calculatedHash = null;
     }
     
+    /**
+     * @throws IllegalStateException if data already set
+     */
     public void readBytes(InputStream in) throws DataFormatException, IOException {
-        //_publicKey = new PublicKey();
-        //_publicKey.readBytes(in);
+        if (_publicKey != null || _signingKey != null || _certificate != null)
+            throw new IllegalStateException();
         _publicKey = PublicKey.create(in);
-        //_signingKey = new SigningPublicKey();
-        //_signingKey.readBytes(in);
         _signingKey = SigningPublicKey.create(in);
-        //_certificate = new Certificate();
-        //_certificate.readBytes(in);
         _certificate = Certificate.create(in);
         __calculatedHash = null;
     }
@@ -86,9 +103,10 @@ public class KeysAndCert extends DataStructureImpl {
         if (object == this) return true;
         if ((object == null) || !(object instanceof KeysAndCert)) return false;
         KeysAndCert  ident = (KeysAndCert) object;
-        return DataHelper.eq(_certificate, ident._certificate)
-               && DataHelper.eq(_signingKey, ident._signingKey)
-               && DataHelper.eq(_publicKey, ident._publicKey);
+        return
+               DataHelper.eq(_signingKey, ident._signingKey)
+               && DataHelper.eq(_publicKey, ident._publicKey)
+               && DataHelper.eq(_certificate, ident._certificate);
     }
     
     /** the public key has enough randomness in it to use it by itself for speed */
diff --git a/core/java/src/net/i2p/data/LeaseSet.java b/core/java/src/net/i2p/data/LeaseSet.java
index a339ed694457a94aa4950223d8c6c5f5bfea8996..e497a1e08ec09373d0fd60bc86dc939396de8359 100644
--- a/core/java/src/net/i2p/data/LeaseSet.java
+++ b/core/java/src/net/i2p/data/LeaseSet.java
@@ -311,8 +311,7 @@ public class LeaseSet extends DatabaseEntry {
     public void readBytes(InputStream in) throws DataFormatException, IOException {
         if (_destination != null)
             throw new IllegalStateException();
-        _destination = new Destination();
-        _destination.readBytes(in);
+        _destination = Destination.create(in);
         _encryptionKey = PublicKey.create(in);
         _signingKey = SigningPublicKey.create(in);
         int numLeases = (int) DataHelper.readLong(in, 1);
diff --git a/core/java/src/net/i2p/data/PublicKey.java b/core/java/src/net/i2p/data/PublicKey.java
index 556ca95e66ac468ee582ba49272518e98aefc5ec..2a719ffc55c8833c8daff6be9fd515ee45be5603 100644
--- a/core/java/src/net/i2p/data/PublicKey.java
+++ b/core/java/src/net/i2p/data/PublicKey.java
@@ -26,8 +26,10 @@ public class PublicKey extends SimpleDataStructure {
     private static final SDSCache<PublicKey> _cache = new SDSCache(PublicKey.class, KEYSIZE_BYTES, CACHE_SIZE);
 
     /**
-     * Pull from cache or return new
-     * @throws AIOOBE if not enough bytes
+     * Pull from cache or return new.
+     * Deprecated - used only by deprecated Destination.readBytes(data, off)
+     *
+     * @throws AIOOBE if not enough bytes, FIXME should throw DataFormatException
      * @since 0.8.3
      */
     public static PublicKey create(byte[] data, int off) {
diff --git a/core/java/src/net/i2p/data/RouterIdentity.java b/core/java/src/net/i2p/data/RouterIdentity.java
index 4666242ea88dd841f569f5e7aa2769b6a840d6eb..9ee7a15fa7a3f2d27fcfb875e6e160aec3d45fba 100644
--- a/core/java/src/net/i2p/data/RouterIdentity.java
+++ b/core/java/src/net/i2p/data/RouterIdentity.java
@@ -13,6 +13,9 @@ package net.i2p.data;
  * Defines the unique identifier of a router, including any certificate or 
  * public key.
  *
+ * As of 0.9.9 this data structure is immutable after the two keys and the certificate
+ * are set; attempts to change them will throw an IllegalStateException.
+ *
  * @author jrandom
  */
 public class RouterIdentity extends KeysAndCert {
diff --git a/core/java/src/net/i2p/data/SDSCache.java b/core/java/src/net/i2p/data/SDSCache.java
index b31f5b14c73098fbb3df5ebb011a3f6999e3d2ea..464ca7f971032c4a9e6909a262e92872798dfb46 100644
--- a/core/java/src/net/i2p/data/SDSCache.java
+++ b/core/java/src/net/i2p/data/SDSCache.java
@@ -139,7 +139,7 @@ public class SDSCache<V extends SimpleDataStructure> {
                found = 0;
             }
         }
-        I2PAppContext.getGlobalContext().statManager().addRateData(_statName, found, 0);
+        I2PAppContext.getGlobalContext().statManager().addRateData(_statName, found);
         return rv;
     }
 
diff --git a/core/java/src/net/i2p/data/SigningPublicKey.java b/core/java/src/net/i2p/data/SigningPublicKey.java
index a5119e7952010882e716a0fd96ceae660911d007..d1c1a4687f627e4c617e28e239ed0d023f71c983 100644
--- a/core/java/src/net/i2p/data/SigningPublicKey.java
+++ b/core/java/src/net/i2p/data/SigningPublicKey.java
@@ -32,8 +32,10 @@ public class SigningPublicKey extends SimpleDataStructure {
     private final SigType _type;
 
     /**
-     * Pull from cache or return new
-     * @throws AIOOBE if not enough bytes
+     * Pull from cache or return new.
+     * Deprecated - used only by deprecated Destination.readBytes(data, off)
+     *
+     * @throws AIOOBE if not enough bytes, FIXME should throw DataFormatException
      * @since 0.8.3
      */
     public static SigningPublicKey create(byte[] data, int off) {
diff --git a/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java
index ee0f1b0d2f01996582e6bb8cf5feea0fe56dbdb0..ac5d43af61fdaf3976038426acbc4a3d04b44cac 100644
--- a/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java
@@ -62,9 +62,7 @@ public class DestReplyMessage extends I2CPMessageImpl {
                 if (size == Hash.HASH_LENGTH) {
                     _hash = Hash.create(in);
                 } else {
-                    Destination d = new Destination();
-                    d.readBytes(in);
-                    _dest = d;
+                    _dest = Destination.create(in);
                 }
             } catch (DataFormatException dfe) {
                 _dest = null;
diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
index c067bdc61033f73776138828a410c1f11bfcbba7..ef0e9419468c1bddbfcb7066e259f81062a7c3a5 100644
--- a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
@@ -92,8 +92,7 @@ public class SendMessageMessage extends I2CPMessageImpl {
         try {
             _sessionId = new SessionId();
             _sessionId.readBytes(in);
-            _destination = new Destination();
-            _destination.readBytes(in);
+            _destination = Destination.create(in);
             _payload = new Payload();
             _payload.readBytes(in);
             _nonce = DataHelper.readLong(in, 4);
diff --git a/core/java/src/net/i2p/data/i2cp/SessionConfig.java b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
index 663f7fcd499c05dee796cae0134fdb64e4679c47..5f0cc4811915f965d7c8ebc687edc1d4ee2111d2 100644
--- a/core/java/src/net/i2p/data/i2cp/SessionConfig.java
+++ b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
@@ -186,7 +186,7 @@ public class SessionConfig extends DataStructureImpl {
     }
 
     public void readBytes(InputStream rawConfig) throws DataFormatException, IOException {
-        _destination = new Destination();
+        _destination = Destination.create(rawConfig);
         _destination.readBytes(rawConfig);
         _options = DataHelper.readProperties(rawConfig);
         _creationDate = DataHelper.readDate(rawConfig);
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index c70b84f8f1f3c16722fa6dc27eb13a2d1d75e721..9a27b61b4ef306e7540d9259a9af195fd0197899 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -30,6 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
 import net.i2p.data.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.i2np.GarlicMessage;
@@ -312,6 +313,7 @@ public class Router implements RouterClock.ClockShiftListener {
     public static final void clearCaches() {
         ByteCache.clearAll();
         SimpleByteCache.clearAll();
+        Destination.clearCache();
     }
 
     /**