diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index 959b8b797486d9a119fcf3a963ac5710cd213dc7..00b9afa935daa92b1590536eb44b4b30f7631b89 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -12,7 +12,19 @@ public class LogsHelper extends HelperBase {
     
     /** @since 0.8.11 */
     public String getJettyVersion() {
-        return Version.getImplVersion();
+        return jettyVersion();
+    }
+    
+    /** @since 0.8.13 */
+    static String jettyVersion() {
+        try {
+            String rv = Version.getImplVersion();
+            if (rv.startsWith("Jetty/"))
+                rv = rv.substring(6);
+            return rv;
+        } catch (Throwable t) {
+            return "unknown";
+        }
     }
 
     public String getLogs() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
index 7e44cd6e83b895504f2e92b04391897c301aa282..1a207763a155efe33203be5126012cae472f0b85 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -376,9 +376,8 @@ public class NetDbRenderer {
             int cost = addr.getCost();
             if (!((style.equals("SSU") && cost == 5) || (style.equals("NTCP") && cost == 10)))
                 buf.append('[').append(_("cost")).append('=').append("" + cost).append("] ");
-            Properties p = new OrderedProperties();
-            p.putAll(addr.getOptions());
-            for (Map.Entry e : p.entrySet()) {
+            Map p = addr.getOptionsMap();
+            for (Map.Entry e : (Set<Map.Entry>) p.entrySet()) {
                 String name = (String) e.getKey();
                 String val = (String) e.getValue();
                 buf.append('[').append(_(DataHelper.stripHTML(name))).append('=').append(DataHelper.stripHTML(val)).append("] ");
@@ -387,9 +386,10 @@ public class NetDbRenderer {
         buf.append("</td></tr>\n");
         if (full) {
             buf.append("<tr><td>" + _("Stats") + ": <br><code>");
-            for (Iterator iter = info.getOptions().keySet().iterator(); iter.hasNext(); ) {
-                String key = (String)iter.next();
-                String val = info.getOption(key);
+            Map p = info.getOptionsMap();
+            for (Map.Entry e : (Set<Map.Entry>) p.entrySet()) {
+                String key = (String) e.getKey();
+                String val = (String) e.getValue();
                 buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("<br>\n");
             }
             buf.append("</code></td></tr>\n");
@@ -412,7 +412,7 @@ public class NetDbRenderer {
             if (style.equals("NTCP")) {
                 rv |= NTCP;
             } else if (style.equals("SSU")) {
-                if (addr.getOptions().getProperty("iport0") != null)
+                if (addr.getOption("iport0") != null)
                     rv |= SSUI;
                 else
                     rv |= SSU;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index d1835a32065f46168517b8eec9ec6cd46e0efc4b..6be4b1cd7ee034d23a7e953f0e7eb7431e630a98 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -17,6 +17,7 @@ import java.util.Properties;
 import java.util.StringTokenizer;
 import java.util.concurrent.ConcurrentHashMap;
 
+import net.i2p.CoreVersion;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.router.Job;
@@ -27,6 +28,7 @@ import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.FileUtil;
 import net.i2p.util.Log;
 import net.i2p.util.Translate;
+import net.i2p.util.VersionComparator;
 
 import org.mortbay.jetty.Server;
 
@@ -95,6 +97,41 @@ public class PluginStarter implements Runnable {
             log.error("Cannot start nonexistent plugin: " + appName);
             return false;
         }
+
+        Properties props = pluginProperties(ctx, appName);
+        String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version");
+        if (minVersion != null &&
+            (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) {
+            String foo = "Plugin " + appName + " requires I2P version " + minVersion + " or higher";
+            log.error(foo);
+            throw new Exception(foo);
+        }
+
+        minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
+        if (minVersion != null &&
+            (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
+            String foo = "Plugin " + appName + " requires Java version " + minVersion + " or higher";
+            log.error(foo);
+            throw new Exception(foo);
+        }
+
+        String jVersion = LogsHelper.jettyVersion();
+        minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
+        if (minVersion != null &&
+            (new VersionComparator()).compare(minVersion, jVersion) > 0) {
+            String foo = "Plugin " + appName + " requires Jetty version " + minVersion + " or higher";
+            log.error(foo);
+            throw new Exception(foo);
+        }
+
+        String maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
+        if (maxVersion != null &&
+            (new VersionComparator()).compare(maxVersion, jVersion) < 0) {
+            String foo = "Plugin " + appName + " requires Jetty version " + maxVersion + " or lower";
+            log.error(foo);
+            throw new Exception(foo);
+        }
+
         if (log.shouldLog(Log.INFO))
             log.info("Starting plugin: " + appName);
 
@@ -113,8 +150,8 @@ public class PluginStarter implements Runnable {
         // load and start things in clients.config
         File clientConfig = new File(pluginDir, "clients.config");
         if (clientConfig.exists()) {
-            Properties props = new Properties();
-            DataHelper.loadProps(props, clientConfig);
+            Properties cprops = new Properties();
+            DataHelper.loadProps(cprops, clientConfig);
             List<ClientAppConfig> clients = ClientAppConfig.getClientApps(clientConfig);
             runClientApps(ctx, pluginDir, clients, "start");
         }
@@ -123,7 +160,7 @@ public class PluginStarter implements Runnable {
         Server server = WebAppStarter.getConsoleServer();
         if (server != null) {
             File consoleDir = new File(pluginDir, "console");
-            Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
+            Properties wprops = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
             File webappDir = new File(consoleDir, "webapps");
             String fileNames[] = webappDir.list(RouterConsoleRunner.WarFilenameFilter.instance());
             if (fileNames != null) {
@@ -138,7 +175,7 @@ public class PluginStarter implements Runnable {
                             log.error("Skipping duplicate webapp " + warName + " in plugin " + appName);
                             continue;
                         }
-                        String enabled = props.getProperty(RouterConsoleRunner.PREFIX + warName + ENABLED);
+                        String enabled = wprops.getProperty(RouterConsoleRunner.PREFIX + warName + ENABLED);
                         if (! "false".equals(enabled)) {
                             if (log.shouldLog(Log.INFO))
                                 log.info("Starting webapp: " + warName);
@@ -181,7 +218,6 @@ public class PluginStarter implements Runnable {
         }
 
         // add summary bar link
-        Properties props = pluginProperties(ctx, appName);
         String name = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(ctx));
         if (name == null)
             name = ConfigClientsHelper.stripHTML(props, "consoleLinkName");
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
index 884a20bb5fcbf1dbd92871b41a59747310f77a3f..511bfb0cd075de25f00352148c67a4a16c89a9d2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
@@ -334,6 +334,21 @@ public class PluginUpdateHandler extends UpdateHandler {
                     statusDone("<b>" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + "</b>");
                     return;
                 }
+                oldVersion = LogsHelper.jettyVersion();
+                minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
+                if (minVersion != null &&
+                    (new VersionComparator()).compare(minVersion, oldVersion) > 0) {
+                    to.delete();
+                    statusDone("<b>" + _("Plugin requires Jetty version {0} or higher", minVersion) + "</b>");
+                    return;
+                }
+                maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
+                if (maxVersion != null &&
+                    (new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
+                    to.delete();
+                    statusDone("<b>" + _("Plugin requires Jetty version {0} or lower", maxVersion) + "</b>");
+                    return;
+                }
 
                 // check if it is running first?
                 try {
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 95dd1875d21bdc38e31f14b548b1bb419a317061..420775065ad354581693816733318023a4630c9f 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -118,6 +118,17 @@ public class DataHelper {
     public static Properties readProperties(InputStream rawStream) 
         throws DataFormatException, IOException {
         Properties props = new OrderedProperties();
+        readProperties(rawStream, props);
+        return props;
+    }
+
+    /**
+     *  Ditto, load into an existing properties
+     *  @param props the Properties to load into
+     *  @since 0.8.13
+     */
+    public static Properties readProperties(InputStream rawStream, Properties props) 
+        throws DataFormatException, IOException {
         long size = readLong(rawStream, 2);
         byte data[] = new byte[(int) size];
         int read = read(rawStream, data);
@@ -1268,6 +1279,8 @@ public class DataHelper {
      *  Why? Just because it has to be consistent so signing will work.
      *  How to spec as returning the same type as the param?
      *  DEPRECATED - Only used by RouterInfo.
+     *
+     *  @return a new list
      */
     public static List<? extends DataStructure> sortStructures(Collection<? extends DataStructure> dataStructures) {
         if (dataStructures == null) return Collections.EMPTY_LIST;
diff --git a/core/java/src/net/i2p/data/DatabaseEntry.java b/core/java/src/net/i2p/data/DatabaseEntry.java
index 3c7b665ca2763eba2f9ab3f3766ad1b476d2aa2f..943fdbd733972d35da754890ef739bdaec0c1cf6 100644
--- a/core/java/src/net/i2p/data/DatabaseEntry.java
+++ b/core/java/src/net/i2p/data/DatabaseEntry.java
@@ -124,16 +124,22 @@ public abstract class DatabaseEntry extends DataStructureImpl {
     /**
      * Configure the proof that the entity stands behind the info here
      *
+     * @throws IllegalStateException if already signed
      */
     public void setSignature(Signature signature) {
+        if (_signature != null)
+            throw new IllegalStateException();
         _signature = signature;
     }
 
     /**
      * Sign the structure using the supplied signing key
      *
+     * @throws IllegalStateException if already signed
      */
     public void sign(SigningPrivateKey key) throws DataFormatException {
+        if (_signature != null)
+            throw new IllegalStateException();
         byte[] bytes = getBytes();
         if (bytes == null) throw new DataFormatException("Not enough data to sign");
         // now sign with the key 
diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/core/java/src/net/i2p/data/RouterAddress.java
index 91a05e6d4e9c5dfec522ae5cfbb6aee260efde90..012ef0194eea9fe33601eafe3683d4a61953c0de 100644
--- a/core/java/src/net/i2p/data/RouterAddress.java
+++ b/core/java/src/net/i2p/data/RouterAddress.java
@@ -12,6 +12,7 @@ package net.i2p.data;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.Map;
@@ -22,16 +23,24 @@ import net.i2p.util.OrderedProperties;
 /**
  * Defines a method of communicating with a router
  *
+ * For efficiency, the options methods and structures here are unsynchronized.
+ * Initialize the structure with readBytes(), or call the setOptions().
+ * Don't change it after that.
+ *
+ * To ensure integrity of the RouterInfo, methods that change an element of the
+ * RouterInfo will throw an IllegalStateException after the RouterInfo is signed.
+ *
  * @author jrandom
  */
 public class RouterAddress extends DataStructureImpl {
     private int _cost;
     private Date _expiration;
     private String _transportStyle;
-    private Properties _options;
+    private final Properties _options;
 
     public RouterAddress() {
         _cost = -1;
+        _options = new OrderedProperties();
     }
 
     /**
@@ -85,28 +94,59 @@ public class RouterAddress extends DataStructureImpl {
     /**
      * Configure the type of transport that must be used to communicate on this address
      *
+     * @throws IllegalStateException if was already set
      */
     public void setTransportStyle(String transportStyle) {
+        if (_transportStyle != null)
+            throw new IllegalStateException();
         _transportStyle = transportStyle;
     }
 
     /**
      * Retrieve the transport specific options necessary for communication 
      *
+     * @deprecated use getOptionsMap()
+     * @return sorted, non-null, NOT a copy, do not modify
      */
     public Properties getOptions() {
         return _options;
     }
 
     /**
-     * Specify the transport specific options necessary for communication 
+     * Retrieve the transport specific options necessary for communication 
      *
+     * @return an unmodifiable view, non-null, sorted
+     * @since 0.8.13
+     */
+    public Map getOptionsMap() {
+        return Collections.unmodifiableMap(_options);
+    }
+
+    /**
+     * @since 0.8.13
+     */
+    public String getOption(String opt) {
+        return _options.getProperty(opt);
+    }
+
+    /**
+     * Specify the transport specific options necessary for communication.
+     * Makes a copy.
+     * @param options non-null
+     * @throws IllegalStateException if was already set
      */
     public void setOptions(Properties options) {
-        _options = options;
+        if (!_options.isEmpty())
+            throw new IllegalStateException();
+        _options.putAll(options);
     }
     
+    /**
+     *  @throws IllegalStateException if was already read in
+     */
     public void readBytes(InputStream in) throws DataFormatException, IOException {
+        if (_transportStyle != null)
+            throw new IllegalStateException();
         _cost = (int) DataHelper.readLong(in, 1);
         _expiration = DataHelper.readDate(in);
         _transportStyle = DataHelper.readString(in);
@@ -115,11 +155,11 @@ public class RouterAddress extends DataStructureImpl {
             _transportStyle = "SSU";
         else if (_transportStyle.equals("NTCP"))
             _transportStyle = "NTCP";
-        _options = DataHelper.readProperties(in);
+        DataHelper.readProperties(in, _options);
     }
     
     public void writeBytes(OutputStream out) throws DataFormatException, IOException {
-        if ((_cost < 0) || (_transportStyle == null) || (_options == null))
+        if ((_cost < 0) || (_transportStyle == null))
             throw new DataFormatException("Not enough data to write a router address");
         DataHelper.writeLong(out, 1, _cost);
         DataHelper.writeDate(out, _expiration);
@@ -131,11 +171,12 @@ public class RouterAddress extends DataStructureImpl {
     public boolean equals(Object object) {
         if ((object == null) || !(object instanceof RouterAddress)) return false;
         RouterAddress addr = (RouterAddress) object;
+        // let's keep this fast as we are putting an address into the RouterInfo set frequently
         return
                _cost == addr._cost &&
-               DataHelper.eq(_transportStyle, addr._transportStyle) &&
-               DataHelper.eq(_options, addr._options) &&
-               DataHelper.eq(_expiration, addr._expiration);
+               DataHelper.eq(_transportStyle, addr._transportStyle);
+               //DataHelper.eq(_options, addr._options) &&
+               //DataHelper.eq(_expiration, addr._expiration);
     }
     
     /**
@@ -161,9 +202,7 @@ public class RouterAddress extends DataStructureImpl {
         buf.append("\n\tExpiration: ").append(_expiration);
         if (_options != null) {
             buf.append("\n\tOptions: #: ").append(_options.size());
-            Properties p = new OrderedProperties();
-            p.putAll(_options);
-            for (Map.Entry e : p.entrySet()) {
+            for (Map.Entry e : _options.entrySet()) {
                 String key = (String) e.getKey();
                 String val = (String) e.getValue();
                 buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]");
diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/core/java/src/net/i2p/data/RouterInfo.java
index ca4420403039506798af37815218097cd4201fe0..6c34ad2023989aed04caff0b47d662e346296e0a 100644
--- a/core/java/src/net/i2p/data/RouterInfo.java
+++ b/core/java/src/net/i2p/data/RouterInfo.java
@@ -19,6 +19,7 @@ import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.Vector;
@@ -32,6 +33,13 @@ import net.i2p.util.OrderedProperties;
  * Defines the data that a router either publishes to the global routing table or
  * provides to trusted peers.  
  *
+ * For efficiency, the methods and structures here are now unsynchronized.
+ * Initialize the RI with readBytes(), or call the setters and then sign() in a single thread.
+ * Don't change it after that.
+ *
+ * To ensure integrity of the RouterInfo, methods that change an element of the
+ * RouterInfo will throw an IllegalStateException after the RouterInfo is signed.
+ *
  * @author jrandom
  */
 public class RouterInfo extends DatabaseEntry {
@@ -41,7 +49,7 @@ public class RouterInfo extends DatabaseEntry {
     private final Set<RouterAddress> _addresses;
     /** may be null to save memory, no longer final */
     private Set<Hash> _peers;
-    private /* FIXME final FIXME */ Properties _options;
+    private final Properties _options;
     private volatile boolean _validated;
     private volatile boolean _isValid;
     private volatile String _stringified;
@@ -67,14 +75,19 @@ public class RouterInfo extends DatabaseEntry {
         _options = new OrderedProperties();
     }
 
+    /**
+     *  Used only by Router and PublishLocalRouterInfoJob.
+     *  Copies ONLY the identity and peers.
+     *  Does not copy published, addresses, options, or signature.
+     */
     public RouterInfo(RouterInfo old) {
         this();
         setIdentity(old.getIdentity());
-        setPublished(old.getPublished());
-        setAddresses(old.getAddresses());
+        //setPublished(old.getPublished());
+        //setAddresses(old.getAddresses());
         setPeers(old.getPeers());
-        setOptions(old.getOptions());
-        setSignature(old.getSignature());
+        //setOptions(old.getOptions());
+        //setSignature(old.getSignature());
         // copy over _byteified?
     }
 
@@ -90,12 +103,6 @@ public class RouterInfo extends DatabaseEntry {
         return KEY_TYPE_ROUTERINFO;
     }
 
-    private void resetCache() {
-        _stringified = null;
-        _byteified = null;
-        _hashCodeInitialized = false;
-    }
-
     /**
      * Retrieve the identity of the router represented
      *
@@ -107,10 +114,12 @@ public class RouterInfo extends DatabaseEntry {
     /**
      * Configure the identity of the router represented
      * 
+     * @throws IllegalStateException if RouterInfo is already signed
      */
     public void setIdentity(RouterIdentity ident) {
+        if (_signature != null)
+            throw new IllegalStateException();
         _identity = ident;
-        resetCache();
         // We only want to cache the bytes for our own RI, which is frequently written.
         // To cache for all RIs doubles the RI memory usage.
         // setIdentity() is only called when we are creating our own RI.
@@ -133,34 +142,35 @@ public class RouterInfo extends DatabaseEntry {
     /**
      * Date on which it was published, in milliseconds since Midnight GMT on Jan 01, 1970
      *
+     * @throws IllegalStateException if RouterInfo is already signed
      */
     public void setPublished(long published) {
+        if (_signature != null)
+            throw new IllegalStateException();
         _published = published;
-        resetCache();
     }
 
     /**
      * Retrieve the set of RouterAddress structures at which this
      * router can be contacted.
      *
+     * @return unmodifiable view, non-null
      */
     public Set<RouterAddress> getAddresses() {
-        synchronized (_addresses) {
-            return new HashSet(_addresses);
-        }
+            return Collections.unmodifiableSet(_addresses);
     }
 
     /**
      * Specify a set of RouterAddress structures at which this router
      * can be contacted.
      *
+     * @throws IllegalStateException if RouterInfo is already signed
      */
     public void setAddresses(Set<RouterAddress> addresses) {
-        synchronized (_addresses) {
-            _addresses.clear();
-            if (addresses != null) _addresses.addAll(addresses);
-        }
-        resetCache();
+        if (_signature != null)
+            throw new IllegalStateException();
+        _addresses.clear();
+        if (addresses != null) _addresses.addAll(addresses);
     }
 
     /**
@@ -180,8 +190,11 @@ public class RouterInfo extends DatabaseEntry {
      * this router can be reached through.
      *
      * @deprecated Implemented here but unused elsewhere
+     * @throws IllegalStateException if RouterInfo is already signed
      */
     public void setPeers(Set<Hash> peers) {
+        if (_signature != null)
+            throw new IllegalStateException();
         if (peers == null || peers.isEmpty()) {
             _peers = null;
             return;
@@ -192,37 +205,46 @@ public class RouterInfo extends DatabaseEntry {
             _peers.clear();
             _peers.addAll(peers);
         }
-        resetCache();
     }
 
     /**
-     * Retrieve a set of options or statistics that the router can expose
+     * Retrieve a set of options or statistics that the router can expose.
      *
+     * @deprecated use getOptionsMap()
+     * @return sorted, non-null, NOT a copy, do not modify!!!
      */
     public Properties getOptions() {
-        if (_options == null) return new Properties();
-        synchronized (_options) {
-            return (Properties) _options.clone();
-        }
+        return _options;
     }
+
+    /**
+     * Retrieve a set of options or statistics that the router can expose.
+     *
+     * @return an unmodifiable view, non-null, sorted
+     * @since 0.8.13
+     */
+    public Map getOptionsMap() {
+        return Collections.unmodifiableMap(_options);
+    }
+
     public String getOption(String opt) {
-        if (_options == null) return null;
-        synchronized (_options) {
-            return _options.getProperty(opt);
-        }
+        return _options.getProperty(opt);
     }
 
     /**
-     * Configure a set of options or statistics that the router can expose
+     * Configure a set of options or statistics that the router can expose.
+     * Makes a copy.
+     *
      * @param options if null, clears current options
+     * @throws IllegalStateException if RouterInfo is already signed
      */
     public void setOptions(Properties options) {
-        synchronized (_options) {
-            _options.clear();
-            if (options != null)
-                _options.putAll(options);
-        }
-        resetCache();
+        if (_signature != null)
+            throw new IllegalStateException();
+
+        _options.clear();
+        if (options != null)
+            _options.putAll(options);
     }
 
     /** 
@@ -234,14 +256,14 @@ public class RouterInfo extends DatabaseEntry {
     protected byte[] getBytes() throws DataFormatException {
         if (_byteified != null) return _byteified;
         if (_identity == null) throw new DataFormatException("Router identity isn't set? wtf!");
-        if (_addresses == null) throw new DataFormatException("Router addressess isn't set? wtf!");
-        if (_options == null) throw new DataFormatException("Router options isn't set? wtf!");
 
         //long before = Clock.getInstance().now();
-        ByteArrayOutputStream out = new ByteArrayOutputStream(6*1024);
+        ByteArrayOutputStream out = new ByteArrayOutputStream(2*1024);
         try {
             _identity.writeBytes(out);
-            DataHelper.writeDate(out, new Date(_published));
+            // avoid thrashing objects
+            //DataHelper.writeDate(out, new Date(_published));
+            DataHelper.writeLong(out, 8, _published);
             int sz = _addresses.size();
             if (sz <= 0 || isHidden()) {
                 // Do not send IP address to peers in hidden mode
@@ -249,11 +271,12 @@ public class RouterInfo extends DatabaseEntry {
             } else {
                 DataHelper.writeLong(out, 1, sz);
                 Collection<RouterAddress> addresses = _addresses;
-                if (sz > 1)
+                if (sz > 1) {
                     // WARNING this sort algorithm cannot be changed, as it must be consistent
                     // network-wide. The signature is not checked at readin time, but only
                     // later, and the addresses are stored in a Set, not a List.
                     addresses = (Collection<RouterAddress>) DataHelper.sortStructures(addresses);
+                }
                 for (RouterAddress addr : addresses) {
                     addr.writeBytes(out);
                 }
@@ -293,7 +316,7 @@ public class RouterInfo extends DatabaseEntry {
      * Determine whether this router info is authorized with a valid signature
      *
      */
-    public synchronized boolean isValid() {
+    public boolean isValid() {
         if (!_validated) doValidate();
         return _isValid;
     }
@@ -304,11 +327,7 @@ public class RouterInfo extends DatabaseEntry {
      * @return -1 if unknown
      */
     public int getNetworkId() {
-        if (_options == null) return -1;
-        String id = null;
-        synchronized (_options) {
-            id = _options.getProperty(PROP_NETWORK_ID);
-        }
+        String id = _options.getProperty(PROP_NETWORK_ID);
         if (id != null) {
             try {
                 return Integer.parseInt(id);
@@ -322,11 +341,7 @@ public class RouterInfo extends DatabaseEntry {
      * @return non-null, empty string if none
      */
     public String getCapabilities() {
-        if (_options == null) return "";
-        String capabilities = null;
-        synchronized (_options) {
-            capabilities = _options.getProperty(PROP_CAPABILITIES);
-        }
+        String capabilities = _options.getProperty(PROP_CAPABILITIES);
         if (capabilities != null)
             return capabilities;
         else
@@ -358,20 +373,27 @@ public class RouterInfo extends DatabaseEntry {
         return (bwTier);
     }
 
+    /**
+     * @throws IllegalStateException if RouterInfo is already signed
+     */
     public void addCapability(char cap) {
-        if (_options == null) _options = new OrderedProperties();
-        synchronized (_options) {
+        if (_signature != null)
+            throw new IllegalStateException();
+
             String caps = _options.getProperty(PROP_CAPABILITIES);
             if (caps == null)
                 _options.setProperty(PROP_CAPABILITIES, ""+cap);
             else if (caps.indexOf(cap) == -1)
                 _options.setProperty(PROP_CAPABILITIES, caps + cap);
-        }
     }
 
+    /**
+     * @throws IllegalStateException if RouterInfo is already signed
+     */
     public void delCapability(char cap) {
-        if (_options == null) return;
-        synchronized (_options) {
+        if (_signature != null)
+            throw new IllegalStateException();
+
             String caps = _options.getProperty(PROP_CAPABILITIES);
             int idx;
             if (caps == null) {
@@ -384,7 +406,6 @@ public class RouterInfo extends DatabaseEntry {
                     buf.deleteCharAt(idx);
                 _options.setProperty(PROP_CAPABILITIES, buf.toString());
             }
-        }
     }
 
     /**
@@ -409,12 +430,9 @@ public class RouterInfo extends DatabaseEntry {
      *
      */
     public RouterAddress getTargetAddress(String transportStyle) {
-        synchronized (_addresses) {
-            for (Iterator iter = _addresses.iterator(); iter.hasNext(); ) {
-                RouterAddress addr = (RouterAddress)iter.next();
-                if (addr.getTransportStyle().equals(transportStyle)) 
-                    return addr;
-            }
+        for (RouterAddress addr :  _addresses) {
+            if (addr.getTransportStyle().equals(transportStyle)) 
+                return addr;
         }
         return null;
     }
@@ -425,12 +443,9 @@ public class RouterInfo extends DatabaseEntry {
      */
     public List<RouterAddress> getTargetAddresses(String transportStyle) {
         List<RouterAddress> ret = new Vector<RouterAddress>();
-        synchronized(this._addresses) {
-            for(Object o : this._addresses) {
-                RouterAddress addr = (RouterAddress)o;
-                if(addr.getTransportStyle().equals(transportStyle))
-                    ret.add(addr);
-            }
+        for (RouterAddress addr :  _addresses) {
+            if(addr.getTransportStyle().equals(transportStyle))
+                ret.add(addr);
         }
         return ret;
     }
@@ -438,9 +453,9 @@ public class RouterInfo extends DatabaseEntry {
     /**
      * Actually validate the signature
      */
-    private synchronized void doValidate() {
-        _validated = true;
+    private void doValidate() {
         _isValid = super.verifySignature();
+        _validated = true;
 
         if (!_isValid) {
             byte data[] = null;
@@ -459,15 +474,21 @@ public class RouterInfo extends DatabaseEntry {
     
     /**
      *  This does NOT validate the signature
+     *
+     *  @throws IllegalStateException if RouterInfo was already read in
      */
-    public synchronized void readBytes(InputStream in) throws DataFormatException, IOException {
+    public void readBytes(InputStream in) throws DataFormatException, IOException {
+        if (_signature != null)
+            throw new IllegalStateException();
         _identity = new RouterIdentity();
         _identity.readBytes(in);
-        Date when = DataHelper.readDate(in);
-        if (when == null)
-            _published = 0;
-        else
-            _published = when.getTime();
+        // avoid thrashing objects
+        //Date when = DataHelper.readDate(in);
+        //if (when == null)
+        //    _published = 0;
+        //else
+        //    _published = when.getTime();
+        _published = DataHelper.readLong(in, 8);
         int numAddresses = (int) DataHelper.readLong(in, 1);
         for (int i = 0; i < numAddresses; i++) {
             RouterAddress address = new RouterAddress();
@@ -485,11 +506,10 @@ public class RouterInfo extends DatabaseEntry {
                 _peers.add(peerIdentityHash);
             }
         }
-        _options = DataHelper.readProperties(in);
+        DataHelper.readProperties(in, _options);
         _signature = new Signature();
         _signature.readBytes(in);
 
-        resetCache();
 
         //_log.debug("Read routerInfo: " + toString());
     }
@@ -497,13 +517,13 @@ public class RouterInfo extends DatabaseEntry {
     /**
      *  This does NOT validate the signature
      */
-    public synchronized void writeBytes(OutputStream out) throws DataFormatException, IOException {
+    public void writeBytes(OutputStream out) throws DataFormatException, IOException {
         if (_identity == null) throw new DataFormatException("Missing identity");
         if (_published < 0) throw new DataFormatException("Invalid published date: " + _published);
         if (_signature == null) throw new DataFormatException("Signature is null");
         //if (!isValid())
         //    throw new DataFormatException("Data is not valid");
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
         baos.write(getBytes());
         _signature.writeBytes(baos);
 
@@ -518,10 +538,11 @@ public class RouterInfo extends DatabaseEntry {
         RouterInfo info = (RouterInfo) object;
         return DataHelper.eq(_identity, info.getIdentity())
                && DataHelper.eq(_signature, info.getSignature())
-               && _published == info.getPublished()
-               && DataHelper.eq(_addresses, info.getAddresses())
-               && DataHelper.eq(_options, info.getOptions()) 
-               && DataHelper.eq(getPeers(), info.getPeers());
+               && _published == info.getPublished();
+               // Let's speed up the NetDB
+               //&& DataHelper.eq(_addresses, info.getAddresses())
+               //&& DataHelper.eq(_options, info.getOptions()) 
+               //&& DataHelper.eq(getPeers(), info.getPeers());
     }
     
     @Override
@@ -541,23 +562,19 @@ public class RouterInfo extends DatabaseEntry {
         buf.append("\n\tIdentity: ").append(_identity);
         buf.append("\n\tSignature: ").append(_signature);
         buf.append("\n\tPublished on: ").append(new Date(_published));
-        Set addresses = _addresses; // getAddresses()
-        buf.append("\n\tAddresses: #: ").append(addresses.size());
-        for (Iterator iter = addresses.iterator(); iter.hasNext();) {
-            RouterAddress addr = (RouterAddress) iter.next();
+        buf.append("\n\tAddresses: #: ").append(_addresses.size());
+        for (RouterAddress addr : _addresses) {
             buf.append("\n\t\tAddress: ").append(addr);
         }
-        Set peers = getPeers();
+        Set<Hash> peers = getPeers();
         buf.append("\n\tPeers: #: ").append(peers.size());
-        for (Iterator iter = peers.iterator(); iter.hasNext();) {
-            Hash hash = (Hash) iter.next();
+        for (Hash hash : peers) {
             buf.append("\n\t\tPeer hash: ").append(hash);
         }
-        Properties options = _options; // getOptions();
-        buf.append("\n\tOptions: #: ").append(options.size());
-        for (Iterator iter = options.keySet().iterator(); iter.hasNext();) {
-            String key = (String) iter.next();
-            String val = options.getProperty(key);
+        buf.append("\n\tOptions: #: ").append(_options.size());
+        for (Map.Entry e : _options.entrySet()) {
+            String key = (String) e.getKey();
+            String val = (String) e.getValue();
             buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]");
         }
         buf.append("]");
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
index f76e6eb49961f558c6c239fdcaff31d06eba318d..153b14e7c928064182255abb0e51f209f9764946 100644
--- a/router/java/src/net/i2p/router/Blocklist.java
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -488,9 +488,7 @@ public class Blocklist {
         for (int j = 0; j < paddr.size(); j++) {
             RouterAddress pa = (RouterAddress) pladdr.get(j);
             if (pa == null) continue;
-            Properties pprops = pa.getOptions();
-            if (pprops == null) continue;
-            String phost = pprops.getProperty("host");
+            String phost = pa.getOption("host");
             if (phost == null) continue;
             if (oldphost != null && oldphost.equals(phost)) continue;
             oldphost = phost;
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 08ea93bd8c3a5984ea157335ec7a10f4ab727184..e7172550f88194f6b1c9ebacbaedf37277e74242 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -60,7 +60,7 @@ import net.i2p.util.SimpleScheduler;
  *
  */
 public class Router implements RouterClock.ClockShiftListener {
-    private final Log _log;
+    private Log _log;
     private final RouterContext _context;
     private final Map<String, String> _config;
     /** full path */
@@ -77,9 +77,9 @@ public class Router implements RouterClock.ClockShiftListener {
     private ShutdownHook _shutdownHook;
     /** non-cancellable shutdown has begun */
     private volatile boolean _shutdownInProgress;
-    private final I2PThread _gracefulShutdownDetector;
-    private final RouterWatchdog _watchdog;
-    private final Thread _watchdogThread;
+    private I2PThread _gracefulShutdownDetector;
+    private RouterWatchdog _watchdog;
+    private Thread _watchdogThread;
     
     public final static String PROP_CONFIG_FILE = "router.configLocation";
     
@@ -128,9 +128,17 @@ public class Router implements RouterClock.ClockShiftListener {
         System.setProperty("Dorg.mortbay.util.FileResource.checkAliases", "true");
     }
     
+    /**
+     *  Instantiation only. Starts no threads. Does not install updates.
+     *  RouterContext is created but not initialized.
+     *  You must call runRouter() after any constructor to start things up.
+     */
     public Router() { this(null, null); }
+
     public Router(Properties envProps) { this(null, envProps); }
+
     public Router(String configFilename) { this(configFilename, null); }
+
     public Router(String configFilename, Properties envProps) {
         _gracefulExitCode = -1;
         _config = new ConcurrentHashMap();
@@ -235,14 +243,16 @@ public class Router implements RouterClock.ClockShiftListener {
             _config.put("router.updateLastInstalled", now);
             saveConfig();
         }
+        // *********  Start no threads before here ********* //
+    }
 
-        // This is here so that we can get the directory location from the context
-        // for the zip file and the base location to unzip to.
-        // If it does an update, it never returns.
-        // I guess it's better to have the other-router check above this, we don't want to
-        // overwrite an existing running router's jar files. Other than ours.
-        installUpdates();
-
+    /**
+     *  Initializes the RouterContext.
+     *  Starts some threads. Does not install updates.
+     *  All this was in the constructor.
+     *  @since 0.8.12
+     */
+    private void startupStuff() {
         // *********  Start no threads before here ********* //
         //
         // NOW we can start the ping file thread.
@@ -372,7 +382,14 @@ public class Router implements RouterClock.ClockShiftListener {
     
     public RouterContext getContext() { return _context; }
     
+    /**
+     *  Initializes the RouterContext.
+     *  Starts the threads. Does not install updates.
+     */
     void runRouter() {
+        if (_isAlive)
+            throw new IllegalStateException();
+        startupStuff();
         _isAlive = true;
         _started = _context.clock().now();
         try {
@@ -1266,13 +1283,18 @@ public class Router implements RouterClock.ClockShiftListener {
 
     public static void main(String args[]) {
         System.out.println("Starting I2P " + RouterVersion.FULL_VERSION);
-        // installUpdates() moved to constructor so we can get file locations from the context
-        // installUpdates();
         //verifyWrapperConfig();
         Router r = new Router();
         if ( (args != null) && (args.length == 1) && ("rebuild".equals(args[0])) ) {
             r.rebuildNewIdentity();
         } else {
+            // This is here so that we can get the directory location from the context
+            // for the zip file and the base location to unzip to.
+            // If it does an update, it never returns.
+            // I guess it's better to have the other-router check above this, we don't want to
+            // overwrite an existing running router's jar files. Other than ours.
+            r.installUpdates();
+            // *********  Start no threads before here ********* //
             r.runRouter();
         }
     }
@@ -1281,6 +1303,7 @@ public class Router implements RouterClock.ClockShiftListener {
     private static final String DELETE_FILE = "deletelist.txt";
     
     /**
+     * Context must be available.
      * Unzip update file found in the router dir OR base dir, to the base dir
      *
      * If we can't write to the base dir, complain.
diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
index 18612c3cd113f16ac9cbf2dcdcfde44ed2356146..cfcdd8a34b49c44cb807ad8e520509cfa08bc235 100644
--- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
@@ -41,7 +41,7 @@ public class PublishLocalRouterInfoJob extends JobImpl {
         RouterInfo ri = new RouterInfo(getContext().router().getRouterInfo());
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Old routerInfo contains " + ri.getAddresses().size() 
-                       + " addresses and " + ri.getOptions().size() + " options");
+                       + " addresses and " + ri.getOptionsMap().size() + " options");
         Properties stats = getContext().statPublisher().publishStatistics();
         stats.setProperty(RouterInfo.PROP_NETWORK_ID, ""+Router.NETWORK_ID);
         try {
@@ -60,7 +60,7 @@ public class PublishLocalRouterInfoJob extends JobImpl {
             getContext().router().setRouterInfo(ri);
             if (_log.shouldLog(Log.INFO))
                 _log.info("Newly updated routerInfo is published with " + stats.size() 
-                          + "/" + ri.getOptions().size() + " options on " 
+                          + "/" + ri.getOptionsMap().size() + " options on " 
                           + new Date(ri.getPublished()));
             try {
                 getContext().netDb().publish(ri);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java
index 5c719d76ca362e4df3b544c5f25d61cec1af25dc..2d23532960fdc9093d570e61cbffa7faafbe3962 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java
@@ -133,8 +133,7 @@ class FloodfillMonitorJob extends JobImpl {
             if (ra == null)
                 happy = false;
             else {
-                Properties props = ra.getOptions();
-                if (props == null || props.getProperty("ihost0") != null)
+                if (ra.getOption("ihost0") != null)
                    happy = false;
             }
         }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index f3a046a45bb29e9bf6866e9f6abe5c44b5ffa593..8a44d5a4e6d94dc9d00fd9d7de5e55cc70a83de9 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -786,8 +786,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             RouterAddress ra = routerInfo.getTargetAddress("SSU");
             if (ra != null) {
                 // Introducers change often, introducee will ping introducer for 2 hours
-                Properties props = ra.getOptions();
-                if (props != null && props.getProperty("ihost0") != null)
+                if (ra.getOption("ihost0") != null)
                     return "Peer " + key.toBase64() + " published > 75m ago with SSU Introducers";
                 if (routerInfo.getTargetAddress("NTCP") == null)
                     return "Peer " + key.toBase64() + " published > 75m ago, SSU only without introducers";
@@ -822,10 +821,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         if (err != null)
             throw new IllegalArgumentException("Invalid store attempt - " + err);
         
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("RouterInfo " + key.toBase64() + " is stored with "
-        //               + routerInfo.getOptions().size() + " options on "
-        //               + new Date(routerInfo.getPublished()));
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("RouterInfo " + key.toBase64() + " is stored with "
+                       + routerInfo.getOptionsMap().size() + " options on "
+                       + new Date(routerInfo.getPublished()));
     
         _context.peerManager().setCapabilities(key, routerInfo.getCapabilities());
         _ds.put(key, routerInfo, persist);
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index 093ca8ed2dd188b946466e7fb9c096fe8bddc274..8b14d22a0f5a8ce73d096b5a318d46508f56ebe9 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -701,8 +701,7 @@ public class ProfileOrganizer {
                             continue;
                         }
                         // This is the quick way of doing UDPAddress.getIntroducerCount() > 0
-                        Properties props = ra.getOptions();
-                        if (props != null && props.getProperty("ihost0") != null)
+                        if (ra.getOption("ihost0") != null)
                             l.add(peer);
                     }
                 }
@@ -1263,9 +1262,7 @@ public class ProfileOrganizer {
         if (paddr == null)
             return rv;
         for (RouterAddress pa : paddr) {
-            Properties pprops = pa.getOptions();
-            if (pprops == null) continue;
-            String phost = pprops.getProperty("host");
+            String phost = pa.getOption("host");
             if (phost == null) continue;
             InetAddress pi;
             try {
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index ae4bfe224cbba5047637b3b243c4b469d60a87b9..8b6b29713d63307d6445fe041acc94b98a3a526e 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -343,15 +343,12 @@ public class TransportManager implements TransportEventListener {
         for (Transport t : _transports.values()) {
             int port = t.getRequestedPort();
             if (t.getCurrentAddress() != null) {
-                Properties opts = t.getCurrentAddress().getOptions();
-                if (opts != null) {
-                    String s = opts.getProperty("port");
+                    String s = t.getCurrentAddress().getOption("port");
                     if (s != null) {
                         try {
                             port = Integer.parseInt(s);
                         } catch (NumberFormatException nfe) {}
                     }
-                }
             }
             // Use UDP port for NTCP too - see comment in NTCPTransport.getRequestedPort() for why this is here
             if (t.getStyle().equals(NTCPTransport.STYLE) && port <= 0 &&
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java
index e13bcc31c7f3c881a675c67cb949323dd3673269..acced89f433a2cbd9fbf5687ef5bba52d1021f5d 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPAddress.java
@@ -57,13 +57,13 @@ public class NTCPAddress {
             _port = -1;
             return;
         }
-        String host = addr.getOptions().getProperty(PROP_HOST);
+        String host = addr.getOption(PROP_HOST);
         if (host == null) {
             _host = null;
             _port = -1;
         } else { 
             _host = host.trim();
-            String port = addr.getOptions().getProperty(PROP_PORT);
+            String port = addr.getOption(PROP_PORT);
             if ( (port != null) && (port.trim().length() > 0) && !("null".equals(port)) ) {
                 try {
                     _port = Integer.parseInt(port.trim());
@@ -156,9 +156,7 @@ public class NTCPAddress {
     
     public boolean equals(RouterAddress addr) {
         if (addr == null) return false;
-        Properties opts = addr.getOptions();
-        if (opts == null) return false;
-        return ( (_host.equals(opts.getProperty(PROP_HOST))) &&
-                 (Integer.toString(_port).equals(opts.getProperty(PROP_PORT))) );
+        return ( (_host.equals(addr.getOption(PROP_HOST))) &&
+                 (Integer.toString(_port).equals(addr.getOption(PROP_PORT))) );
     }
 }
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
index bcee07605e44a222faed26f1fc4c4b3b7f957d29..7fa6060db9ca148091ecfb90c24c11418494b35e 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
@@ -64,31 +64,30 @@ public class UDPAddress {
     
     private void parse(RouterAddress addr) {
         if (addr == null) return;
-        Properties opts = addr.getOptions();
-        _host = opts.getProperty(PROP_HOST);
+        _host = addr.getOption(PROP_HOST);
         if (_host != null) _host = _host.trim();
         try { 
-            String port = opts.getProperty(PROP_PORT);
+            String port = addr.getOption(PROP_PORT);
             if (port != null)
                 _port = Integer.parseInt(port);
         } catch (NumberFormatException nfe) {
             _port = -1;
         }
-        String key = opts.getProperty(PROP_INTRO_KEY);
+        String key = addr.getOption(PROP_INTRO_KEY);
         if (key != null)
             _introKey = Base64.decode(key.trim());
         
         for (int i = MAX_INTRODUCERS; i >= 0; i--) {
-            String host = opts.getProperty(PROP_INTRO_HOST_PREFIX + i);
+            String host = addr.getOption(PROP_INTRO_HOST_PREFIX + i);
             if (host == null) continue;
-            String port = opts.getProperty(PROP_INTRO_PORT_PREFIX + i);
+            String port = addr.getOption(PROP_INTRO_PORT_PREFIX + i);
             if (port == null) continue;
-            String k = opts.getProperty(PROP_INTRO_KEY_PREFIX + i);
+            String k = addr.getOption(PROP_INTRO_KEY_PREFIX + i);
             if (k == null) continue;
             byte ikey[] = Base64.decode(k);
             if ( (ikey == null) || (ikey.length != SessionKey.KEYSIZE_BYTES) )
                 continue;
-            String t = opts.getProperty(PROP_INTRO_TAG_PREFIX + i);
+            String t = addr.getOption(PROP_INTRO_TAG_PREFIX + i);
             if (t == null) continue;
             int p = -1;
             try {