diff --git a/LICENSE.txt b/LICENSE.txt
index 11dd51e38f7f5dc9fd1b9080e109b645ee1cce7e..7681ce65aedfa53a0b5bb46c597783727fd889ad 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -182,7 +182,7 @@ Applications:
    By welterde.
    See licenses/LICENSE-GPLv2.txt
 
-   Jetty 8.1.15.v20140411:
+   Jetty 8.1.16.v20140903:
    See licenses/ABOUT-Jetty.html
    See licenses/NOTICE-Jetty.html
    See licenses/LICENSE-Apache2.0.txt
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
index 47e9bb0fbb58d7a11fc9eec022af6fa9e99024eb..5fda52940222d29180a0ff9445acfa0d97c37d0f 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
@@ -64,10 +64,11 @@ class ConfigParser {
         if (inputLine.startsWith(";")) {
             return "";
         }
-        if (inputLine.split("#").length > 0) {
-            return inputLine.split("#")[0];
+        int hash = inputLine.indexOf('#');
+        if (hash >= 0) {
+            return inputLine.substring(0, hash);
         } else {
-            return "";
+            return inputLine;
         }
     }
 
diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index a4fd04c06393fa91f2b1708aa0ce3ce5f8fec478..5a4d3dd2496f1ba78bad21587eadecb060edf102 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -100,15 +100,15 @@
     <target name="war" depends="jar, bundle, warUpToDate, listChangedFiles" unless="war.uptodate" > 
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
-        <copy todir="build/icons/.icons" >
-            <fileset dir="../icons/" />
+        <copy todir="build/resources/.resources" >
+            <fileset dir="../resources/" />
         </copy>
         <!-- mime.properties must be in with the classes -->
         <copy file="../mime.properties" todir="build/obj/org/klomp/snark/web" />
         <war destfile="../i2psnark.war" webxml="../web.xml" >
           <!-- include only the web stuff, as of 0.7.12 the router will add i2psnark.jar to the classpath for the war -->
           <classes dir="./build/obj" includes="**/web/*" />
-            <fileset dir="build/icons/" />
+            <fileset dir="build/resources/" />
             <manifest>
                 <attribute name="Implementation-Version" value="${full.version}" />
                 <attribute name="Built-By" value="${build.built-by}" />
@@ -121,7 +121,7 @@
     
     <target name="warUpToDate">
         <uptodate property="war.uptodate" targetfile="../i2psnark.war" >
-            <srcfiles dir= "." includes="build/obj/org/klomp/snark/web/*.class ../icons/* ../web.xml" />
+            <srcfiles dir= "." includes="build/obj/org/klomp/snark/web/*.class ../resources/**/* ../web.xml" />
         </uptodate>
     </target>
     
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
index ab15bb85ca070b7674b6aa46de1a8689be73a160..a7c4f4127a714e92942bd77d3fa8d6a99e7b2107 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
@@ -57,8 +57,8 @@ public class Peer implements Comparable<Peer>
   private DataOutputStream dout;
 
   /** running counters */
-  private long downloaded;
-  private long uploaded;
+  private final AtomicLong downloaded = new AtomicLong();
+  private final AtomicLong uploaded = new AtomicLong();
 
   // Keeps state for in/out connections.  Non-null when the handshake
   // was successful, the connection setup and runs
@@ -618,7 +618,7 @@ public class Peer implements Comparable<Peer>
    * @since 0.8.4
    */
   public void downloaded(int size) {
-      downloaded += size;
+      downloaded.addAndGet(size);
   }
 
   /**
@@ -626,7 +626,7 @@ public class Peer implements Comparable<Peer>
    * @since 0.8.4
    */
   public void uploaded(int size) {
-      uploaded += size;
+      uploaded.addAndGet(size);
   }
 
   /**
@@ -635,7 +635,7 @@ public class Peer implements Comparable<Peer>
    */
   public long getDownloaded()
   {
-      return downloaded;
+      return downloaded.get();
   }
 
   /**
@@ -644,7 +644,7 @@ public class Peer implements Comparable<Peer>
    */
   public long getUploaded()
   {
-      return uploaded;
+      return uploaded.get();
   }
 
   /**
@@ -652,8 +652,8 @@ public class Peer implements Comparable<Peer>
    */
   public void resetCounters()
   {
-      downloaded = 0;
-      uploaded = 0;
+      downloaded.set(0);
+      uploaded.set(0);
   }
   
   public long getInactiveTime() {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index 740b7c2be406ff475e001348864b2a388b36d80f..2bb841329503574adecd0b62c78065ad45815de9 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -27,7 +27,6 @@ import java.io.InputStream;
 import java.util.Collections;
 import java.util.List;
 import java.util.Properties;
-import java.util.Random;
 import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
@@ -245,16 +244,19 @@ public class Snark
    *
    * @deprecated unused
    */
+/****
   Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
         StorageListener slistener, CoordinatorListener clistener) { 
     this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); 
   }
+****/
 
   /**
    * single torrent - via router
    *
    * @deprecated unused
    */
+/****
   public Snark(I2PAppContext ctx, Properties opts, String torrent,
                StorageListener slistener, boolean start, String rootDir) { 
     this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
@@ -284,6 +286,7 @@ public class Snark
     if (start)
         this.startTorrent();
   }
+****/
 
   /**
    * multitorrent
@@ -515,18 +518,13 @@ public class Snark
 
     // Create a new ID and fill it with something random.  First nine
     // zeros bytes, then three bytes filled with snark and then
-    // sixteen random bytes.
+    // eight random bytes.
     byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
     byte[] rv = new byte[20];
-    Random random = I2PAppContext.getGlobalContext().random();
-    int i;
-    for (i = 0; i < 9; i++)
-      rv[i] = 0;
-    rv[i++] = snark;
-    rv[i++] = snark;
-    rv[i++] = snark;
-    while (i < 20)
-      rv[i++] = (byte)random.nextInt(256);
+    rv[9] = snark;
+    rv[10] = snark;
+    rv[11] = snark;
+    I2PAppContext.getGlobalContext().random().nextBytes(rv, 12, 8);
     return rv;
   }
 
@@ -958,6 +956,7 @@ public class Snark
    * non-valid argument list.  The given listeners will be
    * passed to all components that take one.
    */
+/****
   private static Snark parseArguments(String[] args,
                               StorageListener slistener,
                               CoordinatorListener clistener)
@@ -972,6 +971,7 @@ public class Snark
     int i = 0;
     while (i < args.length)
       {
+****/
 /*
         if (args[i].equals("--debug"))
           {
@@ -993,7 +993,9 @@ public class Snark
                 catch (NumberFormatException nfe) { }
               }
           }
-        else */ if (args[i].equals("--port"))
+        else */
+/****
+          if (args[i].equals("--port"))
           {
             if (args.length - 1 < i + 1)
               usage("--port needs port number to listen on");
@@ -1099,6 +1101,7 @@ public class Snark
     System.out.println
       ("         \tor (with --share) a file to share.");
   }
+****/
 
   /**
    * Aborts program abnormally.
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 73f273c6a928ba8b8f6d7e3a8a6d417a8b59ed7f..cb50fb6fbeb84ac8140acf1592daca3fcee38d6b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -600,10 +600,10 @@ public class SnarkManager implements CompleteListener {
 
     /**
      * Get all themes
-     * @return String[] -- Array of all the themes found.
+     * @return String[] -- Array of all the themes found, non-null, unsorted
      */
     public String[] getThemes() {
-            String[] themes = null;
+            String[] themes;
             // "docs/themes/snark/"
             File dir = new File(_context.getBaseDir(), "docs/themes/snark");
             FileFilter fileFilter = new FileFilter() { public boolean accept(File file) { return file.isDirectory(); } };
@@ -614,6 +614,8 @@ public class SnarkManager implements CompleteListener {
                 for(int i = 0; i < dirnames.length; i++) {
                     themes[i] = dirnames[i].getName();
                 }
+            } else {
+                themes = new String[0];
             }
             // return the map.
             return themes;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index a984f0ce8183b7fbdafdec33016d0c3ded2e5eb7..f1c6658c9463ef4b508d242fea2a317547f7c98b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -61,7 +61,7 @@ public class I2PSnarkServlet extends BasicServlet {
     
     private static final String DEFAULT_NAME = "i2psnark";
     public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
-    private static final String WARBASE = "/.icons/";
+    private static final String WARBASE = "/.resources/";
     private static final char HELLIP = '\u2026';
  
     public I2PSnarkServlet() {
@@ -191,31 +191,14 @@ public class I2PSnarkServlet extends BasicServlet {
 
         _themePath = "/themes/snark/" + _manager.getTheme() + '/';
         _imgPath = _themePath + "images/";
-        resp.setHeader("X-Frame-Options", "SAMEORIGIN");
-        resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'");
-        resp.setHeader("X-XSS-Protection", "1; mode=block");
+        req.setCharacterEncoding("UTF-8");
 
-        String peerParam = req.getParameter("p");
-        String stParam = req.getParameter("st");
-        String peerString;
-        if (peerParam == null || (!_manager.util().connected()) ||
-            peerParam.replaceAll("[a-zA-Z0-9~=-]", "").length() > 0) {  // XSS
-            peerString = "";
-        } else {
-            peerString = "?p=" + DataHelper.stripHTML(peerParam);
-        }
-        if (stParam != null && !stParam.equals("0")) {
-            stParam = DataHelper.stripHTML(stParam);
-            if (peerString.length() > 0)
-                peerString += "&amp;st=" + stParam;
-            else
-                peerString =  "?st="+ stParam;
-        }
+        String pOverride = _manager.util().connected() ? null : "";
+        String peerString = getQueryString(req, pOverride, null, null);
 
         // AJAX for mainsection
         if ("/.ajax/xhr1.html".equals(path)) {
-            resp.setCharacterEncoding("UTF-8");
-            resp.setContentType("text/html; charset=UTF-8");
+            setHTMLHeaders(resp);
             PrintWriter out = resp.getWriter();
             //if (_log.shouldLog(Log.DEBUG))
             //    _manager.addMessage((_context.clock().now() / 1000) + " xhr1 p=" + req.getParameter("p"));
@@ -233,19 +216,18 @@ public class I2PSnarkServlet extends BasicServlet {
                 // bypass the horrid Resource.getListHTML()
                 String pathInfo = req.getPathInfo();
                 String pathInContext = addPaths(path, pathInfo);
-                req.setCharacterEncoding("UTF-8");
-                resp.setCharacterEncoding("UTF-8");
-                resp.setContentType("text/html; charset=UTF-8");
                 File resource = getResource(pathInContext);
                 if (resource == null) {
                     resp.sendError(404);
                 } else {
                     String base = addPaths(req.getRequestURI(), "/");
-                    String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null);
+                    String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null,
+                                                 req.getParameter("sort"));
                     if (method.equals("POST")) {
                         // P-R-G
                         sendRedirect(req, resp, "");
                     } else if (listing != null) {
+                        setHTMLHeaders(resp);
                         resp.getWriter().write(listing);
                     } else { // shouldn't happen
                         resp.sendError(404);
@@ -265,10 +247,6 @@ public class I2PSnarkServlet extends BasicServlet {
 
         // Either the main page or /configure
 
-        req.setCharacterEncoding("UTF-8");
-        resp.setCharacterEncoding("UTF-8");
-        resp.setContentType("text/html; charset=UTF-8");
-        
         String nonce = req.getParameter("nonce");
         if (nonce != null) {
             if (nonce.equals(String.valueOf(_nonce)))
@@ -280,6 +258,7 @@ public class I2PSnarkServlet extends BasicServlet {
             return;	
         }
         
+        setHTMLHeaders(resp);
         PrintWriter out = resp.getWriter();
         out.write(DOCTYPE + "<html>\n" +
                   "<head><link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\n" +
@@ -293,6 +272,7 @@ public class I2PSnarkServlet extends BasicServlet {
             out.write(_("Configuration"));
         else
             out.write(_("Anonymous BitTorrent Client"));
+        String peerParam = req.getParameter("p");
         if ("2".equals(peerParam))
             out.write(" | Debug Mode");
         out.write("</title>\n");
@@ -324,7 +304,8 @@ public class I2PSnarkServlet extends BasicServlet {
             out.write("<div class=\"snarknavbar\"><a href=\"" + _contextPath + "/\" title=\"");
             out.write(_("Torrents"));
             out.write("\" class=\"snarkRefresh\">");
-            out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "arrow_refresh.png\">&nbsp;&nbsp;");
+            out.write(toThemeImg("arrow_refresh"));
+            out.write(">&nbsp;&nbsp;");
             if (_contextName.equals(DEFAULT_NAME))
                 out.write(_("I2PSnark"));
             else
@@ -334,7 +315,8 @@ public class I2PSnarkServlet extends BasicServlet {
             out.write("<div class=\"snarknavbar\"><a href=\"" + _contextPath + '/' + peerString + "\" title=\"");
             out.write(_("Refresh page"));
             out.write("\" class=\"snarkRefresh\">");
-            out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "arrow_refresh.png\">&nbsp;&nbsp;");
+            out.write(toThemeImg("arrow_refresh"));
+            out.write(">&nbsp;&nbsp;");
             if (_contextName.equals(DEFAULT_NAME))
                 out.write(_("I2PSnark"));
             else
@@ -378,6 +360,22 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write(FOOTER);
     }
 
+    /**
+     *  The standard HTTP headers for all HTML pages
+     *
+     *  @since 0.9.16 moved from doGetAndPost()
+     */
+    private static void setHTMLHeaders(HttpServletResponse resp) {
+        resp.setCharacterEncoding("UTF-8");
+        resp.setContentType("text/html; charset=UTF-8");
+        resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate");
+        resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'");
+        resp.setDateHeader("Expires", 0);
+        resp.setHeader("Pragma", "no-cache");
+        resp.setHeader("X-Frame-Options", "SAMEORIGIN");
+        resp.setHeader("X-XSS-Protection", "1; mode=block");
+    }
+
     private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException {
         List<String> msgs = _manager.getMessages();
         if (!msgs.isEmpty()) {
@@ -389,9 +387,10 @@ public class I2PSnarkServlet extends BasicServlet {
                 out.write(peerString + "&amp;");
             else
                 out.write("?");
-            out.write("action=Clear&amp;nonce=" + _nonce + "\">" +
-                      "<img src=\"" + _imgPath + "delete.png\" title=\"" + _("clear messages") +
-                      "\" alt=\"" + _("clear messages") + "\"></a>" +
+            out.write("action=Clear&amp;nonce=" + _nonce + "\">");
+            String tx = _("clear messages");
+            out.write(toThemeImg("delete", tx, tx));
+            out.write("</a>" +
                       "<ul>");
             for (int i = msgs.size()-1; i >= 0; i--) {
                 String msg = msgs.get(i);
@@ -414,13 +413,7 @@ public class I2PSnarkServlet extends BasicServlet {
         boolean isForm = _manager.util().connected() || !snarks.isEmpty();
         if (isForm) {
             out.write("<form action=\"_post\" method=\"POST\">\n");
-            out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n");
-            // don't lose peer setting
-            if (peerParam != null)
-                out.write("<input type=\"hidden\" name=\"p\" value=\"" + peerParam + "\" >\n");
-            // ...or st setting
-            if (stParam != null)
-                out.write("<input type=\"hidden\" name=\"st\" value=\"" + stParam + "\" >\n");
+            writeHiddenInputs(out, req, null);
         }
         out.write(TABLE_HEADER);
 
@@ -442,90 +435,166 @@ public class I2PSnarkServlet extends BasicServlet {
         }
         int pageSize = Math.max(_manager.getPageSize(), 5);
 
-        out.write("<tr><th><img border=\"0\" src=\"" + _imgPath + "status.png\" title=\"");
-        out.write(_("Status"));
-        out.write("\" alt=\"");
-        out.write(_("Status"));
-        out.write("\"></th>\n<th>");
+        String currentSort = req.getParameter("sort");
+        boolean showSort = total > 1;
+        out.write("<tr><th>");
+        String sort = ("2".equals(currentSort)) ? "-2" : "2";
+        if (showSort) {
+            out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+            out.write("\">");
+        }
+        String tx = _("Status");
+        out.write(toThemeImg("status", tx,
+                             showSort ? _("Sort by {0}", tx)
+                                      : tx));
+        if (showSort)
+            out.write("</a>");
+        out.write("</th>\n<th>");
         if (_manager.util().connected() && !snarks.isEmpty()) {
             out.write(" <a href=\"" + _contextPath + '/');
             if (peerParam != null) {
-                if (stParam != null) {
-                    out.write("?st=");
-                    out.write(stParam);
-                }
-                out.write("\">");
-                out.write("<img border=\"0\" src=\"" + _imgPath + "hidepeers.png\" title=\"");
-                out.write(_("Hide Peers"));
-                out.write("\" alt=\"");
-                out.write(_("Hide Peers"));
+                // disable peer view
                 out.write("\">");
+                tx = _("Hide Peers");
+                out.write(toThemeImg("hidepeers", tx, tx));
             } else {
-                out.write("?p=1");
-                if (stParam != null) {
-                    out.write("&amp;st=");
-                    out.write(stParam);
-                }
-                out.write("\">");
-                out.write("<img border=\"0\" src=\"" + _imgPath + "showpeers.png\" title=\"");
-                out.write(_("Show Peers"));
-                out.write("\" alt=\"");
-                out.write(_("Show Peers"));
+                // enable peer view
+                out.write(getQueryString(req, "1", null, null));
                 out.write("\">");
+                tx = _("Show Peers");
+                out.write(toThemeImg("showpeers", tx, tx));
             }
             out.write("</a><br>\n"); 
         }
         out.write("</th>\n<th colspan=\"2\" align=\"left\">");
-        out.write("<img border=\"0\" src=\"" + _imgPath + "torrent.png\" title=\"");
-        out.write(_("Torrent"));
-        out.write("\" alt=\"");
-        out.write(_("Torrent"));
-        out.write("\"></th>\n<th align=\"center\">");
+        // cycle through sort by name or type
+        boolean isTypeSort = false;
+        if (showSort) {
+            if (currentSort == null || "0".equals(currentSort) || "1".equals(currentSort)) {
+                sort = "-1";
+            } else if ("-1".equals(currentSort)) {
+                sort = "12";
+                isTypeSort = true;
+            } else if ("12".equals(currentSort)) {
+                sort = "-12";
+                isTypeSort = true;
+            } else {
+                sort = "";
+            }
+            out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+            out.write("\">");
+        }
+        tx = _("Torrent");
+        out.write(toThemeImg("torrent", tx,
+                             showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : tx))
+                                      : tx));
+        if (showSort)
+            out.write("</a>");
+        out.write("</th>\n<th align=\"center\">");
         if (total > 0 && (start > 0 || total > pageSize)) {
-            writePageNav(out, start, pageSize, total, peerParam, noThinsp);
+            writePageNav(out, req, start, pageSize, total, noThinsp);
         }
         out.write("</th>\n<th align=\"right\">");
         if (_manager.util().connected() && !snarks.isEmpty()) {
-            out.write("<img border=\"0\" src=\"" + _imgPath + "eta.png\" title=\"");
-            out.write(_("Estimated time remaining"));
-            out.write("\" alt=\"");
+            if (showSort) {
+                sort = ("4".equals(currentSort)) ? "-4" : "4";
+                out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+                out.write("\">");
+            }
             // Translators: Please keep short or translate as " "
-            out.write(_("ETA"));
-            out.write("\">");
+            tx = _("ETA");
+            out.write(toThemeImg("eta", tx,
+                                 showSort ? _("Sort by {0}", _("Estimated time remaining"))
+                                          : _("Estimated time remaining")));
+            if (showSort)
+                out.write("</a>");
         }
         out.write("</th>\n<th align=\"right\">");
-        out.write("<img border=\"0\" src=\"" + _imgPath + "head_rx.png\" title=\"");
-        out.write(_("Downloaded"));
-        out.write("\" alt=\"");
+        // cycle through sort by size or downloaded
+        boolean isDlSort = false;
+        if (showSort) {
+            if ("5".equals(currentSort)) {
+                sort = "-5";
+            } else if ("-5".equals(currentSort)) {
+                sort = "6";
+                isDlSort = true;
+            } else if ("6".equals(currentSort)) {
+                sort = "-6";
+                isDlSort = true;
+            } else {
+                sort = "5";
+            }
+            out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+            out.write("\">");
+        }
         // Translators: Please keep short or translate as " "
-        out.write(_("RX"));
-        out.write("\">");
+        tx = _("RX");
+        out.write(toThemeImg("head_rx", tx,
+                             showSort ? _("Sort by {0}", (isDlSort ? _("Downloaded") : _("Size")))
+                                      : _("Downloaded")));
+        if (showSort)
+            out.write("</a>");
         out.write("</th>\n<th align=\"right\">");
+        boolean isRatSort = false;
         if (!snarks.isEmpty()) {
-            out.write("<img border=\"0\" src=\"" + _imgPath + "head_tx.png\" title=\"");
-            out.write(_("Uploaded"));
-            out.write("\" alt=\"");
+            // cycle through sort by uploaded or ratio
+            boolean nextRatSort = false;
+            if (showSort) {
+                if ("7".equals(currentSort)) {
+                    sort = "-7";
+                } else if ("-7".equals(currentSort)) {
+                    sort = "11";
+                    nextRatSort = true;
+                } else if ("11".equals(currentSort)) {
+                    sort = "-11";
+                    nextRatSort = true;
+                    isRatSort = true;
+                } else if ("-11".equals(currentSort)) {
+                    sort = "7";
+                    isRatSort = true;
+                } else {
+                    sort = "7";
+                }
+                out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+                out.write("\">");
+            }
             // Translators: Please keep short or translate as " "
-            out.write(_("TX"));
-            out.write("\">");
+            tx = _("TX");
+            out.write(toThemeImg("head_tx", tx,
+                                 showSort ? _("Sort by {0}", (nextRatSort ? _("Upload ratio") : _("Uploaded")))
+                                          : _("Uploaded")));
+            if (showSort)
+                out.write("</a>");
         }
         out.write("</th>\n<th align=\"right\">");
         if (_manager.util().connected() && !snarks.isEmpty()) {
-            out.write("<img border=\"0\" src=\"" + _imgPath + "head_rxspeed.png\" title=\"");
-            out.write(_("Down Rate"));
-            out.write("\" alt=\"");
+            if (showSort) {
+                sort = ("8".equals(currentSort)) ? "-8" : "8";
+                out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+                out.write("\">");
+            }
             // Translators: Please keep short or translate as " "
-            out.write(_("RX Rate"));
-            out.write(" \">");
+            tx = _("RX Rate");
+            out.write(toThemeImg("head_rxspeed", tx,
+                                 showSort ? _("Sort by {0}", _("Down Rate"))
+                                          : _("Down Rate")));
+            if (showSort)
+                out.write("</a>");
         }
         out.write("</th>\n<th align=\"right\">");
         if (_manager.util().connected() && !snarks.isEmpty()) {
-            out.write("<img border=\"0\" src=\"" + _imgPath + "head_txspeed.png\" title=\"");
-            out.write(_("Up Rate"));
-            out.write("\" alt=\"");
+            if (showSort) {
+                sort = ("9".equals(currentSort)) ? "-9" : "9";
+                out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
+                out.write("\">");
+            }
             // Translators: Please keep short or translate as " "
-            out.write(_("TX Rate"));
-            out.write(" \">");
+            tx = _("TX Rate");
+            out.write(toThemeImg("head_txspeed", tx,
+                                 showSort ? _("Sort by {0}", _("Up Rate"))
+                                          : _("Up Rate")));
+            if (showSort)
+                out.write("</a>");
         }
         out.write("</th>\n<th align=\"center\">");
 
@@ -581,12 +650,11 @@ public class I2PSnarkServlet extends BasicServlet {
         String uri = _contextPath + '/';
         boolean showDebug = "2".equals(peerParam);
 
-        String stParamStr = stParam == null ? "" : "&amp;st=" + stParam;
         for (int i = 0; i < total; i++) {
             Snark snark = snarks.get(i);
             boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam);
             boolean hide = i < start || i >= start + pageSize;
-            displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, stParamStr);
+            displaySnark(out, req, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, isRatSort);
         }
 
         if (total == 0) {
@@ -637,32 +705,118 @@ public class I2PSnarkServlet extends BasicServlet {
         return start == 0;
     }
     
+    /**
+     *  hidden inputs for nonce and paramters p, st, and sort
+     *
+     *  @param out writes to it
+     *  @param action if non-null, add it as the action
+     *  @since 0.9.16
+     */
+    private void writeHiddenInputs(PrintWriter out, HttpServletRequest req, String action) {
+        StringBuilder buf = new StringBuilder(256);
+        writeHiddenInputs(buf, req, action);
+        out.write(buf.toString());
+    }
+    
+    /**
+     *  hidden inputs for nonce and paramters p, st, and sort
+     *
+     *  @param out appends to it
+     *  @param action if non-null, add it as the action
+     *  @since 0.9.16
+     */
+    private void writeHiddenInputs(StringBuilder buf, HttpServletRequest req, String action) {
+        buf.append("<input type=\"hidden\" name=\"nonce\" value=\"")
+           .append(_nonce).append("\" >\n");
+        String peerParam = req.getParameter("p");
+        if (peerParam != null) {
+            buf.append("<input type=\"hidden\" name=\"p\" value=\"")
+               .append(DataHelper.stripHTML(peerParam)).append("\" >\n");
+        }
+        String stParam = req.getParameter("st");
+        if (stParam != null) {
+            buf.append("<input type=\"hidden\" name=\"st\" value=\"")
+               .append(DataHelper.stripHTML(stParam)).append("\" >\n");
+        }
+        String soParam = req.getParameter("sort");
+        if (soParam != null) {
+            buf.append("<input type=\"hidden\" name=\"sort\" value=\"")
+               .append(DataHelper.stripHTML(soParam)).append("\" >\n");
+        }
+        if (action != null) {
+            buf.append("<input type=\"hidden\" name=\"action\" value=\"")
+               .append(action).append("\" >\n");
+        }
+    }
+    
+    /**
+     *  Build HTML-escaped and stripped query string
+     *
+     *  @param p override or "" for default or null to keep the same as in req
+     *  @param st override or "" for default or null to keep the same as in req
+     *  @param so override or "" for default or null to keep the same as in req
+     *  @return non-null, possibly empty
+     *  @since 0.9.16
+     */
+    private static String getQueryString(HttpServletRequest req, String p, String st, String so) {
+        StringBuilder buf = new StringBuilder(64);
+        if (p == null) {
+            p = req.getParameter("p");
+            if (p != null)
+                p = DataHelper.stripHTML(p);
+        }
+        if (p != null && !p.equals(""))
+            buf.append("?p=").append(p);
+        if (so == null) {
+            so = req.getParameter("sort");
+            if (so != null)
+                so = DataHelper.stripHTML(so);
+        }
+        if (so != null && !so.equals("")) {
+            if (buf.length() <= 0)
+                buf.append("?sort=");
+            else
+                buf.append("&amp;sort=");
+            buf.append(so);
+        }
+        if (st == null) {
+            st = req.getParameter("st");
+            if (st != null)
+                st = DataHelper.stripHTML(st);
+        }
+        if (st != null && !st.equals("")) {
+            if (buf.length() <= 0)
+                buf.append("?st=");
+            else
+                buf.append("&amp;st=");
+            buf.append(st);
+        }
+        return buf.toString();
+    }
+    
     /**
      *  @since 0.9.6
      */
-    private void writePageNav(PrintWriter out, int start, int pageSize, int total,
-                              String peerParam, boolean noThinsp) {
+    private void writePageNav(PrintWriter out, HttpServletRequest req, int start, int pageSize, int total,
+                              boolean noThinsp) {
             // Page nav
             if (start > 0) {
                 // First
                 out.write("<a href=\"" + _contextPath);
-                if (peerParam != null)
-                    out.write("?p=" + peerParam);
-                out.write("\">" +
-                          "<img alt=\"" + _("First") + "\" title=\"" + _("First page") + "\" border=\"0\" src=\"" +
-                          _imgPath + "control_rewind_blue.png\">" +
-                          "</a>&nbsp;");
+                out.write(getQueryString(req, null, "", null));
+                out.write("\">");
+                out.write(toThemeImg("control_rewind_blue", _("First"), _("First page")));
+                out.write("</a>&nbsp;");
                 int prev = Math.max(0, start - pageSize);
                 //if (prev > 0) {
                 if (true) {
                     // Back
-                    out.write("&nbsp;<a href=\"" + _contextPath +  "?st=" + prev);
-                    if (peerParam != null)
-                        out.write("&amp;p=" + peerParam);
-                    out.write("\">" +
-                          "<img alt=\"" + _("Prev") + "\" title=\"" + _("Previous page") + "\" border=\"0\" src=\"" +
-                          _imgPath + "control_back_blue.png\">" +
-                          "</a>&nbsp;");
+                    out.write("&nbsp;<a href=\"" + _contextPath);
+                    String sprev = (prev > 0) ? Integer.toString(prev) : "";
+                    out.write(getQueryString(req, null, sprev, null));
+                    out.write("\">");
+                    out.write(toThemeImg("control_back_blue", _("Prev"), _("Previous page")));
+                    out.write("</a>&nbsp;");
                 }
             } else {
                 out.write(
@@ -691,23 +845,19 @@ public class I2PSnarkServlet extends BasicServlet {
                 //if (next + pageSize < total) {
                 if (true) {
                     // Next
-                    out.write("&nbsp;<a href=\"" + _contextPath +  "?st=" + next);
-                    if (peerParam != null)
-                        out.write("&amp;p=" + peerParam);
-                    out.write("\">" +
-                          "<img alt=\"" + _("Next") + "\" title=\"" + _("Next page") + "\" border=\"0\" src=\"" +
-                          _imgPath + "control_play_blue.png\">" +
-                          "</a>&nbsp;");
+                    out.write("&nbsp;<a href=\"" + _contextPath);
+                    out.write(getQueryString(req, null, Integer.toString(next), null));
+                    out.write("\">");
+                    out.write(toThemeImg("control_play_blue", _("Next"), _("Next page")));
+                    out.write("</a>&nbsp;");
                 }
                 // Last
                 int last = ((total - 1) / pageSize) * pageSize;
-                out.write("&nbsp;<a href=\"" + _contextPath +  "?st=" + last);
-                if (peerParam != null)
-                    out.write("&amp;p=" + peerParam);
-                out.write("\">" +
-                          "<img alt=\"" + _("Last") + "\" title=\"" + _("Last page") + "\" border=\"0\" src=\"" +
-                          _imgPath + "control_fastforward_blue.png\">" +
-                          "</a>&nbsp;");
+                out.write("&nbsp;<a href=\"" + _contextPath);
+                out.write(getQueryString(req, null, Integer.toString(last), null));
+                out.write("\">");
+                out.write(toThemeImg("control_fastforward_blue", _("Last"), _("Last page")));
+                out.write("</a>&nbsp;");
             } else {
                 out.write("&nbsp;" +
                           "<img alt=\"\" border=\"0\" class=\"disable\" src=\"" +
@@ -1111,10 +1261,13 @@ public class I2PSnarkServlet extends BasicServlet {
                         _manager.addMessage(_("Removed") + ": " + DataHelper.stripHTML(k));
                         changed = true;
                      }
-                } else if (k.startsWith("open_")) {
-                     open.add(k.substring(5));
-                } else if (k.startsWith("private_")) {
-                     priv.add(k.substring(8));
+                } else if (k.startsWith("ttype_")) {
+                     String val = req.getParameter(k);
+                     k = k.substring(6);
+                     if ("1".equals(val))
+                         open.add(k);
+                     else if ("2".equals(val))
+                         priv.add(k);
                 }
             }
             if (changed) {
@@ -1149,12 +1302,12 @@ public class I2PSnarkServlet extends BasicServlet {
                     Map<String, Tracker> trackers = _manager.getTrackerMap();
                     trackers.put(name, new Tracker(name, aurl, hurl));
                     _manager.saveTrackerMap();
-                    // open trumps private
-                    if (req.getParameter("_add_open_") != null) {
+                    String type = req.getParameter("add_tracker_type");
+                    if ("1".equals(type)) {
                         List<String> newOpen = new ArrayList<String>(_manager.util().getOpenTrackers());
                         newOpen.add(aurl);
                         _manager.saveOpenTrackers(newOpen);
-                    } else if (req.getParameter("_add_private_") != null) {
+                    } else if ("2".equals(type)) {
                         List<String> newPriv = new ArrayList<String>(_manager.getPrivateTrackers());
                         newPriv.add(aurl);
                         _manager.savePrivateTrackers(newPriv);
@@ -1191,34 +1344,22 @@ public class I2PSnarkServlet extends BasicServlet {
         return buf.toString();
     }
 
-    /**
-     *  Sort alphabetically in current locale, ignore case, ignore leading "the "
-     *  (I guess this is worth it, a lot of torrents start with "The "
-     *  @since 0.7.14
-     */
-    private static class TorrentNameComparator implements Comparator<Snark>, Serializable {
-
-        public int compare(Snark l, Snark r) {
-            // put downloads and magnets first
-            if (l.getStorage() == null && r.getStorage() != null)
-                return -1;
-            if (l.getStorage() != null && r.getStorage() == null)
-                return 1;
-            String ls = l.getBaseName();
-            String llc = ls.toLowerCase(Locale.US);
-            if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_"))
-                ls = ls.substring(4);
-            String rs = r.getBaseName();
-            String rlc = rs.toLowerCase(Locale.US);
-            if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_"))
-                rs = rs.substring(4);
-            return Collator.getInstance().compare(ls, rs);
-        }
-    }
-
     private List<Snark> getSortedSnarks(HttpServletRequest req) {
         ArrayList<Snark> rv = new ArrayList<Snark>(_manager.getTorrents());
-        Collections.sort(rv, new TorrentNameComparator());
+        if (rv.size() > 1) {
+            int sort = 0;
+            String ssort = req.getParameter("sort");
+            if (ssort != null) {
+                try {
+                    sort = Integer.parseInt(ssort);
+                } catch (NumberFormatException nfe) {}
+            }
+            try {
+                Collections.sort(rv, Sorters.getComparator(sort, this));
+            } catch (IllegalArgumentException iae) {
+                // Java 7 TimSort - may be unstable
+            }
+        }
         return rv;
     }
 
@@ -1230,11 +1371,11 @@ public class I2PSnarkServlet extends BasicServlet {
      *
      *  @param stats in/out param (totals)
      *  @param statsOnly if true, output nothing, update stats only
-     *  @param stParam non null; empty or e.g. &amp;st=10
      */
-    private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers,
+    private void displaySnark(PrintWriter out, HttpServletRequest req,
+                              Snark snark, String uri, int row, long stats[], boolean showPeers,
                               boolean isDegraded, boolean noThinsp, boolean showDebug, boolean statsOnly,
-                              String stParam) throws IOException {
+                              boolean showRatios) throws IOException {
         // stats
         long uploaded = snark.getUploaded();
         stats[0] += snark.getDownloaded();
@@ -1288,10 +1429,10 @@ public class I2PSnarkServlet extends BasicServlet {
         String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
         String statusString;
         if (snark.isChecking()) {
-            statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Checking") + "\"></td>" +
+            statusString = toThemeImg("stalled", "", _("Checking")) + "</td>" +
                            "<td class=\"snarkTorrentStatus\">" + _("Checking");
         } else if (snark.isAllocating()) {
-            statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Allocating") + "\"></td>" +
+            statusString = toThemeImg("stalled", "", _("Allocating")) + "</td>" +
                            "<td class=\"snarkTorrentStatus\">" + _("Allocating");
         } else if (err != null && curPeers == 0) {
             // Also don't show if seeding... but then we won't see the not-registered error
@@ -1305,7 +1446,7 @@ public class I2PSnarkServlet extends BasicServlet {
             //                   ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             //else if (isRunning)
             if (isRunning)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td>" +
+                statusString = toThemeImg("trackererror", "", err) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Tracker Error") +
                                ": " + curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers);
@@ -1314,11 +1455,11 @@ public class I2PSnarkServlet extends BasicServlet {
                     err = DataHelper.escapeHTML(err.substring(0, MAX_DISPLAYED_ERROR_LENGTH)) + "&hellip;";
                 else
                     err = DataHelper.escapeHTML(err);
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td>" +
+                statusString = toThemeImg("trackererror", "", err) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Tracker Error");
             }
         } else if (snark.isStarting()) {
-            statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Starting") + "\"></td>" +
+            statusString = toThemeImg("stalled", "", _("Starting")) + "</td>" +
                            "<td class=\"snarkTorrentStatus\">" + _("Starting");
         } else if (remaining == 0 || needed == 0) {  // < 0 means no meta size yet
             // partial complete or seeding
@@ -1334,52 +1475,52 @@ public class I2PSnarkServlet extends BasicServlet {
                     txt = _("Complete");
                 }
                 if (curPeers > 0 && !showPeers)
-                    statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" title=\"" + txt + "\"></td>" +
+                    statusString = toThemeImg(img, "", txt) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + txt +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" +
+                               ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
                 else
-                    statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" title=\"" + txt + "\"></td>" +
+                    statusString = toThemeImg(img, "", txt) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + txt +
                                ": " + curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers);
             } else {
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "complete.png\" title=\"" + _("Complete") + "\"></td>" +
+                statusString = toThemeImg("complete", "", _("Complete")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Complete");
             }
         } else {
             if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" title=\"" + _("OK") + "\"></td>" +
+                statusString = toThemeImg("downloading", "", _("OK")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("OK") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" +
+                               ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning && curPeers > 0 && downBps > 0)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" title=\"" + _("OK") + "\"></td>" +
+                statusString = toThemeImg("downloading", "", _("OK")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("OK") +
                                ": " + curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers);
             else if (isRunning && curPeers > 0 && !showPeers)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Stalled") + "\"></td>" +
+                statusString = toThemeImg("stalled", "", _("Stalled")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Stalled") +
-                               ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" +
+                               ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" +
                                curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
             else if (isRunning && curPeers > 0)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Stalled") + "\"></td>" +
+                statusString = toThemeImg("stalled", "", _("Stalled")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Stalled") +
                                ": " + curPeers + thinsp(noThinsp) +
                                ngettext("1 peer", "{0} peers", knownPeers);
             else if (isRunning && knownPeers > 0)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "nopeers.png\" title=\"" + _("No Peers") + "\"></td>" +
+                statusString = toThemeImg("nopeers", "", _("No Peers")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("No Peers") +
                                ": 0" + thinsp(noThinsp) + knownPeers ;
             else if (isRunning)
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "nopeers.png\" title=\"" + _("No Peers") + "\"></td>" +
+                statusString = toThemeImg("nopeers", "", _("No Peers")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("No Peers");
             else
-                statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stopped.png\" title=\"" + _("Stopped") + "\"></td>" +
+                statusString = toThemeImg("stopped", "", _("Stopped")) + "</td>" +
                                "<td class=\"snarkTorrentStatus\">" + _("Stopped");
         }
         
@@ -1467,8 +1608,17 @@ public class I2PSnarkServlet extends BasicServlet {
         //    out.write("??");  // no meta size yet
         out.write("</td>\n\t");
         out.write("<td align=\"right\" class=\"snarkTorrentUploaded\">");
-        if (isValid && uploaded > 0)
-           out.write(formatSize(uploaded));
+        if (isValid) {
+            if (showRatios) {
+                if (total > 0) {
+                    double ratio = uploaded / ((double) total);
+                    out.write((new DecimalFormat("0.000")).format(ratio));
+                    out.write("&nbsp;x");
+                }
+            } else if (uploaded > 0) {
+                out.write(formatSize(uploaded));
+            }
+        }
         out.write("</td>\n\t");
         out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
         if (isRunning && needed > 0)
@@ -1485,7 +1635,8 @@ public class I2PSnarkServlet extends BasicServlet {
         } else if (isRunning) {
             // Stop Button
             if (isDegraded)
-                out.write("<a href=\"" + _contextPath + "/?action=Stop_" + b64 + "&amp;nonce=" + _nonce + stParam + "\"><img title=\"");
+                out.write("<a href=\"" + _contextPath + "/?action=Stop_" + b64 + "&amp;nonce=" + _nonce +
+                          getQueryString(req, "", null, null).replace("?", "&amp;") + "\"><img title=\"");
             else
                 out.write("<input type=\"image\" name=\"action_Stop_" + b64 + "\" value=\"foo\" title=\"");
             out.write(_("Stop the torrent"));
@@ -1499,7 +1650,8 @@ public class I2PSnarkServlet extends BasicServlet {
                 // Start Button
                 // This works in Opera but it's displayed a little differently, so use noThinsp here too so all 3 icons are consistent
                 if (noThinsp)
-                    out.write("<a href=\"" + _contextPath + "/?action=Start_" + b64 + "&amp;nonce=" + _nonce + stParam + "\"><img title=\"");
+                    out.write("<a href=\"" + _contextPath + "/?action=Start_" + b64 + "&amp;nonce=" + _nonce +
+                              getQueryString(req, "", null, null).replace("?", "&amp;") + "\"><img title=\"");
                 else
                     out.write("<input type=\"image\" name=\"action_Start_" + b64 + "\" value=\"foo\" title=\"");
                 out.write(_("Start the torrent"));
@@ -1513,7 +1665,8 @@ public class I2PSnarkServlet extends BasicServlet {
                 // Remove Button
                 // Doesnt work with Opera so use noThinsp instead of isDegraded
                 if (noThinsp)
-                    out.write("<a href=\"" + _contextPath + "/?action=Remove_" + b64 + "&amp;nonce=" + _nonce + stParam + "\"><img title=\"");
+                    out.write("<a href=\"" + _contextPath + "/?action=Remove_" + b64 + "&amp;nonce=" + _nonce +
+                              getQueryString(req, "", null, null).replace("?", "&amp;") + "\"><img title=\"");
                 else
                     out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\"");
                 out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
@@ -1534,7 +1687,8 @@ public class I2PSnarkServlet extends BasicServlet {
             // Delete Button
             // Doesnt work with Opera so use noThinsp instead of isDegraded
             if (noThinsp)
-                out.write("<a href=\"" + _contextPath + "/?action=Delete_" + b64 + "&amp;nonce=" + _nonce + stParam + "\"><img title=\"");
+                out.write("<a href=\"" + _contextPath + "/?action=Delete_" + b64 + "&amp;nonce=" + _nonce +
+                          getQueryString(req, "", null, null).replace("?", "&amp;") + "\"><img title=\"");
             else
                 out.write("<input type=\"image\" name=\"action_Delete_" + b64 + "\" value=\"foo\" title=\"");
             out.write(_("Delete the .torrent file and the associated data file(s)"));
@@ -1773,9 +1927,9 @@ public class I2PSnarkServlet extends BasicServlet {
         String linkUrl = getTrackerLinkUrl(announce, infohash);
         if (linkUrl != null) {
             StringBuilder buf = new StringBuilder(128);
-            buf.append(linkUrl)
-               .append("<img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
-               .append(_imgPath).append("details.png\"></a>");
+            buf.append(linkUrl);
+            toThemeImg(buf, "details", _("Info"), "");
+            buf.append("</a>");
             return buf.toString();
         }
         return null;
@@ -1843,14 +1997,10 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write("<div class=\"snarkNewTorrent\">\n");
         // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
         out.write("<form action=\"_post\" method=\"POST\">\n");
-        out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n");
-        out.write("<input type=\"hidden\" name=\"action\" value=\"Add\" >\n");
-        // don't lose peer setting
-        String peerParam = req.getParameter("p");
-        if (peerParam != null)
-            out.write("<input type=\"hidden\" name=\"p\" value=\"" + DataHelper.stripHTML(peerParam) + "\" >\n");
+        writeHiddenInputs(out, req, "Add");
         out.write("<div class=\"addtorrentsection\"><span class=\"snarkConfigTitle\">");
-        out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "add.png\"> ");
+        out.write(toThemeImg("add"));
+        out.write(' ');
         out.write(_("Add Torrent"));
         out.write("</span><hr>\n<table border=\"0\"><tr><td>");
         out.write(_("From URL"));
@@ -1875,14 +2025,10 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write("<a name=\"add\"></a><div class=\"newtorrentsection\"><div class=\"snarkNewTorrent\">\n");
         // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
         out.write("<form action=\"_post\" method=\"POST\">\n");
-        out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n");
-        out.write("<input type=\"hidden\" name=\"action\" value=\"Create\" >\n");
-        // don't lose peer setting
-        String peerParam = req.getParameter("p");
-        if (peerParam != null)
-            out.write("<input type=\"hidden\" name=\"p\" value=\"" + DataHelper.stripHTML(peerParam) + "\" >\n");
+        writeHiddenInputs(out, req, "Create");
         out.write("<span class=\"snarkConfigTitle\">");
-        out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "create.png\"> ");
+        out.write(toThemeImg("create"));
+        out.write(' ');
         out.write(_("Create Torrent"));
         out.write("</span><hr>\n<table border=\"0\"><tr><td>");
         //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n");
@@ -1946,11 +2092,11 @@ public class I2PSnarkServlet extends BasicServlet {
         //int seedPct = 0;
        
         out.write("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" +
-                  "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n" +
-                  "<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n" +
-                  "<input type=\"hidden\" name=\"action\" value=\"Save\" >\n" +
-                  "<span class=\"snarkConfigTitle\">" +
-                  "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> ");
+                  "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n");
+        writeHiddenInputs(out, req, "Save");
+        out.write("<span class=\"snarkConfigTitle\">");
+        out.write(toThemeImg("config"));
+        out.write(' ');
         out.write(_("Configuration"));
         out.write("</span><hr>\n"   +
                   "<table border=\"0\"><tr><td>");
@@ -1979,6 +2125,7 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write(": <td><select name='theme'>");
         String theme = _manager.getTheme();
         String[] themes = _manager.getThemes();
+        Arrays.sort(themes);
         for(int i = 0; i < themes.length; i++) {
             if(themes[i].equals(theme))
                 out.write("\n<OPTION value=\"" + themes[i] + "\" SELECTED>" + themes[i]);
@@ -2132,11 +2279,11 @@ public class I2PSnarkServlet extends BasicServlet {
     private void writeTrackerForm(PrintWriter out, HttpServletRequest req) throws IOException {
         StringBuilder buf = new StringBuilder(1024);
         buf.append("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" +
-                  "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n" +
-                  "<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n" +
-                  "<input type=\"hidden\" name=\"action\" value=\"Save2\" >\n" +
-                  "<span class=\"snarkConfigTitle\">" +
-                  "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> ");
+                   "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n");
+        writeHiddenInputs(buf, req, "Save2");
+        buf.append("<span class=\"snarkConfigTitle\">");
+        toThemeImg(buf, "config");
+        buf.append(' ');
         buf.append(_("Trackers"));
         buf.append("</span><hr>\n"   +
                    "<table class=\"trackerconfig\"><tr><th>")
@@ -2146,6 +2293,8 @@ public class I2PSnarkServlet extends BasicServlet {
            .append("</th><th>")
            .append(_("Website URL"))
            .append("</th><th>")
+           .append(_("Standard"))
+           .append("</th><th>")
            .append(_("Open"))
            .append("</th><th>")
            .append(_("Private"))
@@ -2158,18 +2307,25 @@ public class I2PSnarkServlet extends BasicServlet {
             String name = t.name;
             String homeURL = t.baseURL;
             String announceURL = t.announceURL.replace("&#61;", "=");
+            boolean isOpen = openTrackers.contains(t.announceURL);
+            boolean isPrivate = privateTrackers.contains(t.announceURL);
             buf.append("<tr><td><input type=\"checkbox\" class=\"optbox\" name=\"delete_")
                .append(name).append("\" title=\"").append(_("Delete")).append("\">" +
                        "</td><td>").append(name)
                .append("</td><td>").append(urlify(homeURL, 35))
-               .append("</td><td><input type=\"checkbox\" class=\"optbox\" name=\"open_")
+               .append("</td><td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"ttype_")
+               .append(announceURL).append("\"");
+            if (!(isOpen || isPrivate))
+                buf.append(" checked=\"checked\"");
+            buf.append(">" +
+                       "</td><td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"ttype_")
                .append(announceURL).append("\"");
-            if (openTrackers.contains(t.announceURL))
+            if (isOpen)
                 buf.append(" checked=\"checked\"");
             buf.append(">" +
-                       "</td><td><input type=\"checkbox\" class=\"optbox\" name=\"private_")
+                       "</td><td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"ttype_")
                .append(announceURL).append("\"");
-            if (privateTrackers.contains(t.announceURL)) {
+            if (isPrivate) {
                 buf.append(" checked=\"checked\"");
             } else {
                 if (SnarkManager.DEFAULT_TRACKER_ANNOUNCES.contains(t.announceURL))
@@ -2183,27 +2339,29 @@ public class I2PSnarkServlet extends BasicServlet {
            .append(_("Add")).append(":</b></td>" +
                    "<td><input type=\"text\" class=\"trackername\" name=\"tname\" spellcheck=\"false\"></td>" +
                    "<td><input type=\"text\" class=\"trackerhome\" name=\"thurl\" spellcheck=\"false\"></td>" +
-                   "<td><input type=\"checkbox\" class=\"optbox\" name=\"_add_open_\"></td>" +
-                   "<td><input type=\"checkbox\" class=\"optbox\" name=\"_add_private_\"></td>" +
+                   "<td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"add_tracker_type\" checked=\"checked\"></td>" +
+                   "<td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"add_tracker_type\"></td>" +
+                   "<td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"add_tracker_type\"></td>" +
                    "<td><input type=\"text\" class=\"trackerannounce\" name=\"taurl\" spellcheck=\"false\"></td></tr>\n" +
-                   "<tr><td colspan=\"6\">&nbsp;</td></tr>\n" +  // spacer
-                   "<tr><td colspan=\"2\"></td><td colspan=\"4\">\n" +
+                   "<tr><td colspan=\"7\">&nbsp;</td></tr>\n" +  // spacer
+                   "<tr><td colspan=\"2\"></td><td colspan=\"5\">\n" +
                    "<input type=\"submit\" name=\"taction\" class=\"default\" value=\"").append(_("Add tracker")).append("\">\n" +
                    "<input type=\"submit\" name=\"taction\" class=\"delete\" value=\"").append(_("Delete selected")).append("\">\n" +
+                   "<input type=\"submit\" name=\"taction\" class=\"add\" value=\"").append(_("Add tracker")).append("\">\n" +
                    "<input type=\"submit\" name=\"taction\" class=\"accept\" value=\"").append(_("Save tracker configuration")).append("\">\n" +
                    // "<input type=\"reset\" class=\"cancel\" value=\"").append(_("Cancel")).append("\">\n" +
                    "<input type=\"submit\" name=\"taction\" class=\"reload\" value=\"").append(_("Restore defaults")).append("\">\n" +
-                   "<input type=\"submit\" name=\"taction\" class=\"add\" value=\"").append(_("Add tracker")).append("\">\n" +
                    "</td></tr>" +
-                   "<tr><td colspan=\"6\">&nbsp;</td></tr>\n" +  // spacer
+                   "<tr><td colspan=\"7\">&nbsp;</td></tr>\n" +  // spacer
                    "</table></div></div></form>\n");
         out.write(buf.toString());
     }
 
     private void writeConfigLink(PrintWriter out) throws IOException {
         out.write("<div class=\"configsection\"><span class=\"snarkConfig\">\n" +
-                  "<span class=\"snarkConfigTitle\"><a href=\"configure\">" +
-                  "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> ");
+                  "<span class=\"snarkConfigTitle\"><a href=\"configure\">");
+        out.write(toThemeImg("config"));
+        out.write(' ');
         out.write(_("Configuration"));
         out.write("</a></span></span></div>\n");
     }
@@ -2336,23 +2494,6 @@ public class I2PSnarkServlet extends BasicServlet {
 
     private static final String FOOTER = "</div></center></body></html>";
 
-    /**
-     *  Sort alphabetically in current locale, ignore case,
-     *  directories first
-     *  @since 0.9.6
-     */
-    private static class ListingComparator implements Comparator<File>, Serializable {
-
-        public int compare(File l, File r) {
-            boolean ld = l.isDirectory();
-            boolean rd = r.isDirectory();
-            if (ld && !rd)
-                return -1;
-            if (rd && !ld)
-                return 1;
-            return Collator.getInstance().compare(l.getName(), r.getName());
-        }
-    }
 
     /**
      * Modded heavily from the Jetty version in Resource.java,
@@ -2381,10 +2522,11 @@ public class I2PSnarkServlet extends BasicServlet {
      * @param base The encoded base URL
      * @param parent True if the parent directory should be included
      * @param postParams map of POST parameters or null if not a POST
+     * @param sortParam may be null
      * @return String of HTML or null if postParams != null
      * @since 0.7.14
      */
-    private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> postParams)
+    private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> postParams, String sortParam)
         throws IOException
     {
         String decodedBase = decodePath(base);
@@ -2436,6 +2578,10 @@ public class I2PSnarkServlet extends BasicServlet {
             // dummy
             r = new File("");
         }
+
+        boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
+                               r.isDirectory();
+
         StringBuilder buf=new StringBuilder(4096);
         buf.append(DOCTYPE).append("<HTML><HEAD><TITLE>");
         if (title.endsWith("/"))
@@ -2443,9 +2589,17 @@ public class I2PSnarkServlet extends BasicServlet {
         String directory = title;
         title = _("Torrent") + ": " + DataHelper.escapeHTML(title);
         buf.append(title);
-        buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
-             "</HEAD><BODY>\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\"");
-        buf.append(" class=\"snarkRefresh\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("arrow_refresh.png\">&nbsp;&nbsp;");
+        buf.append("</TITLE>\n").append(HEADER_A).append(_themePath).append(HEADER_B)
+            .append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\n");
+        if (showPriority)
+            buf.append("<script src=\"").append(_contextPath).append(WARBASE + "js/folder.js\" type=\"text/javascript\"></script>\n");
+        buf.append("</HEAD><BODY");
+        if (showPriority)
+            buf.append(" onload=\"setupbuttons()\"");
+        buf.append(">\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\"");
+        buf.append(" class=\"snarkRefresh\">");
+        toThemeImg(buf, "arrow_refresh");
+        buf.append("&nbsp;&nbsp;");
         if (_contextName.equals(DEFAULT_NAME))
             buf.append(_("I2PSnark"));
         else
@@ -2454,11 +2608,13 @@ public class I2PSnarkServlet extends BasicServlet {
         
         if (parent)  // always true
             buf.append("<div class=\"page\"><div class=\"mainsection\">");
-        boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
-                               r.isDirectory();
         if (showPriority) {
             buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
             buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
+            if (sortParam != null) {
+                buf.append("<input type=\"hidden\" name=\"sort\" value=\"")
+                   .append(DataHelper.stripHTML(sortParam)).append("\" >\n");
+            }
         }
         if (snark != null) {
             // first table - torrent info
@@ -2471,14 +2627,16 @@ public class I2PSnarkServlet extends BasicServlet {
 
             String fullPath = snark.getName();
             String baseName = encodePath((new File(fullPath)).getName());
-            buf.append("<tr><td>")
-               .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+            buf.append("<tr><td>");
+            toThemeImg(buf, "file");
+            buf.append("&nbsp;<b>")
                .append(_("Torrent file"))
                .append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
                .append(DataHelper.escapeHTML(fullPath))
                .append("</a></td></tr>\n");
-            buf.append("<tr><td>")
-               .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+            buf.append("<tr><td>");
+            toThemeImg(buf, "file");
+            buf.append("&nbsp;<b>")
                .append(_("Data location"))
                .append(":</b> ")
                .append(DataHelper.escapeHTML(snark.getStorage().getBase().getPath()))
@@ -2495,17 +2653,19 @@ public class I2PSnarkServlet extends BasicServlet {
                     buf.append("<tr><td>");
                     String trackerLink = getTrackerLink(announce, snark.getInfoHash());
                     if (trackerLink != null)
-                        buf.append(trackerLink).append(' ');
-                    buf.append("<b>").append(_("Primary Tracker")).append(":</b> ");
+                        buf.append(trackerLink);
+                    else
+                        toThemeImg(buf, "details");
+                    buf.append(" <b>").append(_("Primary Tracker")).append(":</b> ");
                     buf.append(getShortTrackerLink(announce, snark.getInfoHash()));
                     buf.append("</td></tr>");
                 }
                 List<List<String>> alist = meta.getAnnounceList();
                 if (alist != null) {
-                    buf.append("<tr><td>" +
-                               "<img alt=\"\" border=\"0\" src=\"")
-                       .append(_imgPath).append("details.png\"> <b>");
-                    buf.append(_("Tracker List")).append(":</b> ");
+                    buf.append("<tr><td>");
+                    toThemeImg(buf, "details");
+                    buf.append(" <b>")
+                       .append(_("Tracker List")).append(":</b> ");
                     for (List<String> alist2 : alist) {
                         buf.append('[');
                         boolean more = false;
@@ -2527,8 +2687,9 @@ public class I2PSnarkServlet extends BasicServlet {
                 if (com != null) {
                     if (com.length() > 1024)
                         com = com.substring(0, 1024);
-                    buf.append("<tr><td><img alt=\"\" border=\"0\" src=\"")
-                       .append(_imgPath).append("details.png\"> <b>")
+                    buf.append("<tr><td>");
+                    toThemeImg(buf, "details");
+                    buf.append(" <b>")
                        .append(_("Comment")).append(":</b> ")
                        .append(DataHelper.stripHTML(com))
                        .append("</td></tr>\n");
@@ -2536,8 +2697,9 @@ public class I2PSnarkServlet extends BasicServlet {
                 long dat = meta.getCreationDate();
                 if (dat > 0) {
                     String date = (new SimpleDateFormat("yyyy-MM-dd HH:mm")).format(new Date(dat));
-                    buf.append("<tr><td><img alt=\"\" border=\"0\" src=\"")
-                       .append(_imgPath).append("details.png\"> <b>")
+                    buf.append("<tr><td>");
+                    toThemeImg(buf, "details");
+                    buf.append(" <b>")
                        .append(_("Created")).append(":</b> ")
                        .append(date).append(" UTC")
                        .append("</td></tr>\n");
@@ -2546,8 +2708,9 @@ public class I2PSnarkServlet extends BasicServlet {
                 if (cby != null) {
                     if (cby.length() > 128)
                         cby = com.substring(0, 128);
-                    buf.append("<tr><td><img alt=\"\" border=\"0\" src=\"")
-                       .append(_imgPath).append("details.png\"> <b>")
+                    buf.append("<tr><td>");
+                    toThemeImg(buf, "details");
+                    buf.append(" <b>")
                        .append(_("Created By")).append(":</b> ")
                        .append(DataHelper.stripHTML(cby))
                        .append("</td></tr>\n");
@@ -2582,41 +2745,70 @@ public class I2PSnarkServlet extends BasicServlet {
             //buf.append("<tr><td>").append(_("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">")
             //   .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>");
 
-            buf.append("<tr><td>")
-               .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("size.png\" >&nbsp;<b>")
+            buf.append("<tr><td>");
+            toThemeImg(buf, "size");
+            buf.append("&nbsp;<b>")
                .append(_("Size"))
                .append(":</b> ")
                .append(formatSize(snark.getTotalLength()));
             int pieces = snark.getPieces();
             double completion = (pieces - snark.getNeeded()) / (double) pieces;
+            buf.append("&nbsp;");
+            toThemeImg(buf, "head_rx");
+            buf.append("&nbsp;<b>");
             if (completion < 1.0)
-                buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;<b>")
-                   .append(_("Completion"))
+                buf.append(_("Completion"))
                    .append(":</b> ")
                    .append((new DecimalFormat("0.00%")).format(completion));
             else
-                buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;")
-                   .append(_("Complete"));
-            // else unknown
+                buf.append(_("Complete")).append("</b>");
+            // up ratio
+            buf.append("&nbsp;");
+            toThemeImg(buf, "head_tx");
+            buf.append("&nbsp;<b>")
+               .append(_("Upload ratio"))
+               .append(":</b> ");
+            long uploaded = snark.getUploaded();
+            if (uploaded > 0) {
+                double ratio = uploaded / ((double) snark.getTotalLength());
+                buf.append((new DecimalFormat("0.000")).format(ratio));
+                buf.append("&nbsp;x");
+            } else {
+                buf.append('0');
+            }
+            // not including skipped files, but -1 when not running
             long needed = snark.getNeededLength();
-            if (needed > 0)
-                buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;<b>")
+            if (needed < 0) {
+                // including skipped files, valid when not running
+                needed = snark.getRemainingLength();
+            }
+            if (needed > 0) {
+                buf.append("&nbsp;");
+                toThemeImg(buf, "head_rx");
+                buf.append("&nbsp;<b>")
                    .append(_("Remaining"))
                    .append(":</b> ")
                    .append(formatSize(needed));
+            }
             if (meta != null) {
                 List<List<String>> files = meta.getFiles();
                 int fileCount = files != null ? files.size() : 1;
-                buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+                buf.append("&nbsp;");
+                toThemeImg(buf, "file");
+                buf.append("&nbsp;<b>")
                    .append(_("Files"))
                    .append(":</b> ")
                    .append(fileCount);
             }
-            buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+            buf.append("&nbsp;");
+            toThemeImg(buf, "file");
+            buf.append("&nbsp;<b>")
                .append(_("Pieces"))
                .append(":</b> ")
                .append(pieces);
-            buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+            buf.append("&nbsp;");
+            toThemeImg(buf, "file");
+            buf.append("&nbsp;<b>")
                .append(_("Piece size"))
                .append(":</b> ")
                .append(formatSize(snark.getPieceLength(0)))
@@ -2642,7 +2834,6 @@ public class I2PSnarkServlet extends BasicServlet {
         File[] ls = null;
         if (r.isDirectory()) {
             ls = r.listFiles();
-            Arrays.sort(ls, new ListingComparator());
         }  // if r is not a directory, we are only showing torrent info section
         
         if (ls == null) {
@@ -2651,40 +2842,92 @@ public class I2PSnarkServlet extends BasicServlet {
             return buf.toString();
         }
 
+        Storage storage = snark != null ? snark.getStorage() : null;
+        List<Sorters.FileAndIndex> fileList = new ArrayList<Sorters.FileAndIndex>(ls.length);
+        for (int i = 0; i < ls.length; i++) {
+            fileList.add(new Sorters.FileAndIndex(ls[i], storage));
+        }
+
+        boolean showSort = fileList.size() > 1;
+        if (showSort) {
+            int sort = 0;
+            if (sortParam != null) {
+                try {
+                    sort = Integer.parseInt(sortParam);
+                } catch (NumberFormatException nfe) {}
+            }
+            Collections.sort(fileList, Sorters.getFileComparator(sort, this));
+        }
+
         // second table - dir info
         buf.append("<table class=\"snarkDirInfo\"><thead>\n");
         buf.append("<tr>\n")
-           .append("<th colspan=2>")
-           .append("<img border=\"0\" src=\"").append(_imgPath).append("file.png\" title=\"")
-           .append(_("Directory"))
-           .append(": ")
-           .append(directory)
-           .append("\" alt=\"")
-           .append(_("Directory"))
-           .append("\"></th>\n");
-        buf.append("<th align=\"right\">")
-           .append("<img border=\"0\" src=\"").append(_imgPath).append("size.png\" title=\"")
-           .append(_("Size"))
-           .append("\" alt=\"")
-           .append(_("Size"))
-           .append("\"></th>\n");
-        buf.append("<th class=\"headerstatus\">")
-           .append("<img border=\"0\" src=\"").append(_imgPath).append("status.png\" title=\"")
-           .append(_("Status"))
-           .append("\" alt=\"")
-           .append(_("Status"))
-           .append("\"></th>\n");
-        if (showPriority)
-            buf.append("<th class=\"headerpriority\">")
-               .append("<img border=\"0\" src=\"").append(_imgPath).append("priority.png\" title=\"")
-               .append(_("Priority"))
-               .append("\" alt=\"")
-               .append(_("Priority"))
-               .append("\"></th>\n");
-        buf.append("</tr>\n</thead>\n");
+           .append("<th colspan=2>");
+        String tx = _("Directory");
+        // cycle through sort by name or type
+        String sort;
+        boolean isTypeSort = false;
+        if (showSort) {
+            if (sortParam == null || "0".equals(sortParam) || "1".equals(sortParam)) {
+                sort = "-1";
+            } else if ("-1".equals(sortParam)) {
+                sort = "12";
+                isTypeSort = true;
+            } else if ("12".equals(sortParam)) {
+                sort = "-12";
+                isTypeSort = true;
+            } else {
+                sort = "";
+            }
+            buf.append("<a href=\"").append(base)
+               .append(getQueryString(sort)).append("\">");
+        }
+        toThemeImg(buf, "file", tx,
+                   showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : _("Name")))
+                            : tx + ": " + directory);
+        if (showSort)
+            buf.append("</a>");
+        buf.append("</th>\n<th align=\"right\">");
+        if (showSort) {
+            sort = ("5".equals(sortParam)) ? "-5" : "5";
+            buf.append("<a href=\"").append(base)
+               .append(getQueryString(sort)).append("\">");
+        }
+        tx = _("Size");
+        toThemeImg(buf, "size", tx,
+                   showSort ? _("Sort by {0}", tx) : tx);
+        if (showSort)
+            buf.append("</a>");
+        buf.append("</th>\n<th class=\"headerstatus\">");
+        if (showSort) {
+            sort = ("10".equals(sortParam)) ? "-10" : "10";
+            buf.append("<a href=\"").append(base)
+               .append(getQueryString(sort)).append("\">");
+        }
+        tx = _("Status");
+        toThemeImg(buf, "status", tx,
+                   showSort ? _("Sort by {0}", _("Remaining")) : tx);
+        if (showSort)
+            buf.append("</a>");
+        if (showPriority) {
+            buf.append("</th>\n<th class=\"headerpriority\">");
+            if (showSort) {
+                sort = ("13".equals(sortParam)) ? "-13" : "13";
+                buf.append("<a href=\"").append(base)
+                   .append(getQueryString(sort)).append("\">");
+            }
+            tx = _("Priority");
+            toThemeImg(buf, "priority", tx,
+                       showSort ? _("Sort by {0}", tx) : tx);
+            if (showSort)
+                buf.append("</a>");
+        }
+        buf.append("</th>\n</tr>\n</thead>\n");
         buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\"");
         URIUtil.encodePath(buf, addPaths(decodedBase,"../"));
-        buf.append("\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("up.png\"> ")
+        buf.append("\">");
+        toThemeImg(buf, "up");
+        buf.append(' ')
            .append(_("Up to higher level directory"))
            .append("</A></td></tr>\n");
 
@@ -2692,25 +2935,27 @@ public class I2PSnarkServlet extends BasicServlet {
         //DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
         //                                               DateFormat.MEDIUM);
         boolean showSaveButton = false;
-        for (int i=0 ; i< ls.length ; i++)
+        boolean rowEven = true;
+        for (Sorters.FileAndIndex fai : fileList)
         {   
             //String encoded = encodePath(ls[i].getName());
             // bugfix for I2P - Backport from Jetty 6 (zero file lengths and last-modified times)
             // http://jira.codehaus.org/browse/JETTY-361?page=com.atlassian.jira.plugin.system.issuetabpanels%3Achangehistory-tabpanel#issue-tabs
             // See resource.diff attachment
             //Resource item = addPath(encoded);
-            File item = ls[i];
+            File item = fai.file;
             
-            String rowClass = (i % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
+            String rowClass = (rowEven ? "snarkTorrentEven" : "snarkTorrentOdd");
+            rowEven = !rowEven;
             buf.append("<TR class=\"").append(rowClass).append("\">");
             
             // Get completeness and status string
             boolean complete = false;
             String status = "";
             long length = item.length();
-            int fileIndex = -1;
+            int fileIndex = fai.index;
             int priority = 0;
-            if (item.isDirectory()) {
+            if (fai.isDirectory) {
                 complete = true;
                 //status = toImg("tick") + ' ' + _("Directory");
             } else {
@@ -2719,10 +2964,8 @@ public class I2PSnarkServlet extends BasicServlet {
                     complete = true;
                     status = toImg("cancel") + ' ' + _("Torrent not found?");
                 } else {
-                    Storage storage = snark.getStorage();
-                    fileIndex = storage.indexOf(item);
 
-                            long remaining = storage.remaining(fileIndex);
+                            long remaining = fai.remaining;
                             if (remaining < 0) {
                                 complete = true;
                                 status = toImg("cancel") + ' ' + _("File not found in torrent?");
@@ -2730,7 +2973,7 @@ public class I2PSnarkServlet extends BasicServlet {
                                 complete = true;
                                 status = toImg("tick") + ' ' + _("Complete");
                             } else {
-                                priority = storage.getPriority(fileIndex);
+                                priority = fai.priority;
                                 if (priority < 0)
                                     status = toImg("cancel");
                                 else if (priority == 0)
@@ -2745,7 +2988,7 @@ public class I2PSnarkServlet extends BasicServlet {
                 }
             }
 
-            String path = addPaths(decodedBase, ls[i].getName());
+            String path = addPaths(decodedBase, item.getName());
             if (item.isDirectory() && !path.endsWith("/"))
                 path=addPaths(path,"/");
             path = encodePath(path);
@@ -2782,19 +3025,19 @@ public class I2PSnarkServlet extends BasicServlet {
             if (showPriority) {
                 buf.append("<td class=\"priority\">");
                 if ((!complete) && (!item.isDirectory())) {
-                    buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(fileIndex).append("\" ");
+                    buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prihigh\" value=\"5\" name=\"pri.").append(fileIndex).append("\" ");
                     if (priority > 0)
-                        buf.append("checked=\"true\"");
+                        buf.append("checked=\"checked\"");
                     buf.append('>').append(_("High"));
 
-                    buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(fileIndex).append("\" ");
+                    buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prinorm\" value=\"0\" name=\"pri.").append(fileIndex).append("\" ");
                     if (priority == 0)
-                        buf.append("checked=\"true\"");
+                        buf.append("checked=\"checked\"");
                     buf.append('>').append(_("Normal"));
 
-                    buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(fileIndex).append("\" ");
+                    buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"priskip\" value=\"-9\" name=\"pri.").append(fileIndex).append("\" ");
                     if (priority < 0)
-                        buf.append("checked=\"true\"");
+                        buf.append("checked=\"checked\"");
                     buf.append('>').append(_("Skip"));
                     showSaveButton = true;
                 }
@@ -2803,9 +3046,16 @@ public class I2PSnarkServlet extends BasicServlet {
             buf.append("</TR>\n");
         }
         if (showSaveButton) {
-            buf.append("<thead><tr><th colspan=\"4\">&nbsp;</th><th class=\"headerpriority\"><input type=\"submit\" value=\"");
-            buf.append(_("Save priorities"));
-            buf.append("\" name=\"foo\" ></th></tr></thead>\n");
+            buf.append("<thead><tr><th colspan=\"4\">&nbsp;</th><th class=\"headerpriority\">" +
+                       "<a class=\"control\" id=\"setallhigh\" href=\"javascript:void(null);\" onclick=\"setallhigh();\">")
+               .append(toImg("clock_red")).append(_("Set all high")).append("</a>\n" +
+                       "<a class=\"control\" id=\"setallnorm\" href=\"javascript:void(null);\" onclick=\"setallnorm();\">")
+               .append(toImg("clock")).append(_("Set all normal")).append("</a>\n" +
+                       "<a class=\"control\" id=\"setallskip\" href=\"javascript:void(null);\" onclick=\"setallskip();\">")
+               .append(toImg("cancel")).append(_("Skip all")).append("</a>\n" +
+                       "<br><br><input type=\"submit\" class=\"accept\" value=\"").append(_("Save priorities"))
+               .append("\" name=\"savepri\" >\n" +
+                       "</th></tr></thead>\n");
         }
         buf.append("</table>\n");
         if (showPriority)
@@ -2815,7 +3065,23 @@ public class I2PSnarkServlet extends BasicServlet {
         return buf.toString();
     }
 
-    /** @since 0.7.14 */
+    /**
+     *  @param null ok
+     *  @return query string or ""
+     *  @since 0.9.16
+     */
+    private static String getQueryString(String so) {
+        if (so != null && !so.equals(""))
+            return "?sort=" + DataHelper.stripHTML(so);
+        return "";
+    }
+
+    /**
+     *  Pick an icon; try to catch the common types in an i2p environment.
+     *
+     *  @return file name not including ".png"
+     *  @since 0.7.14
+     */
     private String toIcon(File item) {
         if (item.isDirectory())
             return "folder";
@@ -2824,10 +3090,12 @@ public class I2PSnarkServlet extends BasicServlet {
 
     /**
      *  Pick an icon; try to catch the common types in an i2p environment
+     *  Pkg private for FileTypeSorter.
+     *
      *  @return file name not including ".png"
      *  @since 0.7.14
      */
-    private String toIcon(String path) {
+    String toIcon(String path) {
         String icon;
         // Note that for this to work well, our custom mime.properties file must be loaded.
         String plc = path.toLowerCase(Locale.US);
@@ -2856,7 +3124,12 @@ public class I2PSnarkServlet extends BasicServlet {
             icon = "music";
         else if (mime.startsWith("video/"))
             icon = "film";
-        else if (mime.equals("application/zip") || mime.equals("application/x-gtar") ||
+        else if (mime.equals("application/zip")) {
+            if (plc.endsWith(".su3") || plc.endsWith(".su2") || plc.endsWith(".sud"))
+                icon = "itoopie_xxsm";
+            else
+                icon = "compress";
+        } else if (mime.equals("application/x-gtar") ||
                  mime.equals("application/compress") || mime.equals("application/gzip") ||
                  mime.equals("application/x-7z-compressed") || mime.equals("application/x-rar-compressed") ||
                  mime.equals("application/x-tar") || mime.equals("application/x-bzip2"))
@@ -2872,14 +3145,73 @@ public class I2PSnarkServlet extends BasicServlet {
         return icon;
     }
     
-    /** @since 0.7.14 */
+    /**
+     *  Icon file in the .war. Always 16x16.
+     *
+     *  @param icon name without the ".png"
+     *  @since 0.7.14
+     */
     private String toImg(String icon) {
-        return "<img alt=\"\" height=\"16\" width=\"16\" src=\"" + _contextPath + "/.icons/" + icon + ".png\">";
+        return toImg(icon, "");
     }
 
-    /** @since 0.8.2 */
+    /**
+     *  Icon file in the .war. Always 16x16.
+     *
+     *  @param icon name without the ".png"
+     *  @since 0.8.2
+     */
     private String toImg(String icon, String altText) {
-        return "<img alt=\"" + altText + "\" height=\"16\" width=\"16\" src=\"" + _contextPath + "/.icons/" + icon + ".png\">";
+        return "<img alt=\"" + altText + "\" height=\"16\" width=\"16\" src=\"" + _contextPath + WARBASE + "icons/" + icon + ".png\">";
+    }
+    
+    /**
+     *  Image file in the theme.
+     *
+     *  @param image name without the ".png"
+     *  @since 0.9.16
+     */
+    private String toThemeImg(String image) {
+        return toThemeImg(image, "", "");
+    }
+    
+    /**
+     *  Image file in the theme.
+     *
+     *  @param image name without the ".png"
+     *  @since 0.9.16
+     */
+    private void toThemeImg(StringBuilder buf, String image) {
+        toThemeImg(buf, image, "", "");
+    }
+
+    /**
+     *  Image file in the theme.
+     *
+     *  @param image name without the ".png"
+     *  @param altText non-null
+     *  @param titleText non-null
+     *  @since 0.9.16
+     */
+    private String toThemeImg(String image, String altText, String titleText) {
+        StringBuilder buf = new StringBuilder(128);
+        toThemeImg(buf, image, altText, titleText);
+        return buf.toString();
+    }
+
+    /**
+     *  Image file in the theme.
+     *
+     *  @param image name without the ".png"
+     *  @param altText non-null
+     *  @param titleText non-null
+     *  @since 0.9.16
+     */
+    private void toThemeImg(StringBuilder buf, String image, String altText, String titleText) {
+        buf.append("<img alt=\"").append(altText).append("\" src=\"").append(_imgPath).append(image).append(".png\"");
+        if (titleText.length() > 0)
+            buf.append(" title=\"").append(titleText).append('"');
+        buf.append('>');
     }
 
     /** @since 0.8.1 */
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java
new file mode 100644
index 0000000000000000000000000000000000000000..8098520b1cb2b485c6a22bbc2313fa51db9470c0
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java
@@ -0,0 +1,531 @@
+package org.klomp.snark.web;
+
+import java.io.File;
+import java.io.Serializable;
+import java.text.Collator;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Locale;
+
+import org.klomp.snark.MetaInfo;
+import org.klomp.snark.Snark;
+import org.klomp.snark.Storage;
+
+/**
+ *  Comparators for various columns
+ *
+ *  @since 0.9.16 from TorrentNameComparator, moved from I2PSnarkservlet
+ */
+class Sorters {
+
+    /**
+     *  Negative is reverse
+     *
+     *<ul>
+     *<li>0, 1: Name
+     *<li>2: Status
+     *<li>3: Peers
+     *<li>4: ETA
+     *<li>5: Size
+     *<li>6: Downloaded
+     *<li>7: Uploaded
+     *<li>8: Down rate
+     *<li>9: Up rate
+     *<li>10: Remaining (needed)
+     *<li>11: Upload ratio
+     *<li>12: File type
+     *</ul>
+     *
+     *  @param servlet for file type callback only
+     */
+    public static Comparator<Snark> getComparator(int type, I2PSnarkServlet servlet) {
+        boolean rev = type < 0;
+        Comparator<Snark> rv;
+        switch (type) {
+
+          case -1:
+          case 0:
+          case 1:
+          default:
+              rv = new TorrentNameComparator();
+              if (rev)
+                  rv = Collections.reverseOrder(rv);
+              break;
+
+          case -2:
+          case 2:
+              rv = new StatusComparator(rev);
+              break;
+
+          case -3:
+          case 3:
+              rv = new PeersComparator(rev);
+              break;
+
+          case -4:
+          case 4:
+              rv = new ETAComparator(rev);
+              break;
+
+          case -5:
+          case 5:
+              rv = new SizeComparator(rev);
+              break;
+
+          case -6:
+          case 6:
+              rv = new DownloadedComparator(rev);
+              break;
+
+          case -7:
+          case 7:
+              rv = new UploadedComparator(rev);
+              break;
+
+          case -8:
+          case 8:
+              rv = new DownRateComparator(rev);
+              break;
+
+          case -9:
+          case 9:
+              rv = new UpRateComparator(rev);
+              break;
+
+          case -10:
+          case 10:
+              rv = new RemainingComparator(rev);
+              break;
+
+          case -11:
+          case 11:
+              rv = new RatioComparator(rev);
+              break;
+
+          case -12:
+          case 12:
+              rv = new FileTypeComparator(rev, servlet);
+              break;
+
+        }
+        return rv;
+    }
+
+
+    /**
+     *  Sort alphabetically in current locale, ignore case, ignore leading "the "
+     *  (I guess this is worth it, a lot of torrents start with "The "
+     *  @since 0.7.14
+     */
+    private static class TorrentNameComparator implements Comparator<Snark>, Serializable {
+
+        public int compare(Snark l, Snark r) {
+            return comp(l, r);
+        }
+
+        public static int comp(Snark l, Snark r) {
+            // put downloads and magnets first
+            if (l.getStorage() == null && r.getStorage() != null)
+                return -1;
+            if (l.getStorage() != null && r.getStorage() == null)
+                return 1;
+            String ls = l.getBaseName();
+            String llc = ls.toLowerCase(Locale.US);
+            if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_"))
+                ls = ls.substring(4);
+            String rs = r.getBaseName();
+            String rlc = rs.toLowerCase(Locale.US);
+            if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_"))
+                rs = rs.substring(4);
+            return Collator.getInstance().compare(ls, rs);
+        }
+    }
+
+    /**
+     *  Forward or reverse sort, but the fallback is always forward
+     */
+    private static abstract class Sort implements Comparator<Snark>, Serializable {
+
+        private final boolean _rev;
+
+        public Sort(boolean rev) {
+            _rev = rev;
+        }
+
+        public int compare(Snark l, Snark r) {
+            int rv = compareIt(l, r);
+            if (rv != 0)
+                return _rev ? 0 - rv : rv;
+            return TorrentNameComparator.comp(l, r);
+        }
+
+        protected abstract int compareIt(Snark l, Snark r);
+
+        protected static int compLong(long l, long r) {
+            if (l < r)
+                return -1;
+            if (l > r)
+                return 1;
+            return 0;
+        }
+    }
+
+
+    private static class StatusComparator extends Sort {
+
+        private StatusComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            int rv = getStatus(l) - getStatus(r);
+            if (rv != 0)
+                return rv;
+            // use reverse remaining as first tie break
+            return compLong(r.getNeededLength(), l.getNeededLength());
+        }
+
+        private static int getStatus(Snark snark) {
+            long remaining = snark.getRemainingLength(); 
+            long needed = snark.getNeededLength(); 
+            if (snark.isStopped()) {
+                if (remaining < 0)
+                    return 0;
+                if (remaining > 0)
+                    return 5;
+                return 10;
+            }
+            if (snark.isStarting())
+                return 15;
+            if (snark.isAllocating())
+                return 20;
+            if (remaining < 0)
+                return 15; // magnet
+            if (remaining == 0)
+                return 100;
+            if (snark.isChecking())
+                return 95;
+            if (snark.getNeededLength() <= 0)
+                return 90;
+            if (snark.getPeerCount() <= 0)
+                return 40;
+            if (snark.getDownloadRate() <= 0)
+                return 50;
+            return 60;
+        }
+    }
+
+    private static class PeersComparator extends Sort {
+
+        public PeersComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return l.getPeerCount() - r.getPeerCount();
+        }
+    }
+
+    private static class RemainingComparator extends Sort {
+
+        public RemainingComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(l.getNeededLength(), r.getNeededLength());
+        }
+    }
+
+    private static class ETAComparator extends Sort {
+
+        public ETAComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(eta(l), eta(r));
+        }
+
+        private static long eta(Snark snark) {
+            long needed = snark.getNeededLength(); 
+            if (needed <= 0)
+                return 0;
+            long total = snark.getTotalLength();
+            if (needed > total)
+                needed = total;
+            long downBps = snark.getDownloadRate();
+            if (downBps > 0)
+                return needed / downBps;
+            return Long.MAX_VALUE;
+        }
+    }
+
+    private static class SizeComparator extends Sort {
+
+        public SizeComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(l.getTotalLength(), r.getTotalLength());
+        }
+    }
+
+    private static class DownloadedComparator extends Sort {
+
+        public DownloadedComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            long ld = l.getTotalLength() - l.getRemainingLength();
+            long rd = r.getTotalLength() - r.getRemainingLength();
+            return compLong(ld, rd);
+        }
+    }
+
+    private static class UploadedComparator extends Sort {
+
+        public UploadedComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(l.getUploaded(), r.getUploaded());
+        }
+    }
+
+    private static class DownRateComparator extends Sort {
+
+        public DownRateComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(l.getDownloadRate(), r.getDownloadRate());
+        }
+    }
+
+    private static class UpRateComparator extends Sort {
+
+        public UpRateComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            return compLong(l.getUploadRate(), r.getUploadRate());
+        }
+    }
+
+    private static class RatioComparator extends Sort {
+
+        private static final long M = 128 * 1024 * 1024;
+
+        public RatioComparator(boolean rev) { super(rev); }
+
+        public int compareIt(Snark l, Snark r) {
+            long lt = l.getTotalLength();
+            long ld = lt > 0 ? ((M * l.getUploaded()) / lt) : 0;
+            long rt = r.getTotalLength();
+            long rd = rt > 0 ? ((M * r.getUploaded()) / rt) : 0;
+            return compLong(ld, rd);
+        }
+    }
+
+    private static class FileTypeComparator extends Sort {
+
+        private final I2PSnarkServlet servlet;
+
+        public FileTypeComparator(boolean rev, I2PSnarkServlet servlet) {
+            super(rev);
+            this.servlet = servlet;
+        }
+
+        public int compareIt(Snark l, Snark r) {
+            String ls = toName(l);
+            String rs = toName(r);
+            return ls.compareTo(rs);
+        }
+
+        private String toName(Snark snark) {
+            MetaInfo meta = snark.getMetaInfo();
+            if (meta == null)
+                return "0";
+            if (meta.getFiles() != null)
+                return "1";
+            // arbitrary sort based on icon name
+            return servlet.toIcon(meta.getName());
+        }
+    }
+
+    ////////////// Comparators for details page below
+
+    /**
+     *  Class to precompute and efficiently sort data
+     *  on a torrent file entry.
+     */
+    public static class FileAndIndex {
+        public final File file;
+        public final boolean isDirectory;
+        public final long length;
+        public final long remaining;
+        public final int priority;
+        public final int index;
+
+        /**
+         *  @param storage may be null
+         */
+        public FileAndIndex(File file, Storage storage) {
+            this.file = file;
+            index = storage != null ? storage.indexOf(file) : -1;
+            if (index >= 0) {
+                isDirectory = false;
+                remaining = storage.remaining(index);
+                priority = storage.getPriority(index);
+            } else {
+                isDirectory = file.isDirectory();
+                remaining = -1;
+                priority = -999;
+            }
+            length = isDirectory ? 0 : file.length();
+        }
+    }
+
+
+    /**
+     *  Negative is reverse
+     *
+     *<ul>
+     *<li>0, 1: Name
+     *<li>5: Size
+     *<li>10: Remaining (needed)
+     *<li>12: File type
+     *<li>13: Priority
+     *</ul>
+     *
+     *  @param servlet for file type callback only
+     */
+    public static Comparator<FileAndIndex> getFileComparator(int type, I2PSnarkServlet servlet) {
+        boolean rev = type < 0;
+        Comparator<FileAndIndex> rv;
+
+        switch (type) {
+
+          case -1:
+          case 0:
+          case 1:
+          default:
+              rv = new FileNameComparator();
+              if (rev)
+                  rv = Collections.reverseOrder(rv);
+              break;
+
+          case -5:
+          case 5:
+              rv = new FAISizeComparator(rev);
+              break;
+
+          case -10:
+          case 10:
+              rv = new FAIRemainingComparator(rev);
+              break;
+
+          case -12:
+          case 12:
+              rv = new FAITypeComparator(rev, servlet);
+              break;
+
+          case -13:
+          case 13:
+              rv = new FAIPriorityComparator(rev);
+              break;
+
+        }
+        return rv;
+    }
+
+    /**
+     *  Sort alphabetically in current locale, ignore case,
+     *  directories first
+     *  @since 0.9.6 moved from I2PSnarkServlet in 0.9.16
+     */
+    private static class FileNameComparator implements Comparator<FileAndIndex>, Serializable {
+
+        public int compare(FileAndIndex l, FileAndIndex r) {
+            return comp(l, r);
+        }
+
+        public static int comp(FileAndIndex l, FileAndIndex r) {
+            boolean ld = l.isDirectory;
+            boolean rd = r.isDirectory;
+            if (ld && !rd)
+                return -1;
+            if (rd && !ld)
+                return 1;
+            return Collator.getInstance().compare(l.file.getName(), r.file.getName());
+        }
+    }
+
+    /**
+     *  Forward or reverse sort, but the fallback is always forward
+     */
+    private static abstract class FAISort implements Comparator<FileAndIndex>, Serializable {
+
+        private final boolean _rev;
+
+        public FAISort(boolean rev) {
+            _rev = rev;
+        }
+
+        public int compare(FileAndIndex l, FileAndIndex r) {
+            int rv = compareIt(l, r);
+            if (rv != 0)
+                return _rev ? 0 - rv : rv;
+            return FileNameComparator.comp(l, r);
+        }
+
+        protected abstract int compareIt(FileAndIndex l, FileAndIndex r);
+
+        protected static int compLong(long l, long r) {
+            if (l < r)
+                return -1;
+            if (l > r)
+                return 1;
+            return 0;
+        }
+    }
+
+    private static class FAIRemainingComparator extends FAISort {
+
+        public FAIRemainingComparator(boolean rev) { super(rev); }
+
+        public int compareIt(FileAndIndex l, FileAndIndex r) {
+            return compLong(l.remaining, r.remaining);
+        }
+    }
+
+    private static class FAISizeComparator extends FAISort {
+
+        public FAISizeComparator(boolean rev) { super(rev); }
+
+        public int compareIt(FileAndIndex l, FileAndIndex r) {
+            return compLong(l.length, r.length);
+        }
+    }
+
+    private static class FAITypeComparator extends FAISort {
+
+        private final I2PSnarkServlet servlet;
+
+        public FAITypeComparator(boolean rev, I2PSnarkServlet servlet) {
+            super(rev);
+            this.servlet = servlet;
+        }
+
+        public int compareIt(FileAndIndex l, FileAndIndex r) {
+            String ls = toName(l);
+            String rs = toName(r);
+            return ls.compareTo(rs);
+        }
+
+        private String toName(FileAndIndex fai) {
+            if (fai.isDirectory)
+                return "0";
+            // arbitrary sort based on icon name
+            return servlet.toIcon(fai.file.getName());
+        }
+    }
+
+    private static class FAIPriorityComparator extends FAISort {
+
+        public FAIPriorityComparator(boolean rev) { super(rev); }
+
+        /** highest first */
+        public int compareIt(FileAndIndex l, FileAndIndex r) {
+            return r.priority - l.priority;
+        }
+    }
+}
diff --git a/apps/i2psnark/mime.properties b/apps/i2psnark/mime.properties
index b251fb72ea4d8fb0edfaf5ee678938423d66c58e..fff1a696e900c3eaadac33b3278299d6b9b71c00 100644
--- a/apps/i2psnark/mime.properties
+++ b/apps/i2psnark/mime.properties
@@ -8,6 +8,7 @@ epub	= application/epub+zip
 flac	= audio/flac
 flv	= video/x-flv
 iso	= application/x-iso9660-image
+js	= text/javascript
 m4a	= audio/mp4a-latm
 m4v	= video/x-m4v
 mkv	= video/x-matroska
diff --git a/apps/i2psnark/icons/application.png b/apps/i2psnark/resources/icons/application.png
similarity index 100%
rename from apps/i2psnark/icons/application.png
rename to apps/i2psnark/resources/icons/application.png
diff --git a/apps/i2psnark/icons/basket_put.png b/apps/i2psnark/resources/icons/basket_put.png
similarity index 100%
rename from apps/i2psnark/icons/basket_put.png
rename to apps/i2psnark/resources/icons/basket_put.png
diff --git a/apps/i2psnark/icons/cancel.png b/apps/i2psnark/resources/icons/cancel.png
similarity index 100%
rename from apps/i2psnark/icons/cancel.png
rename to apps/i2psnark/resources/icons/cancel.png
diff --git a/apps/i2psnark/icons/cd.png b/apps/i2psnark/resources/icons/cd.png
similarity index 100%
rename from apps/i2psnark/icons/cd.png
rename to apps/i2psnark/resources/icons/cd.png
diff --git a/apps/i2psnark/icons/clock.png b/apps/i2psnark/resources/icons/clock.png
similarity index 100%
rename from apps/i2psnark/icons/clock.png
rename to apps/i2psnark/resources/icons/clock.png
diff --git a/apps/i2psnark/icons/clock_red.png b/apps/i2psnark/resources/icons/clock_red.png
similarity index 100%
rename from apps/i2psnark/icons/clock_red.png
rename to apps/i2psnark/resources/icons/clock_red.png
diff --git a/apps/i2psnark/icons/compress.png b/apps/i2psnark/resources/icons/compress.png
similarity index 100%
rename from apps/i2psnark/icons/compress.png
rename to apps/i2psnark/resources/icons/compress.png
diff --git a/apps/i2psnark/icons/film.png b/apps/i2psnark/resources/icons/film.png
similarity index 100%
rename from apps/i2psnark/icons/film.png
rename to apps/i2psnark/resources/icons/film.png
diff --git a/apps/i2psnark/icons/folder.png b/apps/i2psnark/resources/icons/folder.png
similarity index 100%
rename from apps/i2psnark/icons/folder.png
rename to apps/i2psnark/resources/icons/folder.png
diff --git a/apps/i2psnark/icons/html.png b/apps/i2psnark/resources/icons/html.png
similarity index 100%
rename from apps/i2psnark/icons/html.png
rename to apps/i2psnark/resources/icons/html.png
diff --git a/apps/i2psnark/resources/icons/itoopie_xxsm.png b/apps/i2psnark/resources/icons/itoopie_xxsm.png
new file mode 100644
index 0000000000000000000000000000000000000000..0cec9e5c979f17c6cd488a5badb66a7e7ad3d7f3
Binary files /dev/null and b/apps/i2psnark/resources/icons/itoopie_xxsm.png differ
diff --git a/apps/i2psnark/icons/magnet.png b/apps/i2psnark/resources/icons/magnet.png
similarity index 100%
rename from apps/i2psnark/icons/magnet.png
rename to apps/i2psnark/resources/icons/magnet.png
diff --git a/apps/i2psnark/icons/music.png b/apps/i2psnark/resources/icons/music.png
similarity index 100%
rename from apps/i2psnark/icons/music.png
rename to apps/i2psnark/resources/icons/music.png
diff --git a/apps/i2psnark/icons/package.png b/apps/i2psnark/resources/icons/package.png
similarity index 100%
rename from apps/i2psnark/icons/package.png
rename to apps/i2psnark/resources/icons/package.png
diff --git a/apps/i2psnark/icons/page.png b/apps/i2psnark/resources/icons/page.png
similarity index 100%
rename from apps/i2psnark/icons/page.png
rename to apps/i2psnark/resources/icons/page.png
diff --git a/apps/i2psnark/icons/page_white.png b/apps/i2psnark/resources/icons/page_white.png
similarity index 100%
rename from apps/i2psnark/icons/page_white.png
rename to apps/i2psnark/resources/icons/page_white.png
diff --git a/apps/i2psnark/icons/page_white_acrobat.png b/apps/i2psnark/resources/icons/page_white_acrobat.png
similarity index 100%
rename from apps/i2psnark/icons/page_white_acrobat.png
rename to apps/i2psnark/resources/icons/page_white_acrobat.png
diff --git a/apps/i2psnark/icons/photo.png b/apps/i2psnark/resources/icons/photo.png
similarity index 100%
rename from apps/i2psnark/icons/photo.png
rename to apps/i2psnark/resources/icons/photo.png
diff --git a/apps/i2psnark/icons/plugin.png b/apps/i2psnark/resources/icons/plugin.png
similarity index 100%
rename from apps/i2psnark/icons/plugin.png
rename to apps/i2psnark/resources/icons/plugin.png
diff --git a/apps/i2psnark/icons/tick.png b/apps/i2psnark/resources/icons/tick.png
similarity index 100%
rename from apps/i2psnark/icons/tick.png
rename to apps/i2psnark/resources/icons/tick.png
diff --git a/apps/i2psnark/resources/js/folder.js b/apps/i2psnark/resources/js/folder.js
new file mode 100644
index 0000000000000000000000000000000000000000..fbc906bf92b1b07e21b7cfead20bf8e788945b4e
--- /dev/null
+++ b/apps/i2psnark/resources/js/folder.js
@@ -0,0 +1,93 @@
+function setupbuttons() {
+	updatesetallbuttons();
+	var form = document.forms[0];
+	form.savepri.disabled = true;
+	form.savepri.className = 'disabled';
+}
+
+function priorityclicked() {
+	updatesetallbuttons();
+	var form = document.forms[0];
+	form.savepri.disabled = false;
+	form.savepri.className = 'accept';
+}
+
+function updatesetallbuttons() {
+	var notNorm = false;
+	var notHigh = false;
+	var notSkip = false;
+	var form = document.forms[0];
+	for(i = 0; i < form.elements.length; i++) {
+		var elem = form.elements[i];
+		if (elem.type == 'radio') {
+			if (!elem.checked) {
+				if (elem.className == 'prinorm')
+					notNorm = true;
+				else if (elem.className == 'prihigh')
+					notHigh = true;
+				else
+					notSkip = true;
+			}
+		}
+	}
+	if (notNorm)
+	    document.getElementById('setallnorm').className = 'control';
+	else
+	    document.getElementById('setallnorm').className = 'controld';
+	if (notHigh)
+	    document.getElementById('setallhigh').className = 'control';
+	else
+	    document.getElementById('setallhigh').className = 'controld';
+	if (notSkip)
+	    document.getElementById('setallskip').className = 'control';
+	else
+	    document.getElementById('setallskip').className = 'controld';
+}
+
+function setallnorm() {
+	var form = document.forms[0];
+	for(i = 0; i < form.elements.length; i++) {
+		var elem = form.elements[i];
+		if (elem.type == 'radio') {
+			if (elem.className === 'prinorm')
+				elem.checked = true;
+		}
+	}
+	document.getElementById('setallnorm').className = 'controld';
+	document.getElementById('setallhigh').className = 'control';
+	document.getElementById('setallskip').className = 'control';
+	form.savepri.disabled = false;
+	form.savepri.className = 'accept';
+}
+
+function setallhigh() {
+	var form = document.forms[0];
+	for(i = 0; i < form.elements.length; i++) {
+		var elem = form.elements[i];
+		if (elem.type == 'radio') {
+			if (elem.className === 'prihigh')
+				elem.checked = true;
+		}
+	}
+	document.getElementById('setallnorm').className = 'control';
+	document.getElementById('setallhigh').className = 'controld';
+	document.getElementById('setallskip').className = 'control';
+	form.savepri.disabled = false;
+	form.savepri.className = 'accept';
+}
+
+function setallskip() {
+	var form = document.forms[0];
+	for(i = 0; i < form.elements.length; i++) {
+		var elem = form.elements[i];
+		if (elem.type == 'radio') {
+			if (elem.className === 'priskip')
+				elem.checked = true;
+		}
+	}
+	document.getElementById('setallnorm').className = 'control';
+	document.getElementById('setallhigh').className = 'control';
+	document.getElementById('setallskip').className = 'controld';
+	form.savepri.disabled = false;
+	form.savepri.className = 'accept';
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
index 0e52f17a1c938d08c31c060be48b314dad4fc450..1bfbada56a21f707c9d7098f3742a9abfa5593fa 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
@@ -40,6 +40,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.lang.reflect.Constructor;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
@@ -256,7 +257,15 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
         }
 
         if (gui) {
-            new I2PTunnelGUI(this);
+            // removed from source, now in i2p.scripts
+            //new I2PTunnelGUI(this);
+            try {
+                Class<?> cls = Class.forName("net.i2p.i2ptunnel.I2PTunnelGUI");
+                Constructor<?> con = cls.getConstructor(I2PTunnel.class);
+                con.newInstance(this);
+            } catch (Throwable t) {
+                throw new UnsupportedOperationException("GUI is not available, try -cli", t);
+            }
         } else if (cli) {
             try {
                 System.out.println("Enter 'help' for help.");
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java
deleted file mode 100644
index 7dd7308456dfde7207217d4fb970f63e3f5972ba..0000000000000000000000000000000000000000
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
- * (c) 2003 - 2004 mihi
- */
-package net.i2p.i2ptunnel;
-
-import java.awt.BorderLayout;
-import java.awt.Font;
-import java.awt.Frame;
-import java.awt.TextArea;
-import java.awt.TextField;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-/**
- * AWT gui since kaffe doesn't support swing yet
- */
-public class I2PTunnelGUI extends Frame implements ActionListener, Logging {
-
-    TextField input;
-    TextArea log;
-    I2PTunnel t;
-
-    public I2PTunnelGUI(I2PTunnel t) {
-        super("I2PTunnel control panel");
-        this.t = t;
-        setLayout(new BorderLayout());
-        add("South", input = new TextField());
-        input.addActionListener(this);
-        Font font = new Font("Monospaced", Font.PLAIN, 12);
-        add("Center", log = new TextArea("", 20, 80, TextArea.SCROLLBARS_VERTICAL_ONLY));
-        log.setFont(font);
-        log.setEditable(false);
-        log("enter 'help' for help.");
-        pack();
-        setVisible(true);
-    }
-
-    public void log(String s) {
-        log.append(s + "\n");
-    }
-
-    public void actionPerformed(ActionEvent evt) {
-        log("I2PTunnel>" + input.getText());
-        t.runCommand(input.getText(), this);
-        log("---");
-        input.setText("");
-    }
-}
\ No newline at end of file
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 4813fa2a17a9504fff0cc9bc7e4bdf8cb48ac437..858458e3ffe8516162da4c6e384c41e2eddcfb48 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -601,9 +601,12 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
             return;
         int status = ise != null ? ise.getStatus() : -1;
         String error;
-        //TODO MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION
         if (status == MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET) {
+            // We won't get this one unless it is treated as a hard failure
+            // in streaming. See PacketQueue.java
             error = usingWWWProxy ? "nolsp" : "nols";
+        } else if (status == MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION) {
+            error = usingWWWProxy ? "encp" : "enc";
         } else {
             error = usingWWWProxy ? "dnfp" : "dnf";
         }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index 0597db5f19865e77cf20d61fb026b5c9a2f2d1f0..b49f51a07f3cd117b6449dae579b5472a8cc1b04 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -188,8 +188,7 @@ public class EditBean extends IndexBean {
     
     /** @since 0.9.12 */
     public boolean isSigTypeAvailable(int code) {
-        SigType type = SigType.getByCode(code);
-        return type != null && type.isAvailable();
+        return SigType.isAvailable(code);
     }
     
     /** @since 0.8.9 */
diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml
index bb7de443d01ab0406a78aeb6458da964f0e89e4c..fd9a9a45f70822c26c058c7b3313a8251ed80239 100644
--- a/apps/jetty/build.xml
+++ b/apps/jetty/build.xml
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project basedir="." default="all" name="jetty">
 
-    <property name="jetty.ver" value="8.1.15.v20140411" />
+    <property name="jetty.ver" value="8.1.16.v20140903" />
     <property name="jetty.base" value="jetty-distribution-${jetty.ver}" />
-    <property name="jetty.sha1" value="41ec2b5e5605c038fb28d1f118669f06b4479e71" />
+    <property name="jetty.sha1" value="5440b33a722d82b746b9ce50168bfce3c22af349" />
     <property name="jetty.filename" value="${jetty.base}.zip" />
     <property name="jetty.url" value="http://download.eclipse.org/jetty/${jetty.ver}/dist/${jetty.filename}" />
     <property name="verified.filename" value="verified.txt" />
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar
similarity index 79%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar
index 5dceaf49ce9bf3acfeee7254147cae77df59b25e..ce1acb18bcbf14740de5d705e854c1124005197f 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar
similarity index 69%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar
index d83efa1931f794528c3a8cecb98f24b1f9cc3a3e..d3339b061d852a18edc588a0268ee8bb2aff9f83 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar
similarity index 81%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar
index 330344363ead1e70b00ededaa974c8195e1bf667..30189c766c563200ce13ff1519a37092aee041e4 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar
similarity index 89%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar
index f2554fa395191179b6ac56938fb9e7c2331174f7..a9afd7cc3a610558c910eccb3f3291fc5f4432a7 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar
similarity index 79%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar
index a00233fbf7670d1c5a74812f9444caa38668fb18..f0eb0c51a413ca1607a580d30a3d321557d64ddd 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar
similarity index 83%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar
index 5004187daadd66db8fbfcf793a66dc805a98f87f..240c6705204a7a601db7de2a33c6ef754e21d66e 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar
similarity index 88%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar
index bafcf7293c3d245ab443e7e87c996611e8aa8fb4..e5bde43b82e74f0fa1b09f4072d1a7a7b5854dc2 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar
similarity index 87%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar
index ded9cf48906ffa961558cd90e1e40a7462e1301f..ae8ac5577764dbcb31ce55877eca68673274bdc2 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar
similarity index 91%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar
index 556dc68fe2398ea56bdc2e694e2e6897ab61629f..eb2fa57db19be8a66f83401be190982302bc5eb4 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar
similarity index 90%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar
index d30991166a5b7b7a0f74ab455d0083af52daf027..6fd7146ed492621dadfc059155915f99209ca54c 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar
similarity index 91%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar
index 170d66fc2b08954d420a3ec3b74fc2bad94d4e12..5c3c3460f2f748b302702248d6cd115e140c1c75 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar
similarity index 91%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar
index 17ebd18e7a108b039c9c5e93cb0e5b31aba27a40..85fd7e0982c914521f166d521437364c285a7aae 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar
similarity index 88%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar
index 5f922235da011efd2eb1ac6826805a68041db35f..1e485de6d7bf20d3af4f2a0725ce401a4e293132 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar differ
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar
similarity index 100%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/servlet-api-3.0.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/servlet-api-3.0.jar
similarity index 100%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/servlet-api-3.0.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/servlet-api-3.0.jar
diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar
similarity index 90%
rename from apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar
rename to apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar
index 9c16152913ecb7fa8ace96f827d300d2262e84f7..8ea5baa136932c0ec385691beaeef623059b57af 100644
Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar differ
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
index efe9f4f819dacf5c8c52a82697b8e0816714cabe..7f13242d3dab6f000ec75610012ef5abf52a8358 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
@@ -5,7 +5,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.Router;
 import net.i2p.router.transport.TransportManager;
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 9fae3ab935a7bedce85f1dc42224368013084e36..712830f59a8c0cca05d73d9ed35ef66a2f4cd299 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -29,8 +29,8 @@ import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.Lease;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
 import net.i2p.router.util.HashDistance;   // debug
@@ -199,10 +199,10 @@ public class NetDbRenderer {
             FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb();
             buf.append("<p><b>Total Leasesets: ").append(leases.size());
             buf.append("</b></p><p><b>Published (RAP) Leasesets: ").append(netdb.getKnownLeaseSets());
-            buf.append("</b></p><p><b>Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getModData()))
-               .append("\" Last Changed: ").append(new Date(_context.routingKeyGenerator().getLastChanged()));
-            buf.append("</b></p><p><b>Next Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getNextModData()))
-               .append("\" Change in: ").append(DataHelper.formatDuration(_context.routingKeyGenerator().getTimeTillMidnight()));
+            buf.append("</b></p><p><b>Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
+               .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged()));
+            buf.append("</b></p><p><b>Next Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData()))
+               .append("\" Change in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight()));
             int ff = _context.peerManager().getPeersByCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL).size();
             buf.append("</b></p><p><b>Known Floodfills: ").append(ff);
             buf.append("</b></p><p><b>Currently Floodfill? ");
@@ -415,7 +415,9 @@ public class NetDbRenderer {
             // shouldnt happen
             buf.append("<b>" + _("Published") + ":</b> in ").append(DataHelper.formatDuration2(0-age)).append("???<br>\n");
         }
-        buf.append("<b>" + _("Address(es)") + ":</b> ");
+        buf.append("<b>").append(_("Signing Key")).append(":</b> ")
+           .append(info.getIdentity().getSigningPublicKey().getType().toString());
+        buf.append("<br>\n<b>" + _("Address(es)") + ":</b> ");
         String country = _context.commSystem().getCountry(info.getIdentity().getHash());
         if(country != null) {
             buf.append("<img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase(Locale.US)).append('\"');
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java
index e2cd422705e6188bea878a31d40c2298ecb7033d..1a5461246233f301712523774730f01d93152ca0 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java
@@ -10,7 +10,7 @@ import java.util.TreeSet;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.RouterContext;
 import net.i2p.router.peermanager.DBHistory;
 import net.i2p.router.peermanager.PeerProfile;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java
index 70ab9a1af68e147bb636a3d7f1f3963e65992b96..01ed5836ad11ae81d9d2febc8c5828965d1f329f 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java
@@ -3,8 +3,8 @@ package net.i2p.router.web;
 import java.util.Date;
 
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.Signature;
 
 /**
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index c60ded011077de2304d9d33fbf8d08d893f86465..953fdb34f74cd8f7a7e848b05e54292c0db6924f 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -15,8 +15,8 @@ import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java
index 1b2b7b23026716892f8f4a47fc7501a7620c60e0..9fff49d1e5de8dd6b45b6534a924a4c0cd3e2317 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java
@@ -11,7 +11,7 @@ import java.util.Map;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
index 102bfa6cc18bcaaf63d8bcdf8ea10a3a676a1b55..51b8baef730071efd4f5031497b5aab72eaac95c 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
@@ -44,7 +44,7 @@ class PacketQueue implements SendMessageStatusListener {
     private static final int FINAL_TAGS_TO_SEND = 4;
     private static final int FINAL_TAG_THRESHOLD = 2;
     private static final long REMOVE_EXPIRED_TIME = 67*1000;
-    private static final boolean ENABLE_STATUS_LISTEN = false;
+    private static final boolean ENABLE_STATUS_LISTEN = true;
 
     public PacketQueue(I2PAppContext context, I2PSession session, ConnectionManager mgr) {
         _context = context;
@@ -267,6 +267,20 @@ class PacketQueue implements SendMessageStatusListener {
                 _messageStatusMap.remove(id);
                 break;
 
+            case MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET:
+                // Ideally we would like to make this a hard failure,
+                // but it caused far too many fast-fails that were then
+                // resolved by the user clicking reload in his browser.
+                // Until the LS fetch is faster and more reliable,
+                // or we increase the timeout for it,
+                // we can't treat this one as a hard fail.
+                // Let the streaming retransmission paper over the problem.
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("LS lookup (soft) failure for msg " + msgId + " on " + con);
+                _messageStatusMap.remove(id);
+                break;
+
+
             case MessageStatusMessage.STATUS_SEND_FAILURE_LOCAL:
             case MessageStatusMessage.STATUS_SEND_FAILURE_ROUTER:
             case MessageStatusMessage.STATUS_SEND_FAILURE_NETWORK:
@@ -280,7 +294,6 @@ class PacketQueue implements SendMessageStatusListener {
             case MessageStatusMessage.STATUS_SEND_FAILURE_DESTINATION:
             case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_LEASESET:
             case MessageStatusMessage.STATUS_SEND_FAILURE_EXPIRED_LEASESET:
-            case MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET:
             case SendMessageStatusListener.STATUS_CANCELLED:
                 if (con.getHighestAckedThrough() >= 0) {
                     // a retxed SYN succeeded before the first SYN failed
diff --git a/build.xml b/build.xml
index 873284998af3356231de4d296e4380dd0e5745db..9f3364fe4e2d002e7c6b2cff37fd0a15afbe8ba5 100644
--- a/build.xml
+++ b/build.xml
@@ -548,7 +548,7 @@
             windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}">
             <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.oldcrypto:org.bouncycastle.oldcrypto.*:gnu.crypto.*:gnu.getopt:gnu.gettext:com.nettgryppa.security:net.metanotion:net.metanotion.*" />
             <group title="Streaming Library" packages="net.i2p.client.streaming:net.i2p.client.streaming.impl" />
-            <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters" />
+            <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:net.i2p.data.router:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters" />
             <group title="Router Console" packages="net.i2p.router.web:net.i2p.router.update" />
             <!-- apps and bridges starting here, alphabetical please -->
             <group title="Addressbook Application" packages="net.i2p.addressbook" />
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 8357a485ac14110622603bf06e90e7c1153c3942..79c3108f78d9c4b27d530056819aa8ce053129b5 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -86,7 +86,6 @@ public class I2PAppContext {
     private SHA256Generator _sha;
     protected Clock _clock; // overridden in RouterContext
     private DSAEngine _dsa;
-    private RoutingKeyGenerator _routingKeyGenerator;
     private RandomSource _random;
     private KeyGenerator _keyGenerator;
     protected KeyRing _keyRing; // overridden in RouterContext
@@ -106,7 +105,6 @@ public class I2PAppContext {
     private volatile boolean _shaInitialized;
     protected volatile boolean _clockInitialized; // used in RouterContext
     private volatile boolean _dsaInitialized;
-    private volatile boolean _routingKeyGeneratorInitialized;
     private volatile boolean _randomInitialized;
     private volatile boolean _keyGeneratorInitialized;
     protected volatile boolean _keyRingInitialized; // used in RouterContext
@@ -126,7 +124,7 @@ public class I2PAppContext {
     private final Object _lock1 = new Object(), _lock2 = new Object(), _lock3 = new Object(), _lock4 = new Object(),
                          _lock5 = new Object(), _lock6 = new Object(), _lock7 = new Object(), _lock8 = new Object(),
                          _lock9 = new Object(), _lock10 = new Object(), _lock11 = new Object(), _lock12 = new Object(),
-                         _lock13 = new Object(), _lock14 = new Object(), _lock15 = new Object(), _lock16 = new Object(),
+                         _lock13 = new Object(), _lock14 = new Object(), _lock16 = new Object(),
                          _lock17 = new Object(), _lock18 = new Object(), _lock19 = new Object(), _lock20 = new Object();
 
     /**
@@ -851,19 +849,13 @@ public class I2PAppContext {
      * may want to test out how things react when peers don't agree on 
      * how to skew.
      *
+     * As of 0.9.16, returns null in I2PAppContext.
+     * You must be in RouterContext to get a generator.
+     *
+     * @return null always
      */
     public RoutingKeyGenerator routingKeyGenerator() {
-        if (!_routingKeyGeneratorInitialized)
-            initializeRoutingKeyGenerator();
-        return _routingKeyGenerator;
-    }
-
-    private void initializeRoutingKeyGenerator() {
-        synchronized (_lock15) {
-            if (_routingKeyGenerator == null)
-                _routingKeyGenerator = new RoutingKeyGenerator(this);
-            _routingKeyGeneratorInitialized = true;
-        }
+        return null;
     }
     
     /**
diff --git a/core/java/src/net/i2p/crypto/ECUtil.java b/core/java/src/net/i2p/crypto/ECUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d22284804bd835ef3793f252c6a6d0d353f8bf9
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/ECUtil.java
@@ -0,0 +1,135 @@
+package net.i2p.crypto;
+
+import java.math.BigInteger;
+import java.security.spec.ECField;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+
+import net.i2p.util.NativeBigInteger;
+
+/**
+ *  Used by KeyGenerator.getSigningPublicKey()
+ *
+ *  Modified from
+ *  http://stackoverflow.com/questions/15727147/scalar-multiplication-of-point-over-elliptic-curve
+ *  Apparently public domain.
+ *  Supported P-192 only.
+ *  Added curve parameters to support all curves.
+ *
+ *  @since 0.9.16
+ */
+class ECUtil {
+
+    private static final BigInteger TWO = new BigInteger("2");
+    private static final BigInteger THREE = new BigInteger("3");
+
+    public static ECPoint scalarMult(ECPoint p, BigInteger kin, EllipticCurve curve) {
+        ECPoint r = ECPoint.POINT_INFINITY;
+        BigInteger prime = ((ECFieldFp) curve.getField()).getP();
+        BigInteger k = kin.mod(prime);
+        int length = k.bitLength();
+        byte[] binarray = new byte[length];
+        for (int i = 0; i <= length-1; i++) {
+            binarray[i] = k.mod(TWO).byteValue();
+            k = k.divide(TWO);
+        }
+
+        for (int i = length-1; i >= 0; i--) {
+            // i should start at length-1 not -2 because the MSB of binarry may not be 1
+            r = doublePoint(r, curve);
+            if (binarray[i] == 1) 
+                r = addPoint(r, p, curve);
+        }
+        return r;
+    }
+
+    private static ECPoint addPoint(ECPoint r, ECPoint s, EllipticCurve curve) {
+        if (r.equals(s))
+            return doublePoint(r, curve);
+        else if (r.equals(ECPoint.POINT_INFINITY))
+            return s;
+        else if (s.equals(ECPoint.POINT_INFINITY))
+            return r;
+        BigInteger prime = ((ECFieldFp) curve.getField()).getP();
+        BigInteger slope = (r.getAffineY().subtract(s.getAffineY())).multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(prime)).mod(prime);
+        slope = new NativeBigInteger(slope);
+        BigInteger xOut = (slope.modPow(TWO, prime).subtract(r.getAffineX())).subtract(s.getAffineX()).mod(prime);
+        BigInteger yOut = s.getAffineY().negate().mod(prime);
+        yOut = yOut.add(slope.multiply(s.getAffineX().subtract(xOut))).mod(prime);
+        ECPoint out = new ECPoint(xOut, yOut);
+        return out;
+    }
+
+    private static ECPoint doublePoint(ECPoint r, EllipticCurve curve) {
+        if (r.equals(ECPoint.POINT_INFINITY)) 
+            return r;
+        BigInteger slope = (r.getAffineX().pow(2)).multiply(THREE);
+        slope = slope.add(curve.getA());
+        BigInteger prime = ((ECFieldFp) curve.getField()).getP();
+        slope = slope.multiply((r.getAffineY().multiply(TWO)).modInverse(prime));
+        BigInteger xOut = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(prime);
+        BigInteger yOut = (r.getAffineY().negate()).add(slope.multiply(r.getAffineX().subtract(xOut))).mod(prime);
+        ECPoint out = new ECPoint(xOut, yOut);
+        return out;
+    }
+
+    /**
+     *  P-192 test only.
+     *  See KeyGenerator.main() for a test of all supported curves.
+     */
+/****
+    public static void main(String[] args) {
+        EllipticCurve P192 = ECConstants.P192_SPEC.getCurve();
+        BigInteger xs = new BigInteger("d458e7d127ae671b0c330266d246769353a012073e97acf8", 16);
+        BigInteger ys = new BigInteger("325930500d851f336bddc050cf7fb11b5673a1645086df3b", 16);
+        BigInteger xt = new BigInteger("f22c4395213e9ebe67ddecdd87fdbd01be16fb059b9753a4", 16);
+        BigInteger yt = new BigInteger("264424096af2b3597796db48f8dfb41fa9cecc97691a9c79", 16);
+        ECPoint S = new ECPoint(xs,ys);
+        ECPoint T = new ECPoint(xt,yt);
+
+        // Verifying addition 
+        ECPoint Rst = addPoint(S, T, P192);
+        BigInteger xst = new BigInteger("48e1e4096b9b8e5ca9d0f1f077b8abf58e843894de4d0290", 16);   // Specified value of x of point R for addition  in NIST Routine example
+        System.out.println("x-coordinate of point Rst is : " + Rst.getAffineX()); 
+        System.out.println("y-coordinate of point Rst is : " + Rst.getAffineY());
+        if (Rst.getAffineX().equals(xst))
+            System.out.println("Adding is correct");
+        else
+            System.out.println("Adding FAIL");
+
+        //Verifying Doubling
+        BigInteger xr = new BigInteger("30c5bc6b8c7da25354b373dc14dd8a0eba42d25a3f6e6962", 16);  // Specified value of x of point R for doubling  in NIST Routine example
+        BigInteger yr = new BigInteger("0dde14bc4249a721c407aedbf011e2ddbbcb2968c9d889cf", 16);
+        ECPoint R2s = new ECPoint(xr, yr);  // Specified value of y of point R for doubling  in NIST Routine example
+        System.out.println("x-coordinate of point R2s is : " + R2s.getAffineX()); 
+        System.out.println("y-coordinate of point R2s is : " + R2s.getAffineY());
+        System.out.println("x-coordinate of calculated point is : " + doublePoint(S, P192).getAffineX()); 
+        System.out.println("y-coordinate of calculated point is : " +    doublePoint(S, P192).getAffineY());
+        if (R2s.getAffineX().equals(doublePoint(S, P192).getAffineX()) &&
+            R2s.getAffineY().equals(doublePoint(S, P192).getAffineY()))
+            System.out.println("Doubling is correct");
+        else
+            System.out.println("Doubling FAIL");
+
+        xr = new BigInteger("1faee4205a4f669d2d0a8f25e3bcec9a62a6952965bf6d31", 16);  // Specified value of x of point R for scalar Multiplication  in NIST Routine example
+        yr = new BigInteger("5ff2cdfa508a2581892367087c696f179e7a4d7e8260fb06", 16);   // Specified value of y of point R for scalar Multiplication  in NIST Routine example
+        ECPoint Rds = new ECPoint(xr, yr);
+        BigInteger d = new BigInteger("a78a236d60baec0c5dd41b33a542463a8255391af64c74ee", 16);
+
+        ECPoint Rs = scalarMult(S, d, P192);
+
+        System.out.println("x-coordinate of point Rds is : " + Rds.getAffineX());
+        System.out.println("y-coordinate of point Rds is : " + Rds.getAffineY());
+        System.out.println("x-coordinate of calculated point is : " + Rs.getAffineX());
+        System.out.println("y-coordinate of calculated point is : " + Rs.getAffineY()); 
+
+
+        if (Rds.getAffineX().equals(Rs.getAffineX()) &&
+            Rds.getAffineY().equals(Rs.getAffineY()))
+            System.out.println("Scalar Multiplication is correct");
+        else
+            System.out.println("Scalar Multiplication FAIL");
+    }
+****/
+}
diff --git a/core/java/src/net/i2p/crypto/ElGamalEngine.java b/core/java/src/net/i2p/crypto/ElGamalEngine.java
index e1ac37aadd528bbc8842bc16a7dec83ef85b8c61..a80e0a99eae998bacdf7f6fa2690ecb1bbd1f6cb 100644
--- a/core/java/src/net/i2p/crypto/ElGamalEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalEngine.java
@@ -56,6 +56,9 @@ public class ElGamalEngine {
     private final Log _log;
     private final I2PAppContext _context;
     private final YKGenerator _ykgen;
+
+    private static final BigInteger ELGPM1 = CryptoConstants.elgp.subtract(BigInteger.ONE);
+
     
     /** 
      * The ElGamal engine should only be constructed and accessed through the 
@@ -171,10 +174,11 @@ public class ElGamalEngine {
             if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to encrypt ElGamal block (" + diff + "ms)");
         }
 
-        _context.statManager().addRateData("crypto.elGamal.encrypt", diff, 0);
+        _context.statManager().addRateData("crypto.elGamal.encrypt", diff);
         return out;
     }
 
+
     /** Decrypt the data
      * @param encrypted encrypted data, must be exactly 514 bytes
      *         Contains the two-part encrypted data starting at bytes 0 and 257.
@@ -184,26 +188,26 @@ public class ElGamalEngine {
      * @return unencrypted data or null on failure
      */
     public byte[] decrypt(byte encrypted[], PrivateKey privateKey) {
-        // actually it must be exactly 514 bytes or the arraycopy below will AIOOBE
-        if ((encrypted == null) || (encrypted.length > 514))
-            throw new IllegalArgumentException("Data to decrypt must be <= 514 bytes at the moment");
+        if ((encrypted == null) || (encrypted.length != 514))
+            throw new IllegalArgumentException("Data to decrypt must be exactly 514 bytes");
         long start = _context.clock().now();
 
-        byte[] ybytes = new byte[257];
-        byte[] dbytes = new byte[257];
-        System.arraycopy(encrypted, 0, ybytes, 0, 257);
-        System.arraycopy(encrypted, 257, dbytes, 0, 257);
-        BigInteger y = new NativeBigInteger(1, ybytes);
-        BigInteger d = new NativeBigInteger(1, dbytes);
         BigInteger a = new NativeBigInteger(1, privateKey.getData());
-        BigInteger y1p = CryptoConstants.elgp.subtract(BigInteger.ONE).subtract(a);
+        BigInteger y1p = ELGPM1.subtract(a);
+        // we use this buf first for Y, then for D, then for the hash
+        byte[] buf = SimpleByteCache.acquire(257);
+        System.arraycopy(encrypted, 0, buf, 0, 257);
+        BigInteger y = new NativeBigInteger(1, buf);
         BigInteger ya = y.modPow(y1p, CryptoConstants.elgp);
+        System.arraycopy(encrypted, 257, buf, 0, 257);
+        BigInteger d = new NativeBigInteger(1, buf);
         BigInteger m = ya.multiply(d);
         m = m.mod(CryptoConstants.elgp);
         byte val[] = m.toByteArray();
-        int i = 0;
-        for (i = 0; i < val.length; i++)
+        int i;
+        for (i = 0; i < val.length; i++) {
             if (val[i] != (byte) 0x00) break;
+        }
 
         int payloadLen = val.length - i - 1 - Hash.HASH_LENGTH;
         if (payloadLen < 0) {
@@ -220,10 +224,10 @@ public class ElGamalEngine {
         byte rv[] = new byte[payloadLen];
         System.arraycopy(val, i + 1 + Hash.HASH_LENGTH, rv, 0, rv.length);
 
-        byte[] calcHash = SimpleByteCache.acquire(Hash.HASH_LENGTH);
-        _context.sha().calculateHash(rv, 0, payloadLen, calcHash, 0);
-        boolean ok = DataHelper.eq(calcHash, 0, val, i + 1, Hash.HASH_LENGTH);
-        SimpleByteCache.release(calcHash);
+        // we reuse buf here for the calculated hash
+        _context.sha().calculateHash(rv, 0, payloadLen, buf, 0);
+        boolean ok = DataHelper.eq(buf, 0, val, i + 1, Hash.HASH_LENGTH);
+        SimpleByteCache.release(buf);
         
         long end = _context.clock().now();
 
@@ -233,7 +237,7 @@ public class ElGamalEngine {
                 _log.warn("Took too long to decrypt and verify ElGamal block (" + diff + "ms)");
         }
 
-        _context.statManager().addRateData("crypto.elGamal.decrypt", diff, 0);
+        _context.statManager().addRateData("crypto.elGamal.decrypt", diff);
 
         if (ok) {
             //_log.debug("Hash matches: " + DataHelper.toString(hash.getData(), hash.getData().length));
diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java
index f078aaa5c036d9ff6d0a9d20c2f71830f59c7a88..c23a215232f59995d4d1b6834c983727163b8b3f 100644
--- a/core/java/src/net/i2p/crypto/KeyGenerator.java
+++ b/core/java/src/net/i2p/crypto/KeyGenerator.java
@@ -12,11 +12,25 @@ package net.i2p.crypto;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
+import java.security.KeyFactory;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.ProviderException;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.security.spec.RSAPublicKeySpec;
 
 import net.i2p.I2PAppContext;
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
 import net.i2p.data.Hash;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
@@ -268,24 +282,56 @@ public class KeyGenerator {
     }
 
     /** Convert a SigningPrivateKey to a SigningPublicKey.
-     * DSA-SHA1 only.
+     *  As of 0.9.16, supports all key types.
      *
      * @param priv a SigningPrivateKey object
      * @return a SigningPublicKey object
-     * @throws IllegalArgumentException on bad key
+     * @throws IllegalArgumentException on bad key or unknown type
      */
     public static SigningPublicKey getSigningPublicKey(SigningPrivateKey priv) {
-        if (priv.getType() != SigType.DSA_SHA1)
-            throw new IllegalArgumentException();
-        BigInteger x = new NativeBigInteger(1, priv.toByteArray());
-        BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap);
-        SigningPublicKey pub = new SigningPublicKey();
+        SigType type = priv.getType();
+        if (type == null)
+            throw new IllegalArgumentException("Unknown type");
         try {
-            pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES));
-        } catch (InvalidKeyException ike) {
-            throw new IllegalArgumentException(ike);
+            switch (type.getBaseAlgorithm()) {
+              case DSA:
+                BigInteger x = new NativeBigInteger(1, priv.toByteArray());
+                BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap);
+                SigningPublicKey pub = new SigningPublicKey();
+                pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES));
+                return pub;
+
+              case EC:
+                ECPrivateKey ecpriv = SigUtil.toJavaECKey(priv);
+                BigInteger s = ecpriv.getS();
+                ECParameterSpec spec = (ECParameterSpec) type.getParams();
+                EllipticCurve curve = spec.getCurve();
+                ECPoint g = spec.getGenerator();
+                ECPoint w = ECUtil.scalarMult(g, s, curve);
+                ECPublicKeySpec ecks = new ECPublicKeySpec(w, ecpriv.getParams());
+                KeyFactory eckf = KeyFactory.getInstance("EC");
+                ECPublicKey ecpub = (ECPublicKey) eckf.generatePublic(ecks);
+                return SigUtil.fromJavaKey(ecpub, type);
+
+              case RSA:
+                RSAPrivateKey rsapriv = SigUtil.toJavaRSAKey(priv);
+                BigInteger exp = ((RSAKeyGenParameterSpec)type.getParams()).getPublicExponent();
+                RSAPublicKeySpec rsaks = new RSAPublicKeySpec(rsapriv.getModulus(), exp);
+                KeyFactory rsakf = KeyFactory.getInstance("RSA");
+                RSAPublicKey rsapub = (RSAPublicKey) rsakf.generatePublic(rsaks);
+                return SigUtil.fromJavaKey(rsapub, type);
+
+              case EdDSA:
+                EdDSAPrivateKey epriv = SigUtil.toJavaEdDSAKey(priv);
+                EdDSAPublicKey epub = new EdDSAPublicKey(new EdDSAPublicKeySpec(epriv.getA(), epriv.getParams()));
+                return SigUtil.fromJavaKey(epub, type);
+
+              default:
+                throw new IllegalArgumentException("Unsupported algorithm");
+            }
+        } catch (GeneralSecurityException gse) {
+            throw new IllegalArgumentException("Conversion failed", gse);
         }
-        return pub;
     }
 
     public static void main(String args[]) {
@@ -322,14 +368,20 @@ public class KeyGenerator {
         long stime = 0;
         long vtime = 0;
         SimpleDataStructure keys[] = KeyGenerator.getInstance().generateSigningKeys(type);
-        //System.out.println("pubkey " + keys[0]);
+        SigningPublicKey pubkey = (SigningPublicKey) keys[0];
+        SigningPrivateKey privkey = (SigningPrivateKey) keys[1];
+        SigningPublicKey pubkey2 = getSigningPublicKey(privkey);
+        if (pubkey.equals(pubkey2))
+            System.out.println(type + " private-to-public test PASSED");
+        else
+            System.out.println(type + " private-to-public test FAILED");
         //System.out.println("privkey " + keys[1]);
         for (int i = 0; i < runs; i++) {
             RandomSource.getInstance().nextBytes(src);
             long start = System.nanoTime();
-            Signature sig = DSAEngine.getInstance().sign(src, (SigningPrivateKey) keys[1]);
+            Signature sig = DSAEngine.getInstance().sign(src, privkey);
             long mid = System.nanoTime();
-            boolean ok = DSAEngine.getInstance().verifySignature(sig, src, (SigningPublicKey) keys[0]);
+            boolean ok = DSAEngine.getInstance().verifySignature(sig, src, pubkey);
             long end = System.nanoTime();
             stime += mid - start;
             vtime += end - mid;
diff --git a/core/java/src/net/i2p/crypto/SigUtil.java b/core/java/src/net/i2p/crypto/SigUtil.java
index 09cc3ed781b213980cb2bb804d7d171b67bd2c73..c492dc663066419c56009a59ee0063dff9fe51b7 100644
--- a/core/java/src/net/i2p/crypto/SigUtil.java
+++ b/core/java/src/net/i2p/crypto/SigUtil.java
@@ -345,7 +345,7 @@ public class SigUtil {
     }
 
     /**
-     *  @deprecated unused
+     *
      */
     public static RSAPrivateKey toJavaRSAKey(SigningPrivateKey pk)
                               throws GeneralSecurityException {
@@ -358,7 +358,7 @@ public class SigUtil {
     }
 
     /**
-     *  @deprecated unused
+     *
      */
     public static SigningPublicKey fromJavaKey(RSAPublicKey pk, SigType type)
                               throws GeneralSecurityException {
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index d3dc3685831d09b175077cd041f9ec71bb906f40..ac51f8128cf0acd46ab09206156259a3c9fee9a6 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -24,20 +24,15 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
 import java.math.BigInteger;
 import java.security.MessageDigest;
 import java.text.DecimalFormat;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
@@ -638,13 +633,17 @@ public class DataHelper {
      * Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order.
      * @param value value to write out, non-negative
      * @param rawStream stream to write to
-     * @param numBytes number of bytes to write the number into (padding as necessary)
-     * @throws DataFormatException if value is negative
+     * @param numBytes number of bytes to write the number into, 1-8 (padding as necessary)
+     * @throws DataFormatException if value is negative or if numBytes not 1-8
      * @throws IOException if there is an IO error writing to the stream
      */
     public static void writeLong(OutputStream rawStream, int numBytes, long value) 
         throws DataFormatException, IOException {
-        if (value < 0) throw new DataFormatException("Value is negative (" + value + ")");
+        if (numBytes <= 0 || numBytes > 8)
+            // probably got the args backwards
+            throw new DataFormatException("Bad byte count " + numBytes);
+        if (value < 0)
+            throw new DataFormatException("Value is negative (" + value + ")");
         for (int i = (numBytes - 1) * 8; i >= 0; i -= 8) {
             byte cur = (byte) (value >> i);
             rawStream.write(cur);
@@ -667,7 +666,7 @@ public class DataHelper {
      * @param value non-negative
      */
     public static void toLong(byte target[], int offset, int numBytes, long value) throws IllegalArgumentException {
-        if (numBytes <= 0) throw new IllegalArgumentException("Invalid number of bytes");
+        if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes");
         if (value < 0) throw new IllegalArgumentException("Negative value not allowed");
 
         for (int i = offset + numBytes - 1; i >= offset; i--) {
@@ -1425,58 +1424,6 @@ public class DataHelper {
         out.write(data);
     }
 
-    /**
-     *  Sort based on the Hash of the DataStructure.
-     *  Warning - relatively slow.
-     *  WARNING - this sort order must be consistent network-wide, so while the order is arbitrary,
-     *  it cannot be changed.
-     *  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.emptyList();
-
-        // This used to use Hash.toString(), which is insane, since a change to toString()
-        // would break the whole network. Now use Hash.toBase64().
-        // Note that the Base64 sort order is NOT the same as the raw byte sort order,
-        // despite what you may read elsewhere.
-
-        //ArrayList<DataStructure> rv = new ArrayList(dataStructures.size());
-        //TreeMap<String, DataStructure> tm = new TreeMap();
-        //for (DataStructure struct : dataStructures) {
-        //    tm.put(struct.calculateHash().toString(), struct);
-        //}
-        //for (DataStructure struct : tm.values()) {
-        //    rv.add(struct);
-        //}
-        ArrayList<DataStructure> rv = new ArrayList<DataStructure>(dataStructures);
-        sortStructureList(rv);
-        return rv;
-    }
-
-    /**
-     *  See above.
-     *  DEPRECATED - Only used by RouterInfo.
-     *
-     *  @since 0.9
-     */
-    static void sortStructureList(List<? extends DataStructure> dataStructures) {
-        Collections.sort(dataStructures, new DataStructureComparator());
-    }
-
-    /**
-     * See sortStructures() comments.
-     * @since 0.8.3
-     */
-    private static class DataStructureComparator implements Comparator<DataStructure>, Serializable {
-        public int compare(DataStructure l, DataStructure r) {
-            return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64());
-        }
-    }
-
     /**
      *  NOTE: formatDuration2() recommended in most cases for readability
      */
diff --git a/core/java/src/net/i2p/data/DatabaseEntry.java b/core/java/src/net/i2p/data/DatabaseEntry.java
index 4b84ed106e0775a620f2b112794a1486ac384968..2adc066bfee05f4b1243e3030cbcfb7e992cd9c4 100644
--- a/core/java/src/net/i2p/data/DatabaseEntry.java
+++ b/core/java/src/net/i2p/data/DatabaseEntry.java
@@ -11,6 +11,7 @@ package net.i2p.data;
 
 import java.util.Arrays;
 
+import net.i2p.I2PAppContext;
 import net.i2p.crypto.DSAEngine;
 
 /**
@@ -47,7 +48,7 @@ public abstract class DatabaseEntry extends DataStructureImpl {
 
     protected volatile Signature _signature;
     protected volatile Hash _currentRoutingKey;
-    protected volatile byte[] _routingKeyGenMod;
+    protected volatile long _routingKeyGenMod;
 
     /**
      * A common interface to the timestamp of the two subclasses.
@@ -106,11 +107,15 @@ public abstract class DatabaseEntry extends DataStructureImpl {
      * Get the routing key for the structure using the current modifier in the RoutingKeyGenerator.
      * This only calculates a new one when necessary though (if the generator's key modifier changes)
      *
+     * @throws IllegalStateException if not in RouterContext
      */
     public Hash getRoutingKey() {
-        RoutingKeyGenerator gen = RoutingKeyGenerator.getInstance();
-        byte[] mod = gen.getModData();
-        if (!Arrays.equals(mod, _routingKeyGenMod)) {
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        if (!ctx.isRouterContext())
+            throw new IllegalStateException("Not in router context");
+        RoutingKeyGenerator gen = ctx.routingKeyGenerator();
+        long mod = gen.getLastChanged();
+        if (mod != _routingKeyGenMod) {
             _currentRoutingKey = gen.getRoutingKey(getHash());
             _routingKeyGenMod = mod;
         }
@@ -124,9 +129,16 @@ public abstract class DatabaseEntry extends DataStructureImpl {
         _currentRoutingKey = key;
     }
 
+    /**
+     * @throws IllegalStateException if not in RouterContext
+     */
     public boolean validateRoutingKey() {
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        if (!ctx.isRouterContext())
+            throw new IllegalStateException("Not in router context");
+        RoutingKeyGenerator gen = ctx.routingKeyGenerator();
         Hash destKey = getHash();
-        Hash rk = RoutingKeyGenerator.getInstance().getRoutingKey(destKey);
+        Hash rk = gen.getRoutingKey(destKey);
         return rk.equals(getRoutingKey());
     }
 
diff --git a/core/java/src/net/i2p/data/KeysAndCert.java b/core/java/src/net/i2p/data/KeysAndCert.java
index b0a8a845b7932ed3d7f6f2551fb7031eec4016eb..309799a01fa7099bbe85a589d5e333bd297a192c 100644
--- a/core/java/src/net/i2p/data/KeysAndCert.java
+++ b/core/java/src/net/i2p/data/KeysAndCert.java
@@ -77,6 +77,13 @@ public class KeysAndCert extends DataStructureImpl {
         _signingKey = key;
     }
     
+    /**
+     * @since 0.9.16
+     */
+    public byte[] getPadding() {
+        return _padding;
+    }
+    
     /**
      * @throws IllegalStateException if was already set
      * @since 0.9.12
@@ -114,6 +121,8 @@ public class KeysAndCert extends DataStructureImpl {
         _publicKey.writeBytes(out);
         if (_padding != null)
             out.write(_padding);
+        else if (_signingKey.length() < SigningPublicKey.KEYSIZE_BYTES)
+            throw new DataFormatException("No padding set");
         _signingKey.writeTruncatedBytes(out);
         _certificate.writeBytes(out);
     }
diff --git a/core/java/src/net/i2p/data/PrivateKey.java b/core/java/src/net/i2p/data/PrivateKey.java
index f10248189ec18674bc312b16f6fbdc330b16cc5c..163edcc3f5dc791dd3be60042021b5b1c2bec74e 100644
--- a/core/java/src/net/i2p/data/PrivateKey.java
+++ b/core/java/src/net/i2p/data/PrivateKey.java
@@ -50,6 +50,7 @@ public class PrivateKey extends SimpleDataStructure {
     /** derives a new PublicKey object derived from the secret contents
      * of this PrivateKey
      * @return a PublicKey object
+     * @throws IllegalArgumentException on bad key
      */
     public PublicKey toPublic() {
         return KeyGenerator.getPublicKey(this);
diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java
index 012310b9feee3dcf00f9dc991ebdc69198e3b50f..42a26ef367f6beb8a95e42040ea5d5a88b654272 100644
--- a/core/java/src/net/i2p/data/PrivateKeyFile.java
+++ b/core/java/src/net/i2p/data/PrivateKeyFile.java
@@ -1,11 +1,13 @@
 package net.i2p.data;
 
 
+import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
+import java.io.InputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.security.GeneralSecurityException;
 import java.util.Locale;
 import java.util.Map;
@@ -24,6 +26,7 @@ import net.i2p.crypto.DSAEngine;
 import net.i2p.crypto.KeyGenerator;
 import net.i2p.crypto.SigType;
 import net.i2p.util.RandomSource;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * This helper class reads and writes files in the
@@ -48,11 +51,11 @@ public class PrivateKeyFile {
     
     private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT;
     
-    private final File file;
+    protected final File file;
     private final I2PClient client;
-    private Destination dest;
-    private PrivateKey privKey;
-    private SigningPrivateKey signingPrivKey; 
+    protected Destination dest;
+    protected PrivateKey privKey;
+    protected SigningPrivateKey signingPrivKey; 
 
     /**
      *  Create a new PrivateKeyFile, or modify an existing one, with various
@@ -224,6 +227,16 @@ public class PrivateKeyFile {
      */
     public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert,
                           PrivateKey pk, SigningPrivateKey spk) {
+        this(file, pubkey, spubkey, cert, pk, spk, null);
+    }
+    
+    /**
+     *  @param padding null OK, must be non-null if spubkey length < 128
+     *  @throws IllegalArgumentException on mismatch of spubkey and spk types
+     *  @since 0.9.16
+     */
+    public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert,
+                          PrivateKey pk, SigningPrivateKey spk, byte[] padding) {
         if (spubkey.getType() != spk.getType())
             throw new IllegalArgumentException("Signing key type mismatch");
         this.file = file;
@@ -232,6 +245,8 @@ public class PrivateKeyFile {
         this.dest.setPublicKey(pubkey);
         this.dest.setSigningPublicKey(spubkey);
         this.dest.setCertificate(cert);
+        if (padding != null)
+            this.dest.setPadding(padding);
         this.privKey = pk;
         this.signingPrivKey = spk;
     }
@@ -241,9 +256,9 @@ public class PrivateKeyFile {
      */
     public Destination createIfAbsent() throws I2PException, IOException, DataFormatException {
         if(!this.file.exists()) {
-            FileOutputStream out = null;
+            OutputStream out = null;
             try {
-                out = new FileOutputStream(this.file);
+                out = new SecureFileOutputStream(this.file);
                 if (this.client != null)
                     this.client.createDestination(out);
                 else
@@ -257,7 +272,10 @@ public class PrivateKeyFile {
         return getDestination();
     }
     
-    /** Also sets the local privKey and signingPrivKey */
+    /**
+     *  If the destination is not set, read it in from the file.
+     *  Also sets the local privKey and signingPrivKey.
+     */
     public Destination getDestination() throws I2PSessionException, IOException, DataFormatException {
         if (dest == null) {
             I2PSession s = open();
@@ -408,9 +426,9 @@ public class PrivateKeyFile {
     }
 
     public I2PSession open(Properties opts) throws I2PSessionException, IOException {
-        FileInputStream in = null;
+        InputStream in = null;
         try {
-            in = new FileInputStream(this.file);
+            in = new BufferedInputStream(new FileInputStream(this.file));
             I2PSession s = this.client.createSession(in, opts);
             return s;
         } finally {
@@ -424,13 +442,12 @@ public class PrivateKeyFile {
      *  Copied from I2PClientImpl.createDestination()
      */
     public void write() throws IOException, DataFormatException {
-        FileOutputStream out = null;
+        OutputStream out = null;
         try {
-            out = new FileOutputStream(this.file);
+            out = new SecureFileOutputStream(this.file);
             this.dest.writeBytes(out);
             this.privKey.writeBytes(out);
             this.signingPrivKey.writeBytes(out);
-            out.flush();
         } finally {
             if (out != null) {
                 try { out.close(); } catch (IOException ioe) {}
@@ -438,6 +455,23 @@ public class PrivateKeyFile {
         }
     }
 
+    /**
+     *  Verify that the PublicKey matches the PrivateKey, and
+     *  the SigningPublicKey matches the SigningPrivateKey.
+     *
+     *  @return success
+     *  @since 0.9.16
+     */
+    public boolean validateKeyPairs() {
+        try {
+            if (!dest.getPublicKey().equals(KeyGenerator.getPublicKey(privKey)))
+                return false;
+            return dest.getSigningPublicKey().equals(KeyGenerator.getSigningPublicKey(signingPrivKey));
+        } catch (IllegalArgumentException iae) {
+            return false;
+        }
+    }
+
     @Override
     public String toString() {
         StringBuilder s = new StringBuilder(128);
diff --git a/core/java/src/net/i2p/data/RoutingKeyGenerator.java b/core/java/src/net/i2p/data/RoutingKeyGenerator.java
index 367089442a8fcda8346a039709d9947bd0323318..20e2218ee5548421c2011319ee1ecfb887b7110a 100644
--- a/core/java/src/net/i2p/data/RoutingKeyGenerator.java
+++ b/core/java/src/net/i2p/data/RoutingKeyGenerator.java
@@ -9,215 +9,40 @@ package net.i2p.data;
  *
  */
 
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Arrays;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.TimeZone;
-
 import net.i2p.I2PAppContext;
-import net.i2p.crypto.SHA256Generator;
-import net.i2p.util.HexDump;
-import net.i2p.util.Log;
 
 /**
  * Component to manage the munging of hashes into routing keys - given a hash, 
  * perform some consistent transformation against it and return the result.
  * This transformation is fed by the current "mod data".  
  *
- * Right now the mod data is the current date (GMT) as a string: "yyyyMMdd",
- * and the transformation takes the original hash, appends the bytes of that mod data,
- * then returns the SHA256 of that concatenation.
- *
- * Do we want this to simply do the XOR of the SHA256 of the current mod data and
- * the key?  does that provide the randomization we need?  It'd save an SHA256 op.
- * Bah, too much effort to think about for so little gain.  Other algorithms may come
- * into play layer on about making periodic updates to the routing key for data elements
- * to mess with Sybil.  This may be good enough though.
- *
- * Also - the method generateDateBasedModData() should be called after midnight GMT 
- * once per day to generate the correct routing keys!
- *
- * Warning - API subject to change. Not for use outside the router.
+ * As of 0.9.16, this is essentially just an interface.
+ * Implementation moved to net.i2p.data.router.RouterKeyGenerator.
+ * No generator is available in I2PAppContext; you must be in RouterContext.
  *
  */
-public class RoutingKeyGenerator {
-    private final Log _log;
-    private final I2PAppContext _context;
-
-    public RoutingKeyGenerator(I2PAppContext context) {
-        _log = context.logManager().getLog(RoutingKeyGenerator.class);
-        _context = context;
-        // ensure non-null mod data
-        generateDateBasedModData();
-    }
-
-    public static RoutingKeyGenerator getInstance() {
-        return I2PAppContext.getGlobalContext().routingKeyGenerator();
-    }
-    
-    private volatile byte _currentModData[];
-    private volatile byte _nextModData[];
-    private volatile long _nextMidnight;
-    private volatile long _lastChanged;
-
-    private final static Calendar _cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
-    private static final String FORMAT = "yyyyMMdd";
-    private static final int LENGTH = FORMAT.length();
-    private final static SimpleDateFormat _fmt = new SimpleDateFormat(FORMAT, Locale.US);
-    static {
-        // make sure GMT is set, azi2phelper Vuze plugin is disabling static JVM TZ setting in Router.java
-        _fmt.setCalendar(_cal);
-    }
-
-    /**
-     *  The current (today's) mod data.
-     *  Warning - not a copy, do not corrupt.
-     *
-     *  @return non-null, 8 bytes
-     */
-    public byte[] getModData() {
-        return _currentModData;
-    }
-
-    /**
-     *  Tomorrow's mod data.
-     *  Warning - not a copy, do not corrupt.
-     *  For debugging use only.
-     *
-     *  @return non-null, 8 bytes
-     *  @since 0.9.10
-     */
-    public byte[] getNextModData() {
-        return _nextModData;
-    }
-
-    public long getLastChanged() {
-        return _lastChanged;
-    }
+public abstract class RoutingKeyGenerator {
 
     /**
-     *  How long until midnight (ms)
+     * Get the generator for this context.
      *
-     *  @return could be slightly negative
-     *  @since 0.9.10 moved from UpdateRoutingKeyModifierJob
+     * @return null in I2PAppContext; non-null in RouterContext.
      */
-    public long getTimeTillMidnight() {
-        return _nextMidnight - _context.clock().now();
-    }
-
-    /**
-     *  Set _cal to midnight for the time given.
-     *  Caller must synch.
-     *  @since 0.9.10
-     */
-    private void setCalToPreviousMidnight(long now) {
-            _cal.setTime(new Date(now));
-            _cal.set(Calendar.YEAR, _cal.get(Calendar.YEAR));               // gcj <= 4.0 workaround
-            _cal.set(Calendar.DAY_OF_YEAR, _cal.get(Calendar.DAY_OF_YEAR)); // gcj <= 4.0 workaround
-            _cal.set(Calendar.HOUR_OF_DAY, 0);
-            _cal.set(Calendar.MINUTE, 0);
-            _cal.set(Calendar.SECOND, 0);
-            _cal.set(Calendar.MILLISECOND, 0);
+    public static RoutingKeyGenerator getInstance() {
+        return I2PAppContext.getGlobalContext().routingKeyGenerator();
     }
 
     /**
-     *  Generate mod data from _cal.
-     *  Caller must synch.
-     *  @since 0.9.10
+     *  The version of the current (today's) mod data.
+     *  Use to determine if the routing key should be regenerated.
      */
-    private byte[] generateModDataFromCal() {
-        Date today = _cal.getTime();
-        
-        String modVal = _fmt.format(today);
-        if (modVal.length() != LENGTH)
-            throw new IllegalStateException();
-        byte[] mod = new byte[LENGTH];
-        for (int i = 0; i < LENGTH; i++)
-            mod[i] = (byte)(modVal.charAt(i) & 0xFF);
-        return mod;
-    }
+    public abstract long getLastChanged();
 
     /**
-     * Update the current modifier data with some bytes derived from the current
-     * date (yyyyMMdd in GMT)
-     *
-     * @return true if changed
-     */
-    public synchronized boolean generateDateBasedModData() {
-        long now = _context.clock().now();
-        setCalToPreviousMidnight(now);
-        byte[] mod = generateModDataFromCal();
-        boolean changed = !Arrays.equals(_currentModData, mod);
-        if (changed) {
-            // add a day and store next midnight and mod data for convenience
-            _cal.add(Calendar.DATE, 1);
-            _nextMidnight = _cal.getTime().getTime();
-            byte[] next = generateModDataFromCal();
-            _currentModData = mod;
-            _nextModData = next;
-            _lastChanged = now;
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Routing modifier generated: " + HexDump.dump(mod));
-        }
-        return changed;
-    }
-    
-    /**
-     * Generate a modified (yet consistent) hash from the origKey by generating the
-     * SHA256 of the targetKey with the current modData appended to it
-     *
-     * This makes Sybil's job a lot harder, as she needs to essentially take over the
-     * whole keyspace.
-     *
-     * @throws IllegalArgumentException if origKey is null
-     */
-    public Hash getRoutingKey(Hash origKey) {
-        return getKey(origKey, _currentModData);
-    }
-    
-    /**
-     * Get the routing key using tomorrow's modData, not today's
-     *
-     * @since 0.9.10
-     */
-    public Hash getNextRoutingKey(Hash origKey) {
-        return getKey(origKey, _nextModData);
-    }
-    
-    /**
-     * Generate a modified (yet consistent) hash from the origKey by generating the
-     * SHA256 of the targetKey with the specified modData appended to it
+     * Get the routing key for a key.
      *
      * @throws IllegalArgumentException if origKey is null
      */
-    private static Hash getKey(Hash origKey, byte[] modData) {
-        if (origKey == null) throw new IllegalArgumentException("Original key is null");
-        byte modVal[] = new byte[Hash.HASH_LENGTH + LENGTH];
-        System.arraycopy(origKey.getData(), 0, modVal, 0, Hash.HASH_LENGTH);
-        System.arraycopy(modData, 0, modVal, Hash.HASH_LENGTH, LENGTH);
-        return SHA256Generator.getInstance().calculateHash(modVal);
-    }
-
-/****
-    public static void main(String args[]) {
-        Hash k1 = new Hash();
-        byte k1d[] = new byte[Hash.HASH_LENGTH];
-        RandomSource.getInstance().nextBytes(k1d);
-        k1.setData(k1d);
+    public abstract Hash getRoutingKey(Hash origKey);
 
-        for (int i = 0; i < 10; i++) {
-            System.out.println("K1:  " + k1);
-            Hash k1m = RoutingKeyGenerator.getInstance().getRoutingKey(k1);
-            System.out.println("MOD: " + new String(RoutingKeyGenerator.getInstance().getModData()));
-            System.out.println("K1M: " + k1m);
-        }
-        try {
-            Thread.sleep(2000);
-        } catch (Throwable t) { // nop
-        }
-    }
-****/
 }
diff --git a/core/java/src/net/i2p/data/SigningPrivateKey.java b/core/java/src/net/i2p/data/SigningPrivateKey.java
index a8fcbb208196c88c8310f5b3e33b382a91057e39..07b8969e3c5499746a4318928f80ecf74db16bb0 100644
--- a/core/java/src/net/i2p/data/SigningPrivateKey.java
+++ b/core/java/src/net/i2p/data/SigningPrivateKey.java
@@ -75,8 +75,12 @@ public class SigningPrivateKey extends SimpleDataStructure {
         return _type;
     }
 
-    /** converts this signing private key to its public equivalent
-     * @return a SigningPublicKey object derived from this private key
+    /**
+     *  Converts this signing private key to its public equivalent.
+     *  As of 0.9.16, supports all key types.
+     *
+     *  @return a SigningPublicKey object derived from this private key
+     *  @throws IllegalArgumentException on bad key or unknown or unsupported type
      */
     public SigningPublicKey toPublic() {
         return KeyGenerator.getSigningPublicKey(this);
diff --git a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
index afa22de9d25e128461d69487b00aaa2770f311ed..331ee3dad85ed0d53b285f08681602214012aba2 100644
--- a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
@@ -70,9 +70,12 @@ public class MessagePayloadMessage extends I2CPMessageImpl {
         }
     }
 
+    /**
+     *  @throws UnsupportedOperationException always
+     */
     @Override
     protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
-        throw new RuntimeException("go away, we dont want any");
+        throw new UnsupportedOperationException();
     }
     
     /**
diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
index ef0e9419468c1bddbfcb7066e259f81062a7c3a5..67a67ffa4722aada3d2f691c97d8049fa61bebf1 100644
--- a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
@@ -101,9 +101,12 @@ public class SendMessageMessage extends I2CPMessageImpl {
         }
     }
 
+    /**
+     *  @throws UnsupportedOperationException always
+     */
     @Override
     protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
-        throw new RuntimeException("wtf, dont run me");
+        throw new UnsupportedOperationException();
     }
 
     /**
diff --git a/installer/resources/proxy/enc-header.ht b/installer/resources/proxy/enc-header.ht
new file mode 100644
index 0000000000000000000000000000000000000000..e5c271e234dbf44b87728fdfeff6192292711818
--- /dev/null
+++ b/installer/resources/proxy/enc-header.ht
@@ -0,0 +1,24 @@
+HTTP/1.1 504 Gateway Timeout
+Content-Type: text/html; charset=UTF-8
+Cache-control: no-cache
+Connection: close
+Proxy-Connection: close
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html><head>
+<title>_("Warning: Eepsite Unreachable")</title>
+<link rel="shortcut icon" href="http://proxy.i2p/themes/console/images/favicon.ico">
+<link href="http://proxy.i2p/themes/console/default/console.css" rel="stylesheet" type="text/css">
+</head>
+<body>
+<div class="logo">
+ <a href="http://127.0.0.1:7657/" title="_("Router Console")"><img src="http://proxy.i2p/themes/console/images/i2plogo.png" alt="_("I2P Router Console")" border="0"></a><hr>
+ <a href="http://127.0.0.1:7657/config.jsp">_("Configuration")</a> <a href="http://127.0.0.1:7657/help.jsp">_("Help")</a> <a href="http://127.0.0.1:7657/susidns/index">_("Addressbook")</a>
+</div>
+<div class="warning" id="warning">
+<h3>_("Warning: Eepsite Unreachable")</h3>
+<p>
+_("The eepsite was not reachable, because it uses encryption options that are not supported by your I2P or Java version.")
+<hr>
+<p><b>_("Could not connect to the following destination:")</b>
+</p>
diff --git a/installer/resources/proxy/encp-header.ht b/installer/resources/proxy/encp-header.ht
new file mode 100644
index 0000000000000000000000000000000000000000..9d53fb70760894f79a2e1d830e20f2c10fc44bce
--- /dev/null
+++ b/installer/resources/proxy/encp-header.ht
@@ -0,0 +1,25 @@
+HTTP/1.1 504 Gateway Timeout
+Content-Type: text/html; charset=UTF-8
+Cache-control: no-cache
+Connection: close
+Proxy-Connection: close
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html><head>
+<title>_("Warning: Outproxy Unreachable")</title>
+<link rel="shortcut icon" href="http://proxy.i2p/themes/console/images/favicon.ico">
+<link href="http://proxy.i2p/themes/console/default/console.css" rel="stylesheet" type="text/css">
+</head>
+<body>
+<div class="logo">
+ <a href="http://127.0.0.1:7657/" title="_("Router Console")"><img src="http://proxy.i2p/themes/console/images/i2plogo.png" alt="_("I2P Router Console")" border="0"></a><hr>
+ <a href="http://127.0.0.1:7657/config.jsp">_("Configuration")</a> <a href="http://127.0.0.1:7657/help.jsp">_("Help")</a> <a href="http://127.0.0.1:7657/susidns/index">_("Addressbook")</a>
+</div>
+<div class="warning" id="warning">
+<h3>_("Warning: Outproxy Unreachable")</h3>
+<p>
+_("The HTTP outproxy was not reachable, because it uses encryption options that are not supported by your I2P or Java version.")
+_("You may want to {0}retry{1} as this will randomly reselect an outproxy from the pool you have defined {2}here{3} (if you have more than one configured).", "<a href=\"javascript:parent.window.location.reload()\">", "</a>", "<a href=\"http://127.0.0.1:7657/i2ptunnel/index.jsp\">", "</a>")
+_("If you continue to have trouble you may want to edit your outproxy list {0}here{1}.", "<a href=\"http://127.0.0.1:7657/i2ptunnel/edit.jsp?tunnel=0\">", "</a>")
+</p>
+<hr><p><b>_("Could not connect to the following destination:")</b></p>
diff --git a/installer/resources/themes/snark/light/snark.css b/installer/resources/themes/snark/light/snark.css
index cf61bf1029719fb21dfbece0f4f3bb3d102bef6b..c195775c06038574fe94657eb831bebd0f4bceb4 100644
--- a/installer/resources/themes/snark/light/snark.css
+++ b/installer/resources/themes/snark/light/snark.css
@@ -513,6 +513,40 @@ a:active {
      color: #77b;
 }
 
+a.control, a.controld {
+     background: #fff;
+     border: 1px inset #191;
+     border-radius: 4px;
+     color: #359;
+     font-weight: bold;
+     margin: 2px 4px;
+     padding: 3px 4px;	
+     text-shadow: 0px 0px #410;
+     white-space: nowrap;
+}
+
+a.controld {
+     color: #459;
+     font-weight: normal;
+}
+
+a.control img, a.controld img {
+     display: none;
+}
+
+a.control:hover {
+     background-color: #559;
+     border: 1px outset #559;
+     color: #fff;
+     text-shadow: 0px 1px 5px #410;
+}
+
+a.control:active {
+     background: #f60 !important;
+     color: #fff !important;
+     text-shadow: 0 !important;
+}
+
 input {
      font-size: 9pt;
      font-weight: bold;
@@ -594,6 +628,14 @@ input[type=radio] {
 
 input.default { width: 1px; height: 1px; visibility: hidden; }
 
+input.disabled, input.disabled:hover {
+     background-color: #fff;
+     border: 1px inset #191;
+     color: #459;
+     font-weight: normal;
+     text-shadow: 0px 0px 0px #410;
+}
+
 select {
      background: #fff !important;
      color: #22f;
diff --git a/installer/resources/themes/snark/ubergine/snark.css b/installer/resources/themes/snark/ubergine/snark.css
index 4a57291f5e781807e0d28d3ef48b77ea5bac4fdf..f2e09b4f94cb3aecc395c33c8879e767e9c3983b 100644
--- a/installer/resources/themes/snark/ubergine/snark.css
+++ b/installer/resources/themes/snark/ubergine/snark.css
@@ -366,6 +366,10 @@ table.snarkTorrents tbody tr:hover, table.snarkDirInfo tbody tr:hover {
      width: 16px;
 }
 
+td.snarkFileIcon:first-child {
+     text-align: center;
+}
+
 .snarkFileName {
      padding: 4px 0px !important; 
      text-align: left !important;
@@ -526,6 +530,40 @@ a:hover {
      font-weight: bold;
 }
 
+a.control, a.controld {
+     background: #989;
+     border: 1px inset #bbb;
+     border-radius: 4px;
+     color: #000;
+     font-weight: bold;
+     margin: 2px 4px;
+     padding: 3px 4px;	
+     text-shadow: 0px 0px #410;
+     white-space: nowrap;
+}
+
+a.controld {
+     color: #444;
+     font-weight: normal;
+}
+
+a.controld img {
+     display: none;
+}
+
+a.control:hover {
+     background-color: #f60;
+     border: 1px outset #bbb;
+     color: #fff;
+     text-shadow: 0px 1px 5px #f00;
+}
+
+a.control:active {
+     background: #000 !important;
+     color: #f60 !important;
+     text-shadow: 0 !important;
+}
+
 input {
      font-size: 8.5pt;
      font-weight: bold;
@@ -598,6 +636,14 @@ input[type=radio] {
 
 input.default { width: 1px; height: 1px; visibility: hidden; }
 
+input.disabled, input.disabled:hover {
+     background-color: #989;
+     border: 1px inset #bbb;
+     color: #444;
+     font-weight: normal;
+     text-shadow: 0px 0px 0px #444;
+}
+
 input.accept {
      background: #989 url('../../console/images/accept.png') no-repeat 2px center;
      padding: 2px 3px 2px 20px !important;
diff --git a/installer/resources/themes/snark/vanilla/snark.css b/installer/resources/themes/snark/vanilla/snark.css
index 6afd7c428b2f718216647de4454c01299edfea23..ddbcd8e8c5e72897488b386f337bf7b2ac00efa8 100644
--- a/installer/resources/themes/snark/vanilla/snark.css
+++ b/installer/resources/themes/snark/vanilla/snark.css
@@ -373,6 +373,10 @@ td:first-child {
      width: 16px;
 }
 
+td.snarkFileIcon:first-child {
+     text-align: center;
+}
+
 .snarkFileName {
      padding: 4px 0px !important; 
      text-align: left !important;
@@ -543,6 +547,40 @@ a:hover {
      font-weight: bold;
 }
 
+a.control, a.controld {
+     background: #fef url('images/bling.png') repeat-x scroll center center;
+     border: 1px inset #bbb;
+     border-radius: 4px;
+     color: #f30;
+     font-weight: bold;
+     margin: 2px 4px;
+     padding: 3px 4px;	
+     text-shadow: 0px 0px #410;
+     white-space: nowrap;
+}
+
+a.controld {
+     color: #f60;
+     font-weight: normal;
+}
+
+a.controld img {
+     display: none;
+}
+
+a.control:hover {
+     background-color: #fef;
+     border: 1px outset #bbb;
+     color: #f60;
+     text-shadow: 0px 1px 5px #fdf;
+}
+
+a.control:active {
+     background: #000 !important;
+     color: #f60 !important;
+     text-shadow: 0 !important;
+}
+
 input {
      font-size: 9pt;
      font-weight: bold;
@@ -612,6 +650,14 @@ input[type=radio] {
 
 input.default { width: 1px; height: 1px; visibility: hidden; }
 
+input.disabled, input.disabled:hover {
+     background-color: #989;
+     border: 1px inset #bbb;
+     color: #f60;
+     font-weight: normal;
+     text-shadow: 0px 0px 0px #410;
+}
+
 input.accept {
      background: #f3efc7 url('../../console/images/accept.png') no-repeat 2px center;
      padding: 2px 3px 2px 20px !important;
diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
index 10533cfac830557ee0bd65b2d85f0c1ac70c43c3..805c8cc4d7ea4d40b7cfd55a8c0113b0d7c3ade1 100644
--- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
@@ -146,10 +146,10 @@ public class BuildRequestRecord {
         return (_data.getData()[_data.getOffset() + OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0;
     }
     /**
-     * Time that the request was sent, truncated to the nearest hour
+     * Time that the request was sent (ms), truncated to the nearest hour
      */
     public long readRequestTime() {
-        return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * 60l * 60l * 1000l;
+        return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * (60 * 60 * 1000L);
     }
     /**
      * What message ID should we send the request to the next hop with.  If this is the outbound tunnel endpoint,
@@ -250,6 +250,8 @@ public class BuildRequestRecord {
         else if (isOutEndpoint)
             buf[OFF_FLAG] |= FLAG_OUTBOUND_ENDPOINT;
         long truncatedHour = ctx.clock().now();
+        // prevent hop identification at top of the hour
+        truncatedHour -= ctx.random().nextInt(90*1000);
         truncatedHour /= (60l*60l*1000l);
         DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour);
         DataHelper.toLong(buf, OFF_SEND_MSG_ID, 4, nextMsgId);
diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
index 37ff20186c661d38b40b4d9df7aeaf3f1f04a3b1..790e376227234a03a9439fb63225e7500ddd61a5 100644
--- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
+++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
@@ -17,7 +17,7 @@ import java.util.Set;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
 import net.i2p.data.TunnelId;
diff --git a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java
index 8e48a7439968b04f09c1d03483d6e9e5def9dccd..b34912e8d817a902c15ec4ce8bb29376ce7ce3a5 100644
--- a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java
+++ b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java
@@ -18,7 +18,7 @@ import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 
 /**
diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/router/java/src/net/i2p/data/router/RouterAddress.java
similarity index 98%
rename from core/java/src/net/i2p/data/RouterAddress.java
rename to router/java/src/net/i2p/data/router/RouterAddress.java
index 960e495faf2e290410e2ecef8d2f0b5262a6a1a0..ca0f94d08abb79eddaa71201f38210460268bf4c 100644
--- a/core/java/src/net/i2p/data/RouterAddress.java
+++ b/router/java/src/net/i2p/data/router/RouterAddress.java
@@ -1,4 +1,4 @@
-package net.i2p.data;
+package net.i2p.data.router;
 
 /*
  * free (adj.): unencumbered; not under the control of others
@@ -17,6 +17,9 @@ import java.util.Date;
 import java.util.Map;
 import java.util.Properties;
 
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.DataStructureImpl;
 import net.i2p.util.Addresses;
 import net.i2p.util.OrderedProperties;
 
@@ -36,6 +39,7 @@ import net.i2p.util.OrderedProperties;
  * several releases for the change to propagate as it is backwards-incompatible.
  * Restored as of 0.9.12.
  *
+ * @since 0.9.16 moved from net.i2p.data
  * @author jrandom
  */
 public class RouterAddress extends DataStructureImpl {
diff --git a/core/java/src/net/i2p/data/RouterIdentity.java b/router/java/src/net/i2p/data/router/RouterIdentity.java
similarity index 89%
rename from core/java/src/net/i2p/data/RouterIdentity.java
rename to router/java/src/net/i2p/data/router/RouterIdentity.java
index 346bb5f8d9442b2555647834dfff4f4c246c7374..6dc7ca2d752e84c327b5e89ffb75f9589be5eaca 100644
--- a/core/java/src/net/i2p/data/RouterIdentity.java
+++ b/router/java/src/net/i2p/data/router/RouterIdentity.java
@@ -1,4 +1,7 @@
-package net.i2p.data;
+package net.i2p.data.router;
+
+import net.i2p.data.Certificate;
+import net.i2p.data.KeysAndCert;
 
 /*
  * free (adj.): unencumbered; not under the control of others
@@ -16,6 +19,7 @@ package net.i2p.data;
  * 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.9.16 moved from net.i2p.data
  * @author jrandom
  */
 public class RouterIdentity extends KeysAndCert {
diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/router/java/src/net/i2p/data/router/RouterInfo.java
similarity index 98%
rename from core/java/src/net/i2p/data/RouterInfo.java
rename to router/java/src/net/i2p/data/router/RouterInfo.java
index 22f9ce5ddce74a7eab8f0d8be6b90de132343016..390b013ec5e9c6f35109929367c82e155e433e55 100644
--- a/core/java/src/net/i2p/data/RouterInfo.java
+++ b/router/java/src/net/i2p/data/router/RouterInfo.java
@@ -1,4 +1,4 @@
-package net.i2p.data;
+package net.i2p.data.router;
 
 /*
  * free (adj.): unencumbered; not under the control of others
@@ -31,6 +31,13 @@ import net.i2p.crypto.SHA1;
 import net.i2p.crypto.SHA1Hash;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.crypto.SigType;
+import net.i2p.data.DatabaseEntry;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.KeysAndCert;
+import net.i2p.data.Signature;
+import net.i2p.data.SimpleDataStructure;
 import net.i2p.util.Clock;
 import net.i2p.util.Log;
 import net.i2p.util.OrderedProperties;
@@ -47,6 +54,7 @@ import net.i2p.util.SystemVersion;
  * To ensure integrity of the RouterInfo, methods that change an element of the
  * RouterInfo will throw an IllegalStateException after the RouterInfo is signed.
  *
+ * @since 0.9.16 moved from net.i2p.data
  * @author jrandom
  */
 public class RouterInfo extends DatabaseEntry {
@@ -190,7 +198,7 @@ public class RouterInfo extends DatabaseEntry {
                 // 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.
-                DataHelper.sortStructureList(_addresses);
+                SortHelper.sortStructureList(_addresses);
             }
         }
     }
@@ -308,7 +316,7 @@ public class RouterInfo extends DatabaseEntry {
                     // 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 hashes are stored in a Set, not a List.
-                    peers = (Collection<Hash>) DataHelper.sortStructures(peers);
+                    peers = (Collection<Hash>) SortHelper.sortStructures(peers);
                 for (Hash peerHash : peers) {
                     peerHash.writeBytes(out);
                 }
diff --git a/router/java/src/net/i2p/data/router/RouterKeyGenerator.java b/router/java/src/net/i2p/data/router/RouterKeyGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..69e6aaceca317488cce3034b2d7e705dc5d7740a
--- /dev/null
+++ b/router/java/src/net/i2p/data/router/RouterKeyGenerator.java
@@ -0,0 +1,224 @@
+package net.i2p.data.router;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.SHA256Generator;
+import net.i2p.data.Hash;
+import net.i2p.data.RoutingKeyGenerator;
+import net.i2p.util.HexDump;
+import net.i2p.util.Log;
+
+/**
+ * Component to manage the munging of hashes into routing keys - given a hash, 
+ * perform some consistent transformation against it and return the result.
+ * This transformation is fed by the current "mod data".  
+ *
+ * Right now the mod data is the current date (GMT) as a string: "yyyyMMdd",
+ * and the transformation takes the original hash, appends the bytes of that mod data,
+ * then returns the SHA256 of that concatenation.
+ *
+ * Do we want this to simply do the XOR of the SHA256 of the current mod data and
+ * the key?  does that provide the randomization we need?  It'd save an SHA256 op.
+ * Bah, too much effort to think about for so little gain.  Other algorithms may come
+ * into play layer on about making periodic updates to the routing key for data elements
+ * to mess with Sybil.  This may be good enough though.
+ *
+ * Also - the method generateDateBasedModData() should be called after midnight GMT 
+ * once per day to generate the correct routing keys!
+ *
+ * @since 0.9.16 moved from net.i2p.data.RoutingKeyGenerator..
+ *
+ */
+public class RouterKeyGenerator extends RoutingKeyGenerator {
+    private final Log _log;
+    private final I2PAppContext _context;
+
+    public RouterKeyGenerator(I2PAppContext context) {
+        _log = context.logManager().getLog(RoutingKeyGenerator.class);
+        _context = context;
+        // ensure non-null mod data
+        generateDateBasedModData();
+    }
+    
+    private volatile byte _currentModData[];
+    private volatile byte _nextModData[];
+    private volatile long _nextMidnight;
+    private volatile long _lastChanged;
+
+    private final static Calendar _cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+    private static final String FORMAT = "yyyyMMdd";
+    private static final int LENGTH = FORMAT.length();
+    private final static SimpleDateFormat _fmt = new SimpleDateFormat(FORMAT, Locale.US);
+    static {
+        // make sure GMT is set, azi2phelper Vuze plugin is disabling static JVM TZ setting in Router.java
+        _fmt.setCalendar(_cal);
+    }
+
+    /**
+     *  The current (today's) mod data.
+     *  Warning - not a copy, do not corrupt.
+     *
+     *  @return non-null, 8 bytes
+     */
+    public byte[] getModData() {
+        return _currentModData;
+    }
+
+    /**
+     *  Tomorrow's mod data.
+     *  Warning - not a copy, do not corrupt.
+     *  For debugging use only.
+     *
+     *  @return non-null, 8 bytes
+     *  @since 0.9.10
+     */
+    public byte[] getNextModData() {
+        return _nextModData;
+    }
+
+    public long getLastChanged() {
+        return _lastChanged;
+    }
+
+    /**
+     *  How long until midnight (ms)
+     *
+     *  @return could be slightly negative
+     *  @since 0.9.10 moved from UpdateRoutingKeyModifierJob
+     */
+    public long getTimeTillMidnight() {
+        return _nextMidnight - _context.clock().now();
+    }
+
+    /**
+     *  Set _cal to midnight for the time given.
+     *  Caller must synch.
+     *  @since 0.9.10
+     */
+    private void setCalToPreviousMidnight(long now) {
+            _cal.setTime(new Date(now));
+            _cal.set(Calendar.YEAR, _cal.get(Calendar.YEAR));               // gcj <= 4.0 workaround
+            _cal.set(Calendar.DAY_OF_YEAR, _cal.get(Calendar.DAY_OF_YEAR)); // gcj <= 4.0 workaround
+            _cal.set(Calendar.HOUR_OF_DAY, 0);
+            _cal.set(Calendar.MINUTE, 0);
+            _cal.set(Calendar.SECOND, 0);
+            _cal.set(Calendar.MILLISECOND, 0);
+    }
+
+    /**
+     *  Generate mod data from _cal.
+     *  Caller must synch.
+     *  @since 0.9.10
+     */
+    private byte[] generateModDataFromCal() {
+        Date today = _cal.getTime();
+        
+        String modVal = _fmt.format(today);
+        if (modVal.length() != LENGTH)
+            throw new IllegalStateException();
+        byte[] mod = new byte[LENGTH];
+        for (int i = 0; i < LENGTH; i++)
+            mod[i] = (byte)(modVal.charAt(i) & 0xFF);
+        return mod;
+    }
+
+    /**
+     * Update the current modifier data with some bytes derived from the current
+     * date (yyyyMMdd in GMT)
+     *
+     * @return true if changed
+     */
+    public synchronized boolean generateDateBasedModData() {
+        long now = _context.clock().now();
+        setCalToPreviousMidnight(now);
+        byte[] mod = generateModDataFromCal();
+        boolean changed = !Arrays.equals(_currentModData, mod);
+        if (changed) {
+            // add a day and store next midnight and mod data for convenience
+            _cal.add(Calendar.DATE, 1);
+            _nextMidnight = _cal.getTime().getTime();
+            byte[] next = generateModDataFromCal();
+            _currentModData = mod;
+            _nextModData = next;
+            // ensure version is bumped
+            if (_lastChanged == now)
+                now++;
+            _lastChanged = now;
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Routing modifier generated: " + HexDump.dump(mod));
+        }
+        return changed;
+    }
+    
+    /**
+     * Generate a modified (yet consistent) hash from the origKey by generating the
+     * SHA256 of the targetKey with the current modData appended to it
+     *
+     * This makes Sybil's job a lot harder, as she needs to essentially take over the
+     * whole keyspace.
+     *
+     * @throws IllegalArgumentException if origKey is null
+     */
+    public Hash getRoutingKey(Hash origKey) {
+        return getKey(origKey, _currentModData);
+    }
+    
+    /**
+     * Get the routing key using tomorrow's modData, not today's
+     *
+     * @since 0.9.10
+     */
+    public Hash getNextRoutingKey(Hash origKey) {
+        return getKey(origKey, _nextModData);
+    }
+    
+    /**
+     * Generate a modified (yet consistent) hash from the origKey by generating the
+     * SHA256 of the targetKey with the specified modData appended to it
+     *
+     * @throws IllegalArgumentException if origKey is null
+     */
+    private static Hash getKey(Hash origKey, byte[] modData) {
+        if (origKey == null) throw new IllegalArgumentException("Original key is null");
+        byte modVal[] = new byte[Hash.HASH_LENGTH + LENGTH];
+        System.arraycopy(origKey.getData(), 0, modVal, 0, Hash.HASH_LENGTH);
+        System.arraycopy(modData, 0, modVal, Hash.HASH_LENGTH, LENGTH);
+        return SHA256Generator.getInstance().calculateHash(modVal);
+    }
+
+/****
+    public static void main(String args[]) {
+        Hash k1 = new Hash();
+        byte k1d[] = new byte[Hash.HASH_LENGTH];
+        RandomSource.getInstance().nextBytes(k1d);
+        k1.setData(k1d);
+
+        for (int i = 0; i < 10; i++) {
+            System.out.println("K1:  " + k1);
+            Hash k1m = RoutingKeyGenerator.getInstance().getRoutingKey(k1);
+            System.out.println("MOD: " + new String(RoutingKeyGenerator.getInstance().getModData()));
+            System.out.println("K1M: " + k1m);
+        }
+        try {
+            Thread.sleep(2000);
+        } catch (Throwable t) { // nop
+        }
+    }
+****/
+}
diff --git a/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f466ca055d80a18a42cbc5ae7ca2bc7acdaa7e8
--- /dev/null
+++ b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java
@@ -0,0 +1,62 @@
+package net.i2p.data.router;
+
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import net.i2p.crypto.SigType;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.data.PrivateKey;
+import net.i2p.data.PrivateKeyFile;
+import net.i2p.data.SigningPrivateKey;
+
+/**
+ *  Same format as super, simply adds a method to
+ *  treat it as a RouterIdentity instead of a Destination.
+ *
+ *  @since 0.9.16
+ */
+public class RouterPrivateKeyFile extends PrivateKeyFile {
+    
+    public RouterPrivateKeyFile(File file) {
+        super(file);
+    }
+    
+    /**
+     *  Read it in from the file.
+     *  Also sets the local privKey and signingPrivKey.
+     */
+    public RouterIdentity getRouterIdentity() throws IOException, DataFormatException {
+        InputStream in = null;
+        try {
+            in = new BufferedInputStream(new FileInputStream(this.file));
+            RouterIdentity ri = new RouterIdentity();
+            ri.readBytes(in);
+            privKey = new PrivateKey();
+            privKey.readBytes(in);
+            SigType type = ri.getSigningPublicKey().getType();
+            if (type == null)
+                throw new DataFormatException("Unknown sig type");
+            signingPrivKey = new SigningPrivateKey(type);
+            signingPrivKey.readBytes(in);
+
+            // set it a Destination, so we may call validateKeyPairs()
+            // or other methods
+            dest = new Destination();
+            dest.setPublicKey(ri.getPublicKey());
+            dest.setSigningPublicKey(ri.getSigningPublicKey());
+            dest.setCertificate(ri.getCertificate());
+            dest.setPadding(ri.getPadding());
+
+            return ri;
+        } finally {
+            if (in != null) {
+                try { in.close(); } catch (IOException ioe) {}
+            }
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/data/router/SortHelper.java b/router/java/src/net/i2p/data/router/SortHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..0c40fa3edab3716f53e4e19797c411e96ab82ff5
--- /dev/null
+++ b/router/java/src/net/i2p/data/router/SortHelper.java
@@ -0,0 +1,79 @@
+package net.i2p.data.router;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import net.i2p.data.DataStructure;
+
+/**
+ * The sorting of addresses in RIs
+ *
+ * @since 0.9.16 moved from DataHelper
+ */
+class SortHelper {
+
+    /**
+     *  Sort based on the Hash of the DataStructure.
+     *  Warning - relatively slow.
+     *  WARNING - this sort order must be consistent network-wide, so while the order is arbitrary,
+     *  it cannot be changed.
+     *  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.emptyList();
+
+        // This used to use Hash.toString(), which is insane, since a change to toString()
+        // would break the whole network. Now use Hash.toBase64().
+        // Note that the Base64 sort order is NOT the same as the raw byte sort order,
+        // despite what you may read elsewhere.
+
+        //ArrayList<DataStructure> rv = new ArrayList(dataStructures.size());
+        //TreeMap<String, DataStructure> tm = new TreeMap();
+        //for (DataStructure struct : dataStructures) {
+        //    tm.put(struct.calculateHash().toString(), struct);
+        //}
+        //for (DataStructure struct : tm.values()) {
+        //    rv.add(struct);
+        //}
+        ArrayList<DataStructure> rv = new ArrayList<DataStructure>(dataStructures);
+        sortStructureList(rv);
+        return rv;
+    }
+
+    /**
+     *  See above.
+     *  DEPRECATED - Only used by RouterInfo.
+     *
+     *  @since 0.9
+     */
+    static void sortStructureList(List<? extends DataStructure> dataStructures) {
+        Collections.sort(dataStructures, new DataStructureComparator());
+    }
+
+    /**
+     * See sortStructures() comments.
+     * @since 0.8.3
+     */
+    private static class DataStructureComparator implements Comparator<DataStructure>, Serializable {
+        public int compare(DataStructure l, DataStructure r) {
+            return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64());
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/data/router/package.html b/router/java/src/net/i2p/data/router/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..fac87d49818e7f2dc8ea519d67398422660b638e
--- /dev/null
+++ b/router/java/src/net/i2p/data/router/package.html
@@ -0,0 +1,7 @@
+<html>
+<body>
+<p>
+Classes formerly in net.i2p.data but moved here as they are only used by the router.
+</p>
+</body>
+</html>
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
index 093a6fdc5ac02a43a6b6165c17eacd0d5bcd573e..a6fe1d20930f03e7c577ee90363a870a999c9ff8 100644
--- a/router/java/src/net/i2p/router/Blocklist.java
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -28,8 +28,8 @@ import java.util.TreeSet;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.util.Addresses;
 import net.i2p.util.ConcurrentHashSet;
diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java
index 9c7d92338d147feb1ae22d1f0eac2bf242fbf03e..59a22eb9ca92e9e5188e7200b1be78bc652a0b9d 100644
--- a/router/java/src/net/i2p/router/CommSystemFacade.java
+++ b/router/java/src/net/i2p/router/CommSystemFacade.java
@@ -13,7 +13,7 @@ import java.io.Writer;
 import java.util.Collections;
 import java.util.List;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 
 /**
  * Manages the communication subsystem between peers, including connections, 
diff --git a/router/java/src/net/i2p/router/HandlerJobBuilder.java b/router/java/src/net/i2p/router/HandlerJobBuilder.java
index c1c9832cdcaf6d62918820ac5874b01bf6bc23f9..62e2074a5f2ddb1161698a863a8d45768495f630 100644
--- a/router/java/src/net/i2p/router/HandlerJobBuilder.java
+++ b/router/java/src/net/i2p/router/HandlerJobBuilder.java
@@ -9,7 +9,7 @@ package net.i2p.router;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.I2NPMessage;
 
 /**
diff --git a/router/java/src/net/i2p/router/InNetMessagePool.java b/router/java/src/net/i2p/router/InNetMessagePool.java
index 28296cebba446e79aa01610b1dba399eef9c9ec3..fb4c2a632c2f85f87fbabee70c8d0db853794a09 100644
--- a/router/java/src/net/i2p/router/InNetMessagePool.java
+++ b/router/java/src/net/i2p/router/InNetMessagePool.java
@@ -14,7 +14,7 @@ import java.util.Date;
 import java.util.List;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java
index ce4992f8bd54deef53e963b2b4a4a6173e71dcd6..2807e8fe7fc37683729d3db8179b2241a04fc4e2 100644
--- a/router/java/src/net/i2p/router/KeyManager.java
+++ b/router/java/src/net/i2p/router/KeyManager.java
@@ -18,6 +18,7 @@ import java.io.OutputStream;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataStructure;
 import net.i2p.data.Destination;
@@ -26,6 +27,7 @@ import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
+import net.i2p.router.startup.CreateRouterInfoJob;
 import net.i2p.util.Log;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.SecureFileOutputStream;
@@ -47,10 +49,10 @@ public class KeyManager {
     
     public final static String PROP_KEYDIR = "router.keyBackupDir";
     public final static String DEFAULT_KEYDIR = "keyBackup";
-    private final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key";
-    private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key";
-    private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key";
-    private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key";
+    public final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key";
+    public final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key";
+    public final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key";
+    public final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key";
     
     public KeyManager(RouterContext context) {
         _context = context;
@@ -151,8 +153,9 @@ public class KeyManager {
         private void syncKeys(File keyDir) {
             syncPrivateKey(keyDir);
             syncPublicKey(keyDir);
-            syncSigningKey(keyDir);
-            syncVerificationKey(keyDir);
+            SigType type = CreateRouterInfoJob.getSigTypeConfig(getContext());
+            syncSigningKey(keyDir, type);
+            syncVerificationKey(keyDir, type);
         }
 
         private void syncPrivateKey(File keyDir) {
@@ -181,27 +184,33 @@ public class KeyManager {
                 _publicKey = (PublicKey) readin;
         }
 
-        private void syncSigningKey(File keyDir) {
+        /**
+         *  @param type the SigType to expect on read-in, ignored on write
+         */
+        private void syncSigningKey(File keyDir, SigType type) {
             DataStructure ds;
             File keyFile = new File(keyDir, KEYFILE_PRIVATE_SIGNING);
             boolean exists = (_signingPrivateKey != null);
             if (exists)
                 ds = _signingPrivateKey;
             else
-                ds = new SigningPrivateKey();
+                ds = new SigningPrivateKey(type);
             DataStructure readin = syncKey(keyFile, ds, exists);
             if (readin != null && !exists)
                 _signingPrivateKey = (SigningPrivateKey) readin;
         }
 
-        private void syncVerificationKey(File keyDir) {
+        /**
+         *  @param type the SigType to expect on read-in, ignored on write
+         */
+        private void syncVerificationKey(File keyDir, SigType type) {
             DataStructure ds;
             File keyFile = new File(keyDir, KEYFILE_PUBLIC_SIGNING);
             boolean exists = (_signingPublicKey != null);
             if (exists)
                 ds = _signingPublicKey;
             else
-                ds = new SigningPublicKey();
+                ds = new SigningPublicKey(type);
             DataStructure readin = syncKey(keyFile, ds, exists);
             if (readin != null && !exists)
                 _signingPublicKey  = (SigningPublicKey) readin;
diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java
index abfc566df9c5748e16ec866862159017df64ec4b..849e54f47fca006f333fa63ccf67b600916ee6fc 100644
--- a/router/java/src/net/i2p/router/LeaseSetKeys.java
+++ b/router/java/src/net/i2p/router/LeaseSetKeys.java
@@ -40,7 +40,7 @@ public class LeaseSetKeys {
     /**
      * Key with which a LeaseSet can be revoked (by republishing it with no Leases)
      *
-     * @deprecated unused
+     * Deprecated, unused
      */
     public SigningPrivateKey getRevocationKey() { return _revocationKey; }
 
diff --git a/router/java/src/net/i2p/router/MultiRouter.java b/router/java/src/net/i2p/router/MultiRouter.java
index 5e8cb47608b005235a163a9ae1d1a716fd5cf5c5..abcd32e3d05e505d936d18d32946a42c7962ad61 100644
--- a/router/java/src/net/i2p/router/MultiRouter.java
+++ b/router/java/src/net/i2p/router/MultiRouter.java
@@ -10,7 +10,7 @@ import java.util.Scanner;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Router;
 
 /**
diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
index 7fc3c5082609f52b8d50e4b9342320f8d03be924..fdd1fd30fb204f2b154aaebdfb7f0e4450fce303 100644
--- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
@@ -14,9 +14,10 @@ import java.util.Collections;
 import java.util.Set;
 
 import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.networkdb.reseed.ReseedChecker;
 
 /**
@@ -51,18 +52,51 @@ public abstract class NetworkDatabaseFacade implements Service {
     public abstract LeaseSet lookupLeaseSetLocally(Hash key);
     public abstract void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs);
     public abstract RouterInfo lookupRouterInfoLocally(Hash key);
+
+    /**
+     *  Lookup using the client's tunnels
+     *  Succeeds even if LS validation fails due to unsupported sig type
+     *
+     *  @param fromLocalDest use these tunnels for the lookup, or null for exploratory
+     *  @since 0.9.16
+     */
+    public abstract void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest);
+
+    /**
+     *  Lookup locally in netDB and in badDest cache
+     *  Succeeds even if LS validation failed due to unsupported sig type
+     *
+     *  @since 0.9.16
+     */
+    public abstract Destination lookupDestinationLocally(Hash key);
+
     /** 
-     * return the leaseSet if another leaseSet already existed at that key 
+     * @return the leaseSet if another leaseSet already existed at that key 
      *
      * @throws IllegalArgumentException if the data is not valid
      */
     public abstract LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException;
+
     /** 
-     * return the routerInfo if another router already existed at that key 
+     * @return the routerInfo if another router already existed at that key 
      *
      * @throws IllegalArgumentException if the data is not valid
      */
     public abstract RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException;
+
+    /** 
+     *  @return the old entry if it already existed at that key 
+     *  @throws IllegalArgumentException if the data is not valid
+     *  @since 0.9.16
+     */
+    public DatabaseEntry store(Hash key, DatabaseEntry entry) throws IllegalArgumentException {
+        if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO)
+            return store(key, (RouterInfo) entry);
+        if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET)
+            return store(key, (LeaseSet) entry);
+        throw new IllegalArgumentException("unknown type");
+    }
+
     /**
      * @throws IllegalArgumentException if the local router is not valid
      */
@@ -101,4 +135,12 @@ public abstract class NetworkDatabaseFacade implements Service {
      *  @since IPv6
      */
     public boolean floodfillEnabled() { return false; };
+
+    /**
+     *  Is it permanently negative cached?
+     *
+     *  @param key only for Destinations; for RouterIdentities, see Banlist
+     *  @since 0.9.16
+     */
+    public boolean isNegativeCachedForever(Hash key) { return false; }
 }
diff --git a/router/java/src/net/i2p/router/OutNetMessage.java b/router/java/src/net/i2p/router/OutNetMessage.java
index 633416640cbf9c2a4221da05d9e7c41e0eb494e8..54434ac4b73b705d05ce6439b9e9b55ec7ef0ec2 100644
--- a/router/java/src/net/i2p/router/OutNetMessage.java
+++ b/router/java/src/net/i2p/router/OutNetMessage.java
@@ -18,7 +18,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.util.CDPQEntry;
 import net.i2p.util.Log;
diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java
index a3e71ee8e28392ef942297c84213e5fb5fb45bcd..920eec7d298c34f8888bc79f57da829cd18b22e0 100644
--- a/router/java/src/net/i2p/router/PersistentKeyRing.java
+++ b/router/java/src/net/i2p/router/PersistentKeyRing.java
@@ -70,9 +70,8 @@ public class PersistentKeyRing extends KeyRing {
             Hash h = e.getKey();
             buf.append(h.toBase64().substring(0, 6)).append("&hellip;");
             buf.append("<td>");
-            LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h);
-            if (ls != null) {
-                Destination dest = ls.getDestination();
+            Destination dest = _ctx.netDb().lookupDestinationLocally(h);
+            if (dest != null) {
                 if (_ctx.clientManager().isLocal(dest)) {
                     TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h);
                     if (in != null && in.getDestinationNickname() != null)
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 106efe00dd40b8849f9cc608cfd68e9595d97dd7..8fd08eb795f47e7cfb797fd6cc349754543b1aab 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -29,11 +29,12 @@ 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.router.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.router.message.GarlicMessageHandler;
 import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
+import net.i2p.router.startup.CreateRouterInfoJob;
 import net.i2p.router.startup.StartupJob;
 import net.i2p.router.startup.WorkingDir;
 import net.i2p.router.tasks.*;
@@ -98,10 +99,6 @@ public class Router implements RouterClock.ClockShiftListener {
     /** this does not put an 'H' in your routerInfo **/
     public final static String PROP_HIDDEN_HIDDEN = "router.isHidden";
     public final static String PROP_DYNAMIC_KEYS = "router.dynamicKeys";
-    public final static String PROP_INFO_FILENAME = "router.info.location";
-    public final static String PROP_INFO_FILENAME_DEFAULT = "router.info";
-    public final static String PROP_KEYS_FILENAME = "router.keys.location";
-    public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys";
     public final static String PROP_SHUTDOWN_IN_PROGRESS = "__shutdownInProgress";
     public final static String DNS_CACHE_TIME = "" + (5*60);
     private static final String EVENTLOG = "eventlog.txt";
@@ -672,20 +669,6 @@ public class Router implements RouterClock.ClockShiftListener {
             return Boolean.parseBoolean(h);
         return _context.commSystem().isInBadCountry();
     }
-
-    /**
-     *  Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob.
-     *  Not called by periodic RepublishLocalRouterInfoJob.
-     *  We don't want to change the cert on the fly as it changes the router hash.
-     *  RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert.
-     *  There's no reason to ever add a hidden cert?
-     *  @return the certificate for a new RouterInfo - probably a null cert.
-     */
-    public Certificate createCertificate() {
-        if (_context.getBooleanProperty(PROP_HIDDEN))
-            return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null);
-        return Certificate.NULL_CERT;
-    }
     
     /**
      *  @since 0.9.3
@@ -698,16 +681,18 @@ public class Router implements RouterClock.ClockShiftListener {
      * Ugly list of files that we need to kill if we are building a new identity
      *
      */
-    private static final String _rebuildFiles[] = new String[] { "router.info", 
-                                                                 "router.keys",
-                                                                 "netDb/my.info",      // no longer used
-                                                                 "connectionTag.keys", // never used?
-                                                                 "keyBackup/privateEncryption.key",
-                                                                 "keyBackup/privateSigning.key",
-                                                                 "keyBackup/publicEncryption.key",
-                                                                 "keyBackup/publicSigning.key",
-                                                                 "sessionKeys.dat"     // no longer used
-                                                               };
+    private static final String _rebuildFiles[] = new String[] {
+        CreateRouterInfoJob.INFO_FILENAME,
+        CreateRouterInfoJob.KEYS_FILENAME,
+        CreateRouterInfoJob.KEYS2_FILENAME,
+        "netDb/my.info",      // no longer used
+        "connectionTag.keys", // never used?
+        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_ENC,
+        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_ENC,
+        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_SIGNING,
+        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_SIGNING,
+        "sessionKeys.dat"     // no longer used
+    };
 
     public void killKeys() {
         //new Exception("Clearing identity files").printStackTrace();
@@ -1085,7 +1070,7 @@ public class Router implements RouterClock.ClockShiftListener {
             return;
         _eventLog.addEvent(EventLog.CLOCK_SHIFT, Long.toString(delta));
         // update the routing key modifier
-        _context.routingKeyGenerator().generateDateBasedModData();
+        _context.routerKeyGenerator().generateDateBasedModData();
         if (_context.commSystem().countActivePeers() <= 0)
             return;
         if (delta > 0)
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 1f8a38af68571d41d15eaead9713521197d0bf04..fd5c65775953215c6623473f4d9979c3e6e1a55b 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -10,7 +10,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import net.i2p.I2PAppContext;
 import net.i2p.app.ClientAppManager;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.RoutingKeyGenerator;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.data.router.RouterKeyGenerator;
 import net.i2p.internal.InternalClientManager;
 import net.i2p.router.client.ClientManagerFacadeImpl;
 import net.i2p.router.crypto.TransientSessionKeyManager;
@@ -65,6 +67,7 @@ public class RouterContext extends I2PAppContext {
     //private MessageStateMonitor _messageStateMonitor;
     private RouterThrottle _throttle;
     private RouterAppManager _appManager;
+    private RouterKeyGenerator _routingKeyGenerator;
     private final Set<Runnable> _finalShutdownTasks;
     // split up big lock on this to avoid deadlocks
     private volatile boolean _initialized;
@@ -183,6 +186,7 @@ public class RouterContext extends I2PAppContext {
         _messageHistory = new MessageHistory(this);
         _messageRegistry = new OutboundMessageRegistry(this);
         //_messageStateMonitor = new MessageStateMonitor(this);
+        _routingKeyGenerator = new RouterKeyGenerator(this);
         if (!getBooleanProperty("i2p.dummyNetDb"))
             _netDb = new FloodfillNetworkDatabaseFacade(this); // new KademliaNetworkDatabaseFacade(this);
         else
@@ -582,4 +586,35 @@ public class RouterContext extends I2PAppContext {
             _sessionKeyManagerInitialized = true;
         }
     }
+    
+    /**
+     * Determine how much do we want to mess with the keys to turn them 
+     * into something we can route.  This is context specific because we 
+     * may want to test out how things react when peers don't agree on 
+     * how to skew.
+     *
+     * Returns same thing as routerKeyGenerator()
+     *
+     * @return non-null
+     * @since 0.9.16 Overrides I2PAppContext. Returns non-null in RouterContext and null in I2PAppcontext.
+     */
+    @Override
+    public RoutingKeyGenerator routingKeyGenerator() {
+        return _routingKeyGenerator;
+    }
+
+    /**
+     * Determine how much do we want to mess with the keys to turn them 
+     * into something we can route.  This is context specific because we 
+     * may want to test out how things react when peers don't agree on 
+     * how to skew.
+     *
+     * Returns same thing as routingKeyGenerator()
+     *
+     * @return non-null
+     * @since 0.9.16
+     */
+    public RouterKeyGenerator routerKeyGenerator() {
+        return _routingKeyGenerator;
+    }
 }
diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java
index 805e7bbbc30702de796b4de815b0ad82104a1efb..275e4d85a4667fd1522ef496b52e07b47c5bf488 100644
--- a/router/java/src/net/i2p/router/RouterThrottleImpl.java
+++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java
@@ -1,7 +1,7 @@
 package net.i2p.router;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.peermanager.TunnelHistory;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateAverages;
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index cdcc0008cfced816ce40d533948c0815d4b7b5cb..382552ce68728ac2c8c69cf24cdd1c24e2d5dbf2 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -12,8 +12,10 @@ import java.util.Properties;
 
 import net.i2p.CoreVersion;
 import net.i2p.crypto.SigType;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.Payload;
+import net.i2p.data.PublicKey;
 import net.i2p.data.i2cp.BandwidthLimitsMessage;
 import net.i2p.data.i2cp.CreateLeaseSetMessage;
 import net.i2p.data.i2cp.CreateSessionMessage;
@@ -37,6 +39,7 @@ import net.i2p.data.i2cp.SessionId;
 import net.i2p.data.i2cp.SessionStatusMessage;
 import net.i2p.data.i2cp.SetDateMessage;
 import net.i2p.router.ClientTunnelSettings;
+import net.i2p.router.LeaseSetKeys;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 import net.i2p.util.PasswordManager;
@@ -81,8 +84,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             _log.debug("Message received: \n" + message);
         int type = message.getType();
         if (!_authorized) {
-            // TODO change to default true
-            boolean strict = _context.getBooleanProperty(PROP_AUTH_STRICT);
+            // Default true as of 0.9.16
+            boolean strict = _context.getBooleanPropertyDefaultTrue(PROP_AUTH_STRICT);
             if ((strict && type != GetDateMessage.MESSAGE_TYPE) ||
                 (type != CreateSessionMessage.MESSAGE_TYPE &&
                  type != GetDateMessage.MESSAGE_TYPE &&
@@ -367,8 +370,41 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             _runner.disconnectClient("Invalid CreateLeaseSetMessage");
             return;
         }
-
-        _context.keyManager().registerKeys(message.getLeaseSet().getDestination(), message.getSigningPrivateKey(), message.getPrivateKey());
+        Destination dest = _runner.getConfig().getDestination();
+        Destination ndest = message.getLeaseSet().getDestination();
+        if (!dest.equals(ndest)) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Different destination in LS");
+            _runner.disconnectClient("Different destination in LS");
+            return;
+        }
+        LeaseSetKeys keys = _context.keyManager().getKeys(dest);
+        if (keys == null ||
+            !message.getPrivateKey().equals(keys.getDecryptionKey())) {
+            // Verify and register crypto keys if new or if changed
+            // Private crypto key should never change, and if it does,
+            // one of the checks below will fail
+            PublicKey pk;
+            try {
+                pk = message.getPrivateKey().toPublic();
+            } catch (IllegalArgumentException iae) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Bad private key in LS");
+                _runner.disconnectClient("Bad private key in LS");
+                return;
+            }
+            if (!pk.equals(message.getLeaseSet().getEncryptionKey())) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Private/public crypto key mismatch in LS");
+                _runner.disconnectClient("Private/public crypto key mismatch in LS");
+                return;
+            }
+            // just register new SPK, don't verify, unused
+            _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey());
+        } else if (!message.getSigningPrivateKey().equals(keys.getRevocationKey())) {
+            // just register new SPK, don't verify, unused
+            _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey());
+        }
         try {
             _context.netDb().publish(message.getLeaseSet());
         } catch (IllegalArgumentException iae) {
diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java
index be08388ba2c195dc4fc89e364819ba1b12307968..911365b8e80a49726f22f9571dc953b81444b05f 100644
--- a/router/java/src/net/i2p/router/client/LookupDestJob.java
+++ b/router/java/src/net/i2p/router/client/LookupDestJob.java
@@ -38,7 +38,11 @@ class LookupDestJob extends JobImpl {
     }
 
     /**
-     *  One of h or name non-null
+     *  One of h or name non-null.
+     *
+     *  For hash or b32 name, the dest will be returned if the LS can be found,
+     *  even if the dest uses unsupported crypto.
+     *
      *  @param reqID must be >= 0 if name != null
      *  @param sessID must non-null if reqID >= 0
      *  @param fromLocalDest use these tunnels for the lookup, or null for exploratory
@@ -88,7 +92,7 @@ class LookupDestJob extends JobImpl {
                 returnFail();
         } else {
             DoneJob done = new DoneJob(getContext());
-            getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout, _fromLocalDest);
+            getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest);
         }
     }
 
@@ -98,9 +102,9 @@ class LookupDestJob extends JobImpl {
         }
         public String getName() { return "LeaseSet Lookup Reply to Client"; }
         public void runJob() {
-            LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash);
-            if (ls != null)
-                returnDest(ls.getDestination());
+            Destination dest = getContext().netDb().lookupDestinationLocally(_hash);
+            if (dest != null)
+                returnDest(dest);
             else
                 returnFail();
         }
diff --git a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
index 11b9419f382784ef69a46aca033b83a90107c8d2..460d7a7125ecde2f4049c5157c90822c5d71c6fd 100644
--- a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
@@ -15,16 +15,17 @@ import java.util.Map;
 import java.util.Set;
 
 import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Job;
 import net.i2p.router.NetworkDatabaseFacade;
 import net.i2p.router.RouterContext;
 
 public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
-    private Map<Hash, RouterInfo> _routers;
-    private RouterContext _context;
+    private final Map<Hash, RouterInfo> _routers;
+    private final RouterContext _context;
     
     public DummyNetworkDatabaseFacade(RouterContext ctx) {
         _routers = Collections.synchronizedMap(new HashMap<Hash, RouterInfo>());
@@ -42,6 +43,11 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
     public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {}
     public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, Hash fromLocalDest) {}
     public LeaseSet lookupLeaseSetLocally(Hash key) { return null; }
+
+    public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {}
+
+    public Destination lookupDestinationLocally(Hash key) { return null; }
+
     public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
         RouterInfo info = lookupRouterInfoLocally(key);
         if (info == null) 
@@ -50,13 +56,16 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
             _context.jobQueue().addJob(onFindJob);
     }
     public RouterInfo lookupRouterInfoLocally(Hash key) { return _routers.get(key); }
+
     public void publish(LeaseSet localLeaseSet) {}
     public void publish(RouterInfo localRouterInfo) {}
+
     public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; }
     public RouterInfo store(Hash key, RouterInfo routerInfo) {
         RouterInfo rv = _routers.put(key, routerInfo);
         return rv;
     }
+
     public void unpublish(LeaseSet localLeaseSet) {}
     public void fail(Hash dbEntry) {
         _routers.remove(dbEntry);
diff --git a/router/java/src/net/i2p/router/message/GarlicConfig.java b/router/java/src/net/i2p/router/message/GarlicConfig.java
index 9fe299e0c3ec6dc4291fc6b53fda6f16378f9105..4970a589f908daa3381cd4a2a62465159b834090 100644
--- a/router/java/src/net/i2p/router/message/GarlicConfig.java
+++ b/router/java/src/net/i2p/router/message/GarlicConfig.java
@@ -14,7 +14,7 @@ import java.util.List;
 
 import net.i2p.data.Certificate;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DeliveryInstructions;
 
 /**
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java
index c44ec2f055399a6106ed88e77dea961bb332c8c0..3a762b6fd704800e579d319e63c0e1b063754371 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java
@@ -9,7 +9,7 @@ package net.i2p.router.message;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.HandlerJobBuilder;
diff --git a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
index 9392526b3ff9f102342424dc7971d25fa17da6fb..9a22ec6e842d1bce895df7898bfd2faccadf6ef4 100644
--- a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
+++ b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
@@ -9,7 +9,7 @@ package net.i2p.router.message;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.DeliveryInstructions;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 5040606e8d7177b06982b32de5d85ebb91b8ccb3..d9f7a0a516324d478c1feb055ec285344c951207 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -18,7 +18,7 @@ import net.i2p.data.Lease;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.Payload;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
 import net.i2p.data.i2cp.MessageId;
@@ -425,12 +425,19 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                 getContext().statManager().addRateData("client.leaseSetFailedRemoteTime", lookupTime);
             }
             
-            //if (_finished == Result.NONE) {
+
+            int cause;
+            if (getContext().netDb().isNegativeCachedForever(_to.calculateHash())) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Unable to send to " + _toString + " because the sig type is unsupported");
+                cause = MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION;
+            } else {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Unable to send to " + _toString + " because we couldn't find their leaseSet");
-            //}
+                cause = MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET;
+            }
 
-            dieFatal(MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET);
+            dieFatal(cause);
         }
     }
     
diff --git a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java
index 6d0cfdbcf2aebd8c4681f3b8a39edfb75f8bf271..cc7a337cc5ef7147ea021bbaab8c47e1d5995a87 100644
--- a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java
+++ b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java
@@ -11,7 +11,7 @@ package net.i2p.router.message;
 import java.util.Date;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.Job;
 import net.i2p.router.JobImpl;
diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
index 685e18512c03b5939a9a5204b348162fa2681cf0..9931444aaf10f147f37c83d11621383e6cccab8a 100644
--- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
+++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
@@ -14,8 +14,8 @@ import java.util.Set;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseLookupMessage;
diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
index f6a29355b716e2ddeb3e1641dd2b0755fff8cb2d..8b235260e30f2d1c20e04a81e323b2262bd22da8 100644
--- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
@@ -12,7 +12,7 @@ import java.util.Date;
 import java.util.Properties;
 
 import net.i2p.data.DataFormatException;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java
index ebcde49c693bbb9912dacdaf102a51384985e041..f916e976f1ce30b261652692a1cb68bfe5a4b6bc 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java
@@ -25,8 +25,8 @@ import gnu.getopt.Getopt;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.transport.BadCountries;
 import net.i2p.router.transport.GeoIP;
 import net.i2p.util.FileUtil;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java
index c2ea790a5a5adec972f1b62cef481add81e25dc9..b52df8e1790906561eaf82e48370d39a76b62e56 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java
@@ -12,7 +12,7 @@ import java.util.Set;
 
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java
index 88122c0e27b39dd625b3496a5a8285734c6fd25f..aff802b7724e96f1f0b58612868e9f77cf4b5571 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java
@@ -15,6 +15,8 @@ import java.util.Set;
 import net.i2p.data.Hash;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseLookupMessage;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.kademlia.KBucketSet;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
@@ -71,15 +73,15 @@ class ExploreJob extends SearchJob {
      * and PeerSelector doesn't include the floodfill peers,
      * so we add the ff peers ourselves and then use the regular PeerSelector.
      *
-     * TODO should we encrypt this also like we do for normal lookups?
-     * Could the OBEP capture it and reply with a reference to a hostile peer?
-     *
      * @param replyTunnelId tunnel to receive replies through
      * @param replyGateway gateway for the reply tunnel
      * @param expiration when the search should stop
+     * @param peer the peer to send it to
+     *
+     * @return a DatabaseLookupMessage or GarlicMessage
      */
     @Override
-    protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) {
+    protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) {
         DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true);
         msg.setSearchKey(getState().getTarget());
         msg.setFrom(replyGateway);
@@ -127,7 +129,27 @@ class ExploreJob extends SearchJob {
             _log.debug("Peers we don't want to hear about: " + dontIncludePeers);
         
         msg.setDontIncludePeers(dontIncludePeers);
-        return msg;
+
+        // Now encrypt if we can
+        I2NPMessage outMsg;
+        if (getContext().getProperty(IterativeSearchJob.PROP_ENCRYPT_RI, IterativeSearchJob.DEFAULT_ENCRYPT_RI)) {
+            // request encrypted reply?
+            if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) {
+                MessageWrapper.OneTimeSession sess;
+                sess = MessageWrapper.generateSession(getContext());
+                if (_log.shouldLog(Log.INFO))
+                    _log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() +
+                              ' ' + sess.key + ' ' + sess.tag);
+                msg.setReplySession(sess.key, sess.tag);
+            }
+            outMsg = MessageWrapper.wrap(getContext(), msg, peer);
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug(getJobId() + ": Encrypted exploratory DLM for " + getState().getTarget() + " to " +
+                           peer.getIdentity().calculateHash());
+        } else {
+            outMsg = msg;
+        }
+        return outMsg;
     }
     
     /** max # of concurrent searches */
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
index 8f3a193a5ae9430aa613446a219bba274a0d6d04..46f985a9172c45640bfad53aa59391bc46020e78 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
@@ -2,10 +2,10 @@ package net.i2p.router.networkdb.kademlia;
 
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.ReplyJob;
 import net.i2p.router.RouterContext;
@@ -62,6 +62,9 @@ class FloodOnlyLookupMatchJob extends JobImpl implements ReplyJob {
             } else {
                 getContext().netDb().store(dsm.getKey(), (RouterInfo) dsm.getEntry());
             }
+        } catch (UnsupportedCryptoException uce) {
+            _search.failed();
+            return;
         } catch (IllegalArgumentException iae) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn(_search.getJobId() + ": Received an invalid store reply", iae);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java
index 8ef121d0605e8a4260335827cb8db82484f01265..a07ac8c70829b7687a7e3f1d07d59cb18723e9ae 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java
@@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.HandlerJobBuilder;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java
index e4c6ce71af9727503c3bf25f3ea8445dcfc1cd14..99d8c62658a6c959a1e3369c9d6d7873e2bd19ac 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java
@@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.HandlerJobBuilder;
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 99c4beda29ccb80e0fb5c1fbf679066c2e167fe7..d94475ff20597b1c1a8c11bfa2d30a6e918837f0 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java
@@ -3,14 +3,17 @@ package net.i2p.router.networkdb.kademlia;
 import java.util.List;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.peermanager.PeerProfile;
 import net.i2p.router.util.EventLog;
+import net.i2p.stat.Rate;
+import net.i2p.stat.RateStat;
 import net.i2p.util.Log;
+import net.i2p.util.SystemVersion;
 
 /**
  * Simple job to monitor the floodfill pool.
@@ -78,6 +81,10 @@ class FloodfillMonitorJob extends JobImpl {
 
         // auto from here down
 
+        // ARM ElG decrypt is too slow
+        if (SystemVersion.isARM())
+            return false;
+
         // Only if up a while...
         if (getContext().router().getUptime() < MIN_UPTIME)
             return false;
@@ -148,12 +155,22 @@ class FloodfillMonitorJob extends JobImpl {
                    happy = false;
             }
         }
-        
+
+        double elG = 0;
+        RateStat stat = getContext().statManager().getRate("crypto.elGamal.decrypt");
+        if (stat != null) {
+            Rate rate = stat.getRate(60*60*1000L);
+            if (rate != null) {
+                elG = rate.getAvgOrLifetimeAvg();
+                happy = happy && elG <= 40.0d;
+            }
+        }
+
         if (_log.shouldLog(Log.DEBUG)) {
             final RouterContext rc = getContext();
             final String log = String.format(
                     "FF criteria breakdown: happy=%b, capabilities=%s, maxLag=%d, known=%d, " +
-                    "active=%d, participating=%d, offset=%d, ssuAddr=%s",
+                    "active=%d, participating=%d, offset=%d, ssuAddr=%s ElG=%f",
                     happy, 
                     rc.router().getRouterInfo().getCapabilities(),
                     rc.jobQueue().getMaxLag(),
@@ -161,7 +178,8 @@ class FloodfillMonitorJob extends JobImpl {
                     rc.commSystem().countActivePeers(),
                     rc.tunnelManager().getParticipatingCount(),
                     Math.abs(rc.clock().getOffset()),
-                    rc.router().getRouterInfo().getTargetAddress("SSU").toString()
+                    rc.router().getRouterInfo().getTargetAddress("SSU").toString(),
+                    elG
                     );
             _log.debug(log);
         }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
index 24a7bc40a169e9a516d1840fe9187a363f28cd00..9358092260ea0e75d9e0f2c3b72718096d18129c 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
@@ -7,11 +7,13 @@ import java.util.Map;
 import java.util.Set;
 
 import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.data.router.RouterKeyGenerator;
 import net.i2p.router.Job;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
@@ -31,7 +33,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
     private final Set<Hash> _verifiesInProgress;
     private FloodThrottler _floodThrottler;
     private LookupThrottler _lookupThrottler;
-    private NegativeLookupCache _negativeCache;
 
     /**
      *  This is the flood redundancy. Entries are
@@ -65,7 +66,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         _context.statManager().createRateStat("netDb.searchReplyNotValidated", "How many search replies we get that we are NOT able to validate (fetch)", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
         _context.statManager().createRateStat("netDb.searchReplyValidationSkipped", "How many search replies we get from unreliable peers that we skip?", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
         _context.statManager().createRateStat("netDb.republishQuantity", "How many peers do we need to send a found leaseSet to?", "NetworkDatabase", new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
-        _context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l });
     }
 
     @Override
@@ -73,7 +73,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         super.startup();
         _context.jobQueue().addJob(new FloodfillMonitorJob(_context, this));
         _lookupThrottler = new LookupThrottler();
-        _negativeCache = new NegativeLookupCache();
 
         // refresh old routers
         Job rrj = new RefreshRoutersJob(_context, this);
@@ -171,25 +170,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         return _lookupThrottler.shouldThrottle(from, id);
     }
 
-    /**
-     *  Increment in the negative lookup cache
-     *  @since 0.9.4
-     */
-    void lookupFailed(Hash key) {
-        _negativeCache.lookupFailed(key);
-    }
-
-    /**
-     *  Is the key in the negative lookup cache?
-     *  @since 0.9.4
-     */
-    boolean isNegativeCached(Hash key) {
-        boolean rv = _negativeCache.isCached(key);
-        if (rv)
-            _context.statManager().addRateData("netDb.negativeCache", 1);
-        return rv;
-    }
-
     /**
      *  Send to a subset of all floodfill peers.
      *  We do this to implement Kademlia within the floodfills, i.e.
@@ -197,16 +177,17 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
      */
     public void flood(DatabaseEntry ds) {
         Hash key = ds.getHash();
-        Hash rkey = _context.routingKeyGenerator().getRoutingKey(key);
+        RouterKeyGenerator gen = _context.routerKeyGenerator();
+        Hash rkey = gen.getRoutingKey(key);
         FloodfillPeerSelector sel = (FloodfillPeerSelector)getPeerSelector();
         List<Hash> peers = sel.selectFloodfillParticipants(rkey, MAX_TO_FLOOD, getKBuckets());
         // todo key cert skip?
-        long until = _context.routingKeyGenerator().getTimeTillMidnight();
+        long until = gen.getTimeTillMidnight();
         if (until < NEXT_RKEY_LS_ADVANCE_TIME ||
             (ds.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO && until < NEXT_RKEY_RI_ADVANCE_TIME)) {
-            // to avoid lookup failures after midnight, also flood to some closest to the
+            // to avoid lookup faulures after midnight, also flood to some closest to the
             // next routing key for a period of time before midnight.
-            Hash nkey = _context.routingKeyGenerator().getNextRoutingKey(key);
+            Hash nkey = gen.getNextRoutingKey(key);
             List<Hash> nextPeers = sel.selectFloodfillParticipants(nkey, NEXT_FLOOD_QTY, getKBuckets());
             int i = 0;
             for (Hash h : nextPeers) {
@@ -301,7 +282,9 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
     }
     
     /**
-     * Lookup using exploratory tunnels
+     * Lookup using exploratory tunnels.
+     *
+     * Caller should check negative cache and/or banlist before calling.
      *
      * Begin a kademlia style search for the key specified, which can take up to timeoutMs and
      * will fire the appropriate jobs on success or timeout (or if the kademlia search completes
@@ -315,7 +298,10 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
     }
 
     /**
-     * Lookup using the client's tunnels
+     * Lookup using the client's tunnels.
+     *
+     * Caller should check negative cache and/or banlist before calling.
+     *
      * @param fromLocalDest use these tunnels for the lookup, or null for exploratory
      * @return null always
      * @since 0.9.10
@@ -473,6 +459,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         // should we skip the search?
         if (_floodfillEnabled ||
             _context.jobQueue().getMaxLag() > 500 ||
+            _context.banlist().isBanlistedForever(peer) ||
             getKBucketSetSize() > MAX_DB_BEFORE_SKIPPING_SEARCH) {
             // don't try to overload ourselves (e.g. failing 3000 router refs at
             // once, and then firing off 3000 netDb lookup tasks)
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java
index fdf47bf3ccb2db8b6e0f740ef2bf6a237d7b71a9..6870edcd5833239955c8a5a189556b08e9fa8433 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java
@@ -18,8 +18,8 @@ import java.util.Set;
 import java.util.TreeSet;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.kademlia.KBucketSet;
 import net.i2p.kademlia.SelectionCollector;
 import net.i2p.kademlia.XORComparator;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
index f66b50020a770914a7dc9feec16073a99703eb27..085c0c921a7bcea39b43d24425469603602b99ba 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
@@ -6,9 +6,10 @@ import java.util.Set;
 
 import net.i2p.data.Certificate;
 import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
@@ -173,9 +174,9 @@ class FloodfillVerifyStoreJob extends JobImpl {
         FloodfillPeerSelector sel = (FloodfillPeerSelector)_facade.getPeerSelector();
         Certificate keyCert = null;
         if (!_isRouterInfo) {
-            LeaseSet ls = _facade.lookupLeaseSetLocally(_key);
-            if (ls != null) {
-                Certificate cert = ls.getDestination().getCertificate();
+            Destination dest = _facade.lookupDestinationLocally(_key);
+            if (dest != null) {
+                Certificate cert = dest.getCertificate();
                 if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY)
                     keyCert = cert;
             }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java
index 0d03d7d4fed88baaabf0889ab22f3f06eb60a2e3..eceb4b576e4809b62e83b509d9416318db04e982 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java
@@ -11,8 +11,8 @@ package net.i2p.router.networkdb.kademlia;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DatabaseLookupMessage;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
index f623022ca7e134adda1e224be3818ae6689b3028..ffb6d77f00f4106a84bef18b9303b9a4f93e4966 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
@@ -14,9 +14,9 @@ import java.util.Date;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
 import net.i2p.router.JobImpl;
@@ -51,6 +51,8 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
         long recvBegin = System.currentTimeMillis();
         
         String invalidMessage = null;
+        // set if invalid store but not his fault
+        boolean dontBlamePeer = false;
         boolean wasNew = false;
         RouterInfo prevNetDb = null;
         Hash key = _message.getKey();
@@ -72,6 +74,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
                 if (getContext().clientManager().isLocal(key)) {
                     //getContext().statManager().addRateData("netDb.storeLocalLeaseSetAttempt", 1, 0);
                     // throw rather than return, so that we send the ack below (prevent easy attack)
+                    dontBlamePeer = true;
                     throw new IllegalArgumentException("Peer attempted to store local leaseSet: " +
                                                        key.toBase64().substring(0, 4));
                 }
@@ -114,6 +117,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
                     //if (!ls.getReceivedAsReply())
                     //    match.setReceivedAsPublished(true);
                 }
+            } catch (UnsupportedCryptoException uce) {
+                invalidMessage = uce.getMessage();
+                dontBlamePeer = true;
             } catch (IllegalArgumentException iae) {
                 invalidMessage = iae.getMessage();
             }
@@ -131,8 +137,10 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
                 if (getContext().routerHash().equals(key)) {
                     //getContext().statManager().addRateData("netDb.storeLocalRouterInfoAttempt", 1, 0);
                     // throw rather than return, so that we send the ack below (prevent easy attack)
+                    dontBlamePeer = true;
                     throw new IllegalArgumentException("Peer attempted to store our RouterInfo");
                 }
+                getContext().profileManager().heardAbout(key);
                 prevNetDb = getContext().netDb().store(key, ri);
                 wasNew = ((null == prevNetDb) || (prevNetDb.getPublished() < ri.getPublished()));
                 // Check new routerinfo address against blocklist
@@ -152,7 +160,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
                                 _log.warn("New address received, Blocklisting old peer " + key + ' ' + ri);
                     }
                 }
-                getContext().profileManager().heardAbout(key);
+            } catch (UnsupportedCryptoException uce) {
+                invalidMessage = uce.getMessage();
+                dontBlamePeer = true;
             } catch (IllegalArgumentException iae) {
                 invalidMessage = iae.getMessage();
             }
@@ -165,14 +175,16 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
         long recvEnd = System.currentTimeMillis();
         getContext().statManager().addRateData("netDb.storeRecvTime", recvEnd-recvBegin);
         
-        if (_message.getReplyToken() > 0) 
+        // ack even if invalid or unsupported
+        // TODO any cases where we shouldn't?
+        if (_message.getReplyToken() > 0)
             sendAck();
         long ackEnd = System.currentTimeMillis();
         
         if (_from != null)
             _fromHash = _from.getHash();
         if (_fromHash != null) {
-            if (invalidMessage == null) {
+            if (invalidMessage == null || dontBlamePeer) {
                 getContext().profileManager().dbStoreReceived(_fromHash, wasNew);
                 getContext().statManager().addRateData("netDb.storeHandled", ackEnd-recvEnd);
             } else {
@@ -180,7 +192,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Peer " + _fromHash.toBase64() + " sent bad data: " + invalidMessage);
             }
-        } else if (invalidMessage != null) {
+        } else if (invalidMessage != null && !dontBlamePeer) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Unknown peer sent bad data: " + invalidMessage);
         }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java
index a69a2bc9063810c78c899f2ea79ce68232c389f2..6bd687185ca833693a982d6084fd5579b3a8d021 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java
@@ -7,7 +7,7 @@ import java.util.Set;
 import java.util.TreeMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
index 13d76368877357176c371ba621fb8166b5aa454a..ac283109caa317bd9e87174d3e25b34284cdcfab 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
@@ -1,7 +1,7 @@
 package net.i2p.router.networkdb.kademlia;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.util.Log;
 import net.i2p.router.JobImpl;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
index 7a2fa777096ae2d97e37b6e3efd13321b91f45de..490a45a2181b88f4b957a31ea3657c96898cc0bd 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
@@ -13,9 +13,9 @@ import java.util.concurrent.ConcurrentHashMap;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.kademlia.KBucketSet;
 import net.i2p.kademlia.XORComparator;
 import net.i2p.router.CommSystemFacade;
@@ -28,6 +28,8 @@ import net.i2p.router.TunnelInfo;
 import net.i2p.router.TunnelManagerFacade;
 import net.i2p.router.util.RandomIterator;
 import net.i2p.util.Log;
+import net.i2p.util.NativeBigInteger;
+import net.i2p.util.SystemVersion;
 
 /**
  * A traditional Kademlia search that continues to search
@@ -88,8 +90,13 @@ class IterativeSearchJob extends FloodSearchJob {
      */
     private static final int MAX_CONCURRENT = 1;
 
-    /** testing */
-    private static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups";
+    public static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups";
+
+    /** only on fast boxes, for now */
+    public static final boolean DEFAULT_ENCRYPT_RI =
+            SystemVersion.isX86() && SystemVersion.is64Bit() &&
+            !SystemVersion.isApache() && !SystemVersion.isGNU() &&
+            NativeBigInteger.isNative();
 
     /**
      *  Lookup using exploratory tunnels
@@ -315,7 +322,7 @@ class IterativeSearchJob extends FloodSearchJob {
             _sentTime.put(peer, Long.valueOf(now));
 
             I2NPMessage outMsg = null;
-            if (_isLease || getContext().getBooleanProperty(PROP_ENCRYPT_RI)) {
+            if (_isLease || getContext().getProperty(PROP_ENCRYPT_RI, DEFAULT_ENCRYPT_RI)) {
                 // Full ElG is fairly expensive so only do it for LS lookups
                 // if we have the ff RI, garlic encrypt it
                 RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer);
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 b4d108d024bbca3744bc35f5ed7976d312e845dd..5507710cdc0cf2e0000135767d25a1b39fe77733 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -19,14 +19,20 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
+import net.i2p.crypto.SigType;
+import net.i2p.data.Certificate;
 import net.i2p.data.DatabaseEntry;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
+import net.i2p.data.KeyCertificate;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.kademlia.KBucketSet;
 import net.i2p.kademlia.RejectTrimmer;
 import net.i2p.kademlia.SelectionCollector;
@@ -63,6 +69,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     protected final RouterContext _context;
     private final ReseedChecker _reseedChecker;
     private volatile long _lastRIPublishTime;
+    private NegativeLookupCache _negativeCache;
 
     /** 
      * Map of Hash to RepublishLeaseSetJob for leases we'realready managing.
@@ -155,6 +162,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         _reseedChecker = new ReseedChecker(context);
         context.statManager().createRateStat("netDb.lookupDeferred", "how many lookups are deferred?", "NetworkDatabase", new long[] { 60*60*1000 });
         context.statManager().createRateStat("netDb.exploreKeySet", "how many keys are queued for exploration?", "NetworkDatabase", new long[] { 60*60*1000 });
+        context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l });
         // following are for StoreJob
         context.statManager().createRateStat("netDb.storeRouterInfoSent", "How many routerInfo store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l });
         context.statManager().createRateStat("netDb.storeLeaseSetSent", "How many leaseSet store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l });
@@ -223,6 +231,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         //_ds = null;
         _exploreKeys.clear(); // hope this doesn't cause an explosion, it shouldn't.
         // _exploreKeys = null;
+        _negativeCache.clear();
     }
     
     public synchronized void restart() {
@@ -262,6 +271,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         //_ds = new TransientDataStore();
 //        _exploreKeys = new HashSet(64);
         _dbDir = dbDir;
+        _negativeCache = new NegativeLookupCache();
         
         createHandlers();
         
@@ -480,7 +490,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     }
 
     /**
-     *  Lookup using exploratory tunnels
+     *  Lookup using exploratory tunnels.
+     *  Use lookupDestination() if you don't need the LS or need it validated.
      */
     public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
         lookupLeaseSet(key, onFindJob, onFailedLookupJob, timeoutMs, null);
@@ -488,6 +499,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
 
     /**
      *  Lookup using the client's tunnels
+     *  Use lookupDestination() if you don't need the LS or need it validated.
+     *
      *  @param fromLocalDest use these tunnels for the lookup, or null for exploratory
      *  @since 0.9.10
      */
@@ -500,6 +513,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 _log.debug("leaseSet found locally, firing " + onFindJob);
             if (onFindJob != null)
                 _context.jobQueue().addJob(onFindJob);
+        } else if (isNegativeCached(key)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Negative cached, not searching: " + key);
+            if (onFailedLookupJob != null)
+                _context.jobQueue().addJob(onFailedLookupJob);
         } else {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("leaseSet not found locally, running search");
@@ -509,6 +527,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             _log.debug("after lookupLeaseSet");
     }
     
+    /**
+     *  Use lookupDestination() if you don't need the LS or need it validated.
+     */
     public LeaseSet lookupLeaseSetLocally(Hash key) {
         if (!_initialized) return null;
         DatabaseEntry ds = _ds.get(key);
@@ -531,6 +552,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             return null;
         }
     }
+
+    /**
+     *  Lookup using the client's tunnels
+     *  Succeeds even if LS validation and store fails due to unsupported sig type, expired, etc.
+     *
+     *  Note that there are not separate success and fail jobs. Caller must call
+     *  lookupDestinationLocally() in the job to determine success.
+     *
+     *  @param onFinishedJob non-null
+     *  @param fromLocalDest use these tunnels for the lookup, or null for exploratory
+     *  @since 0.9.16
+     */
+    public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {
+        if (!_initialized) return;
+        Destination d = lookupDestinationLocally(key);
+        if (d != null) {
+            _context.jobQueue().addJob(onFinishedJob);
+        } else {
+            search(key, onFinishedJob, onFinishedJob, timeoutMs, true, fromLocalDest);
+        }
+    }
+
+    /**
+     *  Lookup locally in netDB and in badDest cache
+     *  Succeeds even if LS validation fails due to unsupported sig type, expired, etc.
+     *
+     *  @since 0.9.16
+     */
+    public Destination lookupDestinationLocally(Hash key) {
+        if (!_initialized) return null;
+        DatabaseEntry ds = _ds.get(key);
+        if (ds != null) {
+            if (ds.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
+                LeaseSet ls = (LeaseSet)ds;
+                return ls.getDestination();
+            }
+        } else {
+            return _negativeCache.getBadDest(key);
+        }
+        return null;
+    }
     
     public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
         if (!_initialized) return;
@@ -538,6 +600,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         if (ri != null) {
             if (onFindJob != null)
                 _context.jobQueue().addJob(onFindJob);
+        } else if (_context.banlist().isBanlistedForever(key)) {
+            if (onFailedLookupJob != null)
+                _context.jobQueue().addJob(onFailedLookupJob);
         } else {
             search(key, onFindJob, onFailedLookupJob, timeoutMs, false);
         }
@@ -694,9 +759,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
      * Unlike for RouterInfos, this is only called once, when stored.
      * After that, LeaseSet.isCurrent() is used.
      *
+     * @throws UnsupportedCryptoException if that's why it failed.
      * @return reason why the entry is not valid, or null if it is valid
      */
-    private String validate(Hash key, LeaseSet leaseSet) {
+    private String validate(Hash key, LeaseSet leaseSet) throws UnsupportedCryptoException {
         if (!key.equals(leaseSet.getDestination().calculateHash())) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Invalid store attempt! key does not match leaseSet.destination!  key = "
@@ -704,9 +770,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             return "Key does not match leaseSet.destination - " + key.toBase64();
         }
         if (!leaseSet.verifySignature()) {
+            // throws UnsupportedCryptoException
+            processStoreFailure(key, leaseSet);
             if (_log.shouldLog(Log.WARN))
-                _log.warn("Invalid leaseSet signature!  leaseSet = " + leaseSet);
-            return "Invalid leaseSet signature on " + leaseSet.getDestination().calculateHash().toBase64();
+                _log.warn("Invalid leaseSet signature! " + leaseSet);
+            return "Invalid leaseSet signature on " + key;
         }
         long earliest = leaseSet.getEarliestLeaseDate();
         long latest = leaseSet.getLatestLeaseDate();
@@ -722,7 +790,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                           + " first exp. " + new Date(earliest)
                           + " last exp. " + new Date(latest),
                           new Exception("Rejecting store"));
-            return "Expired leaseSet for " + leaseSet.getDestination().calculateHash().toBase64() 
+            return "Expired leaseSet for " + leaseSet.getDestination().calculateHash()
                    + " expired " + DataHelper.formatDuration(age) + " ago";
         }
         if (latest > now + (Router.CLOCK_FUDGE_FACTOR + MAX_LEASE_FUTURE)) {
@@ -739,9 +807,13 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     }
     
     /**
-     * Store the leaseSet
+     * Store the leaseSet.
+     *
+     * If the store fails due to unsupported crypto, it will negative cache
+     * the hash until restart.
      *
      * @throws IllegalArgumentException if the leaseSet is not valid
+     * @throws UnsupportedCryptoException if that's why it failed.
      * @return previous entry or null
      */
     public LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException {
@@ -798,6 +870,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
      *
      * Call this only on first store, to check the key and signature once
      *
+     * If the store fails due to unsupported crypto, it will banlist
+     * the router hash until restart and then throw UnsupportedCrytpoException.
+     *
+     * @throws UnsupportedCryptoException if that's why it failed.
      * @return reason why the entry is not valid, or null if it is valid
      */
     private String validate(Hash key, RouterInfo routerInfo) throws IllegalArgumentException {
@@ -807,6 +883,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             return "Key does not match routerInfo.identity";
         }
         if (!routerInfo.isValid()) {
+            // throws UnsupportedCryptoException
+            processStoreFailure(key, routerInfo);
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Invalid routerInfo signature!  forged router structure!  router = " + routerInfo);
             return "Invalid routerInfo signature";
@@ -892,15 +970,29 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     }
     
     /**
-     * store the routerInfo
+     * Store the routerInfo.
+     *
+     * If the store fails due to unsupported crypto, it will banlist
+     * the router hash until restart and then throw UnsupportedCrytpoException.
      *
      * @throws IllegalArgumentException if the routerInfo is not valid
+     * @throws UnsupportedCryptoException if that's why it failed.
      * @return previous entry or null
      */
     public RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException {
         return store(key, routerInfo, true);
     }
 
+    /**
+     * Store the routerInfo.
+     *
+     * If the store fails due to unsupported crypto, it will banlist
+     * the router hash until restart and then throw UnsupportedCrytpoException.
+     *
+     * @throws IllegalArgumentException if the routerInfo is not valid
+     * @throws UnsupportedCryptoException if that's why it failed.
+     * @return previous entry or null
+     */
     RouterInfo store(Hash key, RouterInfo routerInfo, boolean persist) throws IllegalArgumentException {
         if (!_initialized) return null;
         
@@ -934,6 +1026,59 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             _kb.add(key);
         return rv;
     }
+
+    /**
+     *  If the validate fails, call this
+     *  to determine if it was because of unsupported crypto.
+     *
+     *  If so, this will banlist-forever the router hash or permanently negative cache the dest hash,
+     *  and then throw the exception. Otherwise it does nothing.
+     *
+     *  @throws UnsupportedCryptoException if that's why it failed.
+     *  @since 0.9.16
+     */
+    private void processStoreFailure(Hash h, DatabaseEntry entry) throws UnsupportedCryptoException {
+        if (entry.getHash().equals(h)) {
+            if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
+                LeaseSet ls = (LeaseSet) entry;
+                Destination d = ls.getDestination();
+                Certificate c = d.getCertificate();
+                if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
+                    try {
+                        KeyCertificate kc = c.toKeyCertificate();
+                        SigType type = kc.getSigType();
+                        if (type == null || !type.isAvailable()) {
+                            failPermanently(d);
+                            String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode());
+                            if (_log.shouldLog(Log.WARN))
+                                _log.warn("Unsupported sig type " + stype + " for destination " + h);
+                            throw new UnsupportedCryptoException("Sig type " + stype);
+                        }
+                    } catch (DataFormatException dfe) {}
+                }
+            } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
+                RouterInfo ri = (RouterInfo) entry;
+                RouterIdentity id = ri.getIdentity();
+                Certificate c = id.getCertificate();
+                if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
+                    try {
+                        KeyCertificate kc = c.toKeyCertificate();
+                        SigType type = kc.getSigType();
+                        if (type == null || !type.isAvailable()) {
+                            String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode());
+                            _context.banlist().banlistRouterForever(h, "Unsupported signature type " + stype);
+                            if (_log.shouldLog(Log.WARN))
+                                _log.warn("Unsupported sig type " + stype + " for router " + h);
+                            throw new UnsupportedCryptoException("Sig type " + stype);
+                        }
+                    } catch (DataFormatException dfe) {}
+                }
+            }
+        }
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("Verify fail, cause unknown: " + entry);
+    }
+
     
     /**
      *   Final remove for a leaseset.
@@ -1005,8 +1150,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
      * without any match)
      *
      * Unused - called only by FNDF.searchFull() from FloodSearchJob which is overridden - don't use this.
+     *
+     * @throws UnsupportedOperationException always
      */
     SearchJob search(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, boolean isLease) {
+        throw new UnsupportedOperationException();
+/****
         if (!_initialized) return null;
         boolean isNew = true;
         SearchJob searchJob = null;
@@ -1031,6 +1180,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             _context.statManager().addRateData("netDb.lookupDeferred", deferred, searchJob.getExpiration()-_context.clock().now());
         }
         return searchJob;
+****/
     }
     
     /**
@@ -1102,6 +1252,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         _context.jobQueue().addJob(new StoreJob(_context, this, key, ds, onSuccess, onFailure, sendTimeout, toIgnore));
     }
 
+    /**
+     *  Increment in the negative lookup cache
+     *
+     *  @param key for Destinations or RouterIdentities
+     *  @since 0.9.4 moved from FNDF to KNDF in 0.9.16
+     */
+    void lookupFailed(Hash key) {
+        _negativeCache.lookupFailed(key);
+    }
+
+    /**
+     *  Is the key in the negative lookup cache?
+     *&
+     *  @param key for Destinations or RouterIdentities
+     *  @since 0.9.4 moved from FNDF to KNDF in 0.9.16
+     */
+    boolean isNegativeCached(Hash key) {
+        boolean rv = _negativeCache.isCached(key);
+        if (rv)
+            _context.statManager().addRateData("netDb.negativeCache", 1);
+        return rv;
+    }
+
+    /**
+     *  Negative cache until restart
+     *  @since 0.9.16
+     */
+    void failPermanently(Destination dest) {
+        _negativeCache.failPermanently(dest);
+    }
+
+    /**
+     *  Is it permanently negative cached?
+     *
+     *  @param key only for Destinations; for RouterIdentities, see Banlist
+     *  @since 0.9.16
+     */
+    public boolean isNegativeCachedForever(Hash key) {
+        return _negativeCache.getBadDest(key) != null;
+    }
+
     /**
      * Debug info, HTML formatted
      * @since 0.9.10
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
index 15ae87a8bc06d08b66db151c03d531d3513825e6..ff9d3d50115ca1b8de222a1481b5f3bfd5856945 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
@@ -8,7 +8,7 @@ import net.i2p.crypto.TagSetHandle;
 import net.i2p.data.Certificate;
 import net.i2p.data.Hash;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
 import net.i2p.data.i2np.DeliveryInstructions;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
index abbb67ed624a99aff4192008fd090c84dd5c6177..1784f9434d075a7f892deea38f934a3acb69a109 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
@@ -1,6 +1,9 @@
 package net.i2p.router.networkdb.kademlia;
 
+import java.util.Map;
+import net.i2p.data.Destination;
 import net.i2p.data.Hash;
+import net.i2p.util.LHMCache;
 import net.i2p.util.ObjectCounter;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
@@ -12,11 +15,15 @@ import net.i2p.util.SimpleTimer;
  */
 class NegativeLookupCache {
     private final ObjectCounter<Hash> counter;
+    private final Map<Hash, Destination> badDests;
+
     private static final int MAX_FAILS = 3;
+    private static final int MAX_BAD_DESTS = 128;
     private static final long CLEAN_TIME = 2*60*1000;
 
     public NegativeLookupCache() {
         this.counter = new ObjectCounter<Hash>();
+        this.badDests = new LHMCache<Hash, Destination>(MAX_BAD_DESTS);
         SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME);
     }
 
@@ -25,7 +32,46 @@ class NegativeLookupCache {
     }
 
     public boolean isCached(Hash h) {
-        return this.counter.count(h) >= MAX_FAILS;
+        if (counter.count(h) >= MAX_FAILS)
+            return true;
+        synchronized(badDests) {
+            return badDests.get(h) != null;
+        }
+    }
+
+    /**
+     *  Negative cache the hash until restart,
+     *  but cache the destination.
+     *
+     *  @since 0.9.16
+     */
+    public void failPermanently(Destination dest) {
+        Hash h = dest.calculateHash();
+        synchronized(badDests) {
+            badDests.put(h, dest);
+        }
+    }
+
+    /**
+     *  Get an unsupported but cached Destination
+     *
+     *  @return dest or null if not cached
+     *  @since 0.9.16
+     */
+    public Destination getBadDest(Hash h) {
+        synchronized(badDests) {
+            return badDests.get(h);
+        }
+    }
+
+    /**
+     *  @since 0.9.16
+     */
+    public void clear() {
+        counter.clear();
+        synchronized(badDests) {
+            badDests.clear();
+        }
     }
 
     private class Cleaner implements SimpleTimer.TimedEvent {
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java
index f73eb00fef5aa41f8f86568460cf2ffa2ecf4863..4eb23e13cbd9dbf8d4cbdd814b0d29f6c17660b1 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java
@@ -16,7 +16,7 @@ import java.util.Set;
 import java.util.TreeMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.kademlia.KBucketSet;
 import net.i2p.kademlia.SelectionCollector;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
index 620679b7e498f9766bc79f9d57b7a0aa1129347c..6c60e3a41c82456028cc72f655b6d369e794b481 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
@@ -29,7 +29,7 @@ import net.i2p.data.Base64;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java
index 852a5c7920c2366fc3e4557c9722176415431daf..20dd2a075433698b05cc00eaacd349516533f23b 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java
@@ -5,7 +5,7 @@ import java.util.List;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
index 54a93831965866c70c25051aa87cf07a2eae5386..f0edded8b0d63888bed92992069459cd4b032fbd 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
@@ -17,11 +17,12 @@ import net.i2p.data.DatabaseEntry;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Job;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
@@ -201,22 +202,29 @@ class SearchJob extends JobImpl {
                 _log.debug(getJobId() + ": Already completed");
             return;
         }
+        if (_state.isAborted()) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getJobId() + ": Search aborted");
+            _state.complete();
+            fail();
+            return;
+        }
         if (_log.shouldLog(Log.INFO))
             _log.info(getJobId() + ": Searching: " + _state);
         if (isLocal()) {
             if (_log.shouldLog(Log.INFO))
                 _log.info(getJobId() + ": Key found locally");
-            _state.complete(true);
+            _state.complete();
             succeed();
         } else if (isExpired()) {
             if (_log.shouldLog(Log.INFO))
                 _log.info(getJobId() + ": Key search expired");
-            _state.complete(true);
+            _state.complete();
             fail();
         } else if (_state.getAttempted().size() > MAX_PEERS_QUERIED) {
             if (_log.shouldLog(Log.INFO))
                 _log.info(getJobId() + ": Too many peers quried");
-            _state.complete(true);
+            _state.complete();
             fail();
         } else {
             //_log.debug("Continuing search");
@@ -424,7 +432,7 @@ class SearchJob extends JobImpl {
         int timeout = getPerPeerTimeoutMs(to);
         long expiration = getContext().clock().now() + timeout;
 
-        DatabaseLookupMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration);	
+        I2NPMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration, router);	
 	
         TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundExploratoryTunnel(to);
         if (outTunnel == null) {
@@ -437,9 +445,9 @@ class SearchJob extends JobImpl {
 	
         if (_log.shouldLog(Log.DEBUG))
             _log.debug(getJobId() + ": Sending search to " + to
-                       + " for " + msg.getSearchKey().toBase64() + " w/ replies through [" 
-                       + msg.getFrom().toBase64() + "] via tunnel [" 
-                       + msg.getReplyTunnel() + "]");
+                       + " for " + getState().getTarget() + " w/ replies through " 
+                       + inTunnel.getPeer(0) + " via tunnel " 
+                       + inTunnelId);
 
         SearchMessageSelector sel = new SearchMessageSelector(getContext(), router, _expiration, _state);
         SearchUpdateReplyFoundJob reply = new SearchUpdateReplyFoundJob(getContext(), router, _state, _facade, 
@@ -482,8 +490,11 @@ class SearchJob extends JobImpl {
      * @param replyTunnelId tunnel to receive replies through
      * @param replyGateway gateway for the reply tunnel
      * @param expiration when the search should stop 
+     * @param peer unused here; see ExploreJob extension
+     *
+     * @return a DatabaseLookupMessage
      */
-    protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) {
+    protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) {
         DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true);
         msg.setSearchKey(_state.getTarget());
         //msg.setFrom(replyGateway.getIdentity().getHash());
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java
index 3d756529d833aa60b65c372df52d2b7901c23c42..30e8e4b18ac35e2e735600ff0e40bd0787d94456 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java
@@ -3,7 +3,7 @@ package net.i2p.router.networkdb.kademlia;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java
index 8d8e5f19d9cc45b46fda1a374a126f1dc46428f0..a8354b096fcbfec3aaa95d649d26c1fb5c0ea616 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java
@@ -2,7 +2,7 @@ package net.i2p.router.networkdb.kademlia;
 
 import net.i2p.data.Hash;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
index 106c8ad4dcf35444522aac8c91b00387c2dcf0ea..61bd1b645d61e66a68e3ebf60c110f7e2a071c66 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
@@ -19,15 +19,16 @@ import net.i2p.router.RouterContext;
  */
 class SearchState {
     private final RouterContext _context;
-    private final HashSet<Hash> _pendingPeers;
+    private final Set<Hash> _pendingPeers;
     private final Map<Hash, Long> _pendingPeerTimes;
-    private final HashSet<Hash> _attemptedPeers;
-    private final HashSet<Hash> _failedPeers;
-    private final HashSet<Hash> _successfulPeers;
-    private final HashSet<Hash> _repliedPeers;
+    private final Set<Hash> _attemptedPeers;
+    private final Set<Hash> _failedPeers;
+    private final Set<Hash> _successfulPeers;
+    private final Set<Hash> _repliedPeers;
     private final Hash _searchKey;
     private volatile long _completed;
     private volatile long _started;
+    private volatile boolean _aborted;
     
     public SearchState(RouterContext context, Hash key) {
         _context = context;
@@ -87,10 +88,19 @@ class SearchState {
             return new HashSet<Hash>(_failedPeers);
         }
     }
+
     public boolean completed() { return _completed != -1; }
-    public void complete(boolean completed) {
-        if (completed)
-            _completed = _context.clock().now();
+
+    public void complete() {
+        _completed = _context.clock().now();
+    }
+
+    /** @since 0.9.16 */
+    public boolean isAborted() { return _aborted; }
+
+    /** @since 0.9.16 */
+    public void abort() {
+        _aborted = true;
     }
     
     public long getWhenStarted() { return _started; }
@@ -177,6 +187,8 @@ class SearchState {
             buf.append(" completed? false ");
         else
             buf.append(" completed on ").append(new Date(_completed));
+        if (_aborted)
+            buf.append("  (Aborted)");
         buf.append("\n\tAttempted: ");
         synchronized (_attemptedPeers) {
             buf.append(_attemptedPeers.size()).append(' ');
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
index 22602b497a6d7d787dceab1f2145be80e6955a55..63cc1c18f8947384830e6eaaf3b88c7f6d40d99b 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
@@ -5,7 +5,7 @@ import java.util.Date;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -18,24 +18,26 @@ import net.i2p.util.Log;
 /**
  * Called after a match to a db search is found
  *
+ * Used only by SearchJob which is only used by ExploreJob
  */
 class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob {
-    private Log _log;
+    private final Log _log;
     private I2NPMessage _message;
-    private Hash _peer;
-    private SearchState _state;
-    private KademliaNetworkDatabaseFacade _facade;
-    private SearchJob _job;
-    private TunnelInfo _outTunnel;
-    private TunnelInfo _replyTunnel;
-    private boolean _isFloodfillPeer;
-    private long _sentOn;
+    private final Hash _peer;
+    private final SearchState _state;
+    private final KademliaNetworkDatabaseFacade _facade;
+    private final SearchJob _job;
+    private final TunnelInfo _outTunnel;
+    private final TunnelInfo _replyTunnel;
+    private final boolean _isFloodfillPeer;
+    private final long _sentOn;
     
     public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, 
                                      SearchState state, KademliaNetworkDatabaseFacade facade, 
                                      SearchJob job) {
         this(context, peer, state, facade, job, null, null);
     }
+
     public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, 
                                      SearchState state, KademliaNetworkDatabaseFacade facade, 
                                      SearchJob job, TunnelInfo outTunnel, TunnelInfo replyTunnel) {
@@ -52,6 +54,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob {
     }
     
     public String getName() { return "Update Reply Found for Kademlia Search"; }
+
     public void runJob() {
         if (_isFloodfillPeer)
             _job.decrementOutstandingFloodfillSearches();
@@ -59,7 +62,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob {
         I2NPMessage message = _message;
         if (_log.shouldLog(Log.INFO))
             _log.info(getJobId() + ": Reply from " + _peer.toBase64() 
-                      + " with message " + message.getClass().getName());
+                      + " with message " + message.getClass().getSimpleName());
         
         long howLong = System.currentTimeMillis() - _sentOn;
         // assume requests are 1KB (they're almost always much smaller, but tunnels have a fixed size)
@@ -78,34 +81,21 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob {
         
         if (message instanceof DatabaseStoreMessage) {
             long timeToReply = _state.dataFound(_peer);
-            
             DatabaseStoreMessage msg = (DatabaseStoreMessage)message;
             DatabaseEntry entry = msg.getEntry();
-            if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
-                try {
-                    _facade.store(msg.getKey(), (LeaseSet) entry);
-                    getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
-                } catch (IllegalArgumentException iae) {
-                    if (_log.shouldLog(Log.ERROR))
-                        _log.warn("Peer " + _peer + " sent us an invalid leaseSet: " + iae.getMessage());
-                    getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
-                }
-            } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
-                if (_log.shouldLog(Log.INFO))
-                    _log.info(getJobId() + ": dbStore received on search containing router " 
-                              + msg.getKey() + " with publishDate of " 
-                              + new Date(entry.getDate()));
-                try {
-                    _facade.store(msg.getKey(), (RouterInfo) entry);
-                    getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
-                } catch (IllegalArgumentException iae) {
-                    if (_log.shouldLog(Log.ERROR))
-                        _log.warn("Peer " + _peer + " sent us an invalid routerInfo: " + iae.getMessage());
-                    getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
-                }
-            } else {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error(getJobId() + ": Unknown db store type?!@ " + entry.getType());
+            try {
+                _facade.store(msg.getKey(), entry);
+                getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
+            } catch (UnsupportedCryptoException iae) {
+                // don't blame the peer
+                getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
+                _state.abort();
+                // searchNext() will call fail()
+            } catch (IllegalArgumentException iae) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Peer " + _peer + " sent us invalid data: ", iae);
+                // blame the peer
+                getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
             }
         } else if (message instanceof DatabaseSearchReplyMessage) {
             _job.replyFound((DatabaseSearchReplyMessage)message, _peer);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java
index 711510fb4d39c6b1dbed94e6bde557a5019652a4..62a878d6dedf70988315cebba31198febc6f06e1 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java
@@ -1,7 +1,7 @@
 package net.i2p.router.networkdb.kademlia;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java
index cced2f5466511db1b0efac7acf1a4116faeb58bf..7938ab072c200b7f957ec8f4d444a6695bd11063 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java
@@ -12,7 +12,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
index 311f2ba8f1c7a663ad420b5efb82f302dbd1c777..81bef6950aceeabf9229ec81d8bca6aa1f8f856f 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
@@ -18,7 +18,7 @@ import net.i2p.data.DatabaseEntry;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java
index 6901f0afa6911af69dc08362f0b4652da48c70b6..352c19cd710a3b586adac18bb2651cf5ccfe97bf 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java
@@ -1,7 +1,7 @@
 package net.i2p.router.networkdb.kademlia;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DeliveryStatusMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.MessageSelector;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java
index 2a7318d25f08e64da79cb6fb119e0677a4bc7f93..11603efb989bc52d827e533157a5efd92a8e0096 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java
@@ -18,7 +18,7 @@ import java.util.Set;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0159eb02017f56d6f9b5230a143a01c6a490ee95
--- /dev/null
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java
@@ -0,0 +1,18 @@
+package net.i2p.router.networkdb.kademlia;
+
+/**
+ *  Signature verification failed because the
+ *  sig type is unknown or unavailable.
+ *
+ *  @since 0.9.16
+ */
+public class UnsupportedCryptoException extends IllegalArgumentException {
+
+    public UnsupportedCryptoException(String msg) {
+        super(msg);
+    }
+
+    public UnsupportedCryptoException(String msg, Throwable t) {
+        super(msg, t);
+    }
+}
diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java
index ef7b1de349bbd7578e45aa763c7befca998cbbe4..d2de16d9ba5c4a60c8fa51f561824995b91d4b0e 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerManager.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java
@@ -19,7 +19,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.PeerSelectionCriteria;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java
index 24176a208872b882b9605c7dd5045b1fc65abf87..466c63ec190e0507eea0625c2f829eef94f1088c 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java
@@ -5,7 +5,7 @@ import java.util.List;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index a224f576ac93e4eda765f09356cfe41879eb30a2..7e89cab2550be77b9591e0add5e634a862f8a6ed 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -19,8 +19,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.NetworkDatabaseFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.tunnel.pool.TunnelPeerSelector;
diff --git a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
index 3af4164b5eb8e0f680fca7525dfd7da05e721e4e..305869badae5c22bd6754d0709d67aa015e02b1e 100644
--- a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
+++ b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
@@ -16,7 +16,7 @@ import net.i2p.router.tasks.ReadConfigJob;
 import net.i2p.util.Log;
 
 /** This actually boots almost everything */
-public class BootCommSystemJob extends JobImpl {
+class BootCommSystemJob extends JobImpl {
     private Log _log;
     
     public static final String PROP_USE_TRUSTED_LINKS = "router.trustedLinks";
diff --git a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java
index bf8d36a7729b4c8c16d2319325207115da1fa25c..e512f9ea381819409c6ead9b27d038f9dcf19d6a 100644
--- a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java
+++ b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java
@@ -12,7 +12,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 
 /** start up the network database */
-public class BootNetworkDbJob extends JobImpl {
+class BootNetworkDbJob extends JobImpl {
     
     public BootNetworkDbJob(RouterContext ctx) {
         super(ctx);
diff --git a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java
index 7ac5254f0e936484681751c3503ab67ebc38865f..33f4010236e1f76b76e85a10f44b6f30c3bf7686 100644
--- a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java
+++ b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java
@@ -12,7 +12,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 
 /** start up the peer manager */
-public class BootPeerManagerJob extends JobImpl {
+class BootPeerManagerJob extends JobImpl {
     
     public BootPeerManagerJob(RouterContext ctx) {
         super(ctx);
diff --git a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java
index 3b88a2d9cd179e485ae098a2bfc3145e3b625693..566204068b865e436b978109d5e480dfca2687f4 100644
--- a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java
+++ b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java
@@ -15,7 +15,7 @@ import net.i2p.router.RouterContext;
 /**
  *  For future restricted routes. Does nothing now.
  */
-public class BuildTrustedLinksJob extends JobImpl {
+class BuildTrustedLinksJob extends JobImpl {
     private final Job _next;
     
     public BuildTrustedLinksJob(RouterContext context, Job next) {
diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
index 498cbe6658be5ecd009ba3e120d075fbd7aba3be..83f2c7a7efee30d1b3f70ad973a551227b3ec383 100644
--- a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
@@ -12,16 +12,22 @@ import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.security.GeneralSecurityException;
 import java.util.Properties;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.KeyCertificate;
 import net.i2p.data.PrivateKey;
+import net.i2p.data.PrivateKeyFile;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
+import net.i2p.data.SimpleDataStructure;
 import net.i2p.router.Job;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
@@ -40,7 +46,14 @@ public class CreateRouterInfoJob extends JobImpl {
     private final Log _log;
     private final Job _next;
     
-    public CreateRouterInfoJob(RouterContext ctx, Job next) {
+    public static final String INFO_FILENAME = "router.info";
+    public static final String KEYS_FILENAME = "router.keys";
+    public static final String KEYS2_FILENAME = "router.keys.dat";
+    private static final String PROP_ROUTER_SIGTYPE = "router.sigType";
+    /** TODO when changing, check isAvailable() and fallback to DSA_SHA1 */
+    private static final SigType DEFAULT_SIGTYPE = SigType.DSA_SHA1;
+
+    CreateRouterInfoJob(RouterContext ctx, Job next) {
         super(ctx);
         _next = next;
         _log = ctx.logManager().getLog(CreateRouterInfoJob.class);
@@ -59,9 +72,13 @@ public class CreateRouterInfoJob extends JobImpl {
     
     /**
      *  Writes 6 files: router.info (standard RI format),
-     *  router,keys, and 4 individual key files under keyBackup/
+     *  router.keys2, and 4 individual key files under keyBackup/
+     *
+     *  router.keys2 file format: This is the
+     *  same "eepPriv.dat" format used by the client code,
+     *  as documented in PrivateKeyFile.
      *
-     *  router.keys file format: Note that this is NOT the
+     *  Old router.keys file format: Note that this is NOT the
      *  same "eepPriv.dat" format used by the client code.
      *<pre>
      *   - Private key (256 bytes)
@@ -74,9 +91,9 @@ public class CreateRouterInfoJob extends JobImpl {
      *  Caller must hold Router.routerInfoFileLock.
      */
     RouterInfo createRouterInfo() {
+        SigType type = getSigTypeConfig(getContext());
         RouterInfo info = new RouterInfo();
         OutputStream fos1 = null;
-        OutputStream fos2 = null;
         try {
             info.setAddresses(getContext().commSystem().createAddresses());
             Properties stats = getContext().statPublisher().publishStatistics();
@@ -86,21 +103,26 @@ public class CreateRouterInfoJob extends JobImpl {
             // not necessary, in constructor
             //info.setPeers(new HashSet());
             info.setPublished(getCurrentPublishDate(getContext()));
+            Object keypair[] = getContext().keyGenerator().generatePKIKeypair();
+            PublicKey pubkey = (PublicKey)keypair[0];
+            PrivateKey privkey = (PrivateKey)keypair[1];
+            SimpleDataStructure signingKeypair[] = getContext().keyGenerator().generateSigningKeys(type);
+            SigningPublicKey signingPubKey = (SigningPublicKey)signingKeypair[0];
+            SigningPrivateKey signingPrivKey = (SigningPrivateKey)signingKeypair[1];
             RouterIdentity ident = new RouterIdentity();
-            Certificate cert = getContext().router().createCertificate();
+            Certificate cert = createCertificate(getContext(), signingPubKey);
             ident.setCertificate(cert);
-            PublicKey pubkey = null;
-            PrivateKey privkey = null;
-            SigningPublicKey signingPubKey = null;
-            SigningPrivateKey signingPrivKey = null;
-            Object keypair[] = getContext().keyGenerator().generatePKIKeypair();
-            pubkey = (PublicKey)keypair[0];
-            privkey = (PrivateKey)keypair[1];
-            Object signingKeypair[] = getContext().keyGenerator().generateSigningKeypair();
-            signingPubKey = (SigningPublicKey)signingKeypair[0];
-            signingPrivKey = (SigningPrivateKey)signingKeypair[1];
             ident.setPublicKey(pubkey);
             ident.setSigningPublicKey(signingPubKey);
+            byte[] padding;
+            int padLen = SigningPublicKey.KEYSIZE_BYTES - signingPubKey.length();
+            if (padLen > 0) {
+                padding = new byte[padLen];
+                getContext().random().nextBytes(padding);
+                ident.setPadding(padding);
+            } else {
+                padding = null;
+            }
             info.setIdentity(ident);
             
             info.sign(signingPrivKey);
@@ -108,34 +130,54 @@ public class CreateRouterInfoJob extends JobImpl {
             if (!info.isValid())
                 throw new DataFormatException("RouterInfo we just built is invalid: " + info);
             
-            String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-            File ifile = new File(getContext().getRouterDir(), infoFilename);
+            // remove router.keys
+            (new File(getContext().getRouterDir(), KEYS_FILENAME)).delete();
+
+            // write router.info
+            File ifile = new File(getContext().getRouterDir(), INFO_FILENAME);
             fos1 = new BufferedOutputStream(new SecureFileOutputStream(ifile));
             info.writeBytes(fos1);
             
-            String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-            File kfile = new File(getContext().getRouterDir(), keyFilename);
-            fos2 = new BufferedOutputStream(new SecureFileOutputStream(kfile));
-            privkey.writeBytes(fos2);
-            signingPrivKey.writeBytes(fos2);
-            pubkey.writeBytes(fos2);
-            signingPubKey.writeBytes(fos2);
+            // write router.keys.dat
+            File kfile = new File(getContext().getRouterDir(), KEYS2_FILENAME);
+            PrivateKeyFile pkf = new PrivateKeyFile(kfile, pubkey, signingPubKey, cert,
+                                                    privkey, signingPrivKey, padding);
+            pkf.write();
             
             getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
             
-            _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
             getContext().router().eventLog().addEvent(EventLog.REKEYED, ident.calculateHash().toBase64());
+        } catch (GeneralSecurityException gse) {
+            _log.log(Log.CRIT, "Error building the new router information", gse);
         } catch (DataFormatException dfe) {
             _log.log(Log.CRIT, "Error building the new router information", dfe);
         } catch (IOException ioe) {
             _log.log(Log.CRIT, "Error writing out the new router information", ioe);
         } finally {
             if (fos1 != null) try { fos1.close(); } catch (IOException ioe) {}
-            if (fos2 != null) try { fos2.close(); } catch (IOException ioe) {}
         }
         return info;
     }
     
+    /**
+     *  The configured SigType to expect on read-in
+     *  @since 0.9.16
+     */
+    public static SigType getSigTypeConfig(RouterContext ctx) {
+        SigType cstype = CreateRouterInfoJob.DEFAULT_SIGTYPE;
+        String sstype = ctx.getProperty(PROP_ROUTER_SIGTYPE);
+        if (sstype != null) {
+            SigType ntype = SigType.parseSigType(sstype);
+            if (ntype != null)
+                cstype = ntype;
+        }
+        // fallback?
+        if (cstype != SigType.DSA_SHA1 && !cstype.isAvailable())
+            cstype = SigType.DSA_SHA1;
+        return cstype;
+    }
     
     /**
      * We probably don't want to expose the exact time at which a router published its info.
@@ -146,4 +188,22 @@ public class CreateRouterInfoJob extends JobImpl {
         //_log.info("Setting published date to /now/");
         return context.clock().now();
     }
+
+    /**
+     *  Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob.
+     *  Not called by periodic RepublishLocalRouterInfoJob.
+     *  We don't want to change the cert on the fly as it changes the router hash.
+     *  RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert.
+     *  There's no reason to ever add a hidden cert?
+     *
+     *  @return the certificate for a new RouterInfo - probably a null cert.
+     *  @since 0.9.16 moved from Router
+     */
+    static Certificate createCertificate(RouterContext ctx, SigningPublicKey spk) {
+        if (spk.getType() != SigType.DSA_SHA1)
+            return new KeyCertificate(spk);
+        if (ctx.getBooleanProperty(Router.PROP_HIDDEN))
+            return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null);
+        return Certificate.NULL_CERT;
+    }
 }
diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
index 9a560da5f42ecebda2508d463e60b1b970985f7a..e02ef04aa1ed2ce4f26ea30c6da511def9e740fd 100644
--- a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
@@ -15,18 +15,28 @@ import java.io.InputStream;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import net.i2p.crypto.KeyGenerator;
+import net.i2p.crypto.SigType;
+import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.data.router.RouterPrivateKeyFile;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
-public class LoadRouterInfoJob extends JobImpl {
+/**
+ *  Run once or twice at startup by StartupJob,
+ *  and then runs BootCommSystemJob
+ */
+class LoadRouterInfoJob extends JobImpl {
     private final Log _log;
     private RouterInfo _us;
     private static final AtomicBoolean _keyLengthChecked = new AtomicBoolean();
@@ -45,6 +55,7 @@ public class LoadRouterInfoJob extends JobImpl {
         if (_us == null) {
             RebuildRouterInfoJob r = new RebuildRouterInfoJob(getContext());
             r.rebuildRouterInfo(false);
+            // run a second time
             getContext().jobQueue().addJob(this);
             return;
         } else {
@@ -54,18 +65,21 @@ public class LoadRouterInfoJob extends JobImpl {
         }
     }
     
+    /**
+     *  Loads router.info and router.keys2 or router.keys.
+     *
+     *  See CreateRouterInfoJob for file formats
+     */
     private void loadRouterInfo() {
-        String routerInfoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
         RouterInfo info = null;
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        
-        File rif = new File(getContext().getRouterDir(), routerInfoFile);
+        File rif = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
         boolean infoExists = rif.exists();
-        File rkf = new File(getContext().getRouterDir(), keyFilename);
+        File rkf = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
         boolean keysExist = rkf.exists();
+        File rkf2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
+        boolean keys2Exist = rkf2.exists();
         
         InputStream fis1 = null;
-        InputStream fis2 = null;
         try {
             // if we have a routerinfo but no keys, things go bad in a hurry:
             // CRIT   ...rkdb.PublishLocalRouterInfoJob: Internal error - signing private key not known?  rescheduling publish for 30s
@@ -73,7 +87,7 @@ public class LoadRouterInfoJob extends JobImpl {
             // CRIT   ...sport.udp.EstablishmentManager: Error in the establisher java.lang.NullPointerException
             // at net.i2p.router.transport.udp.PacketBuilder.buildSessionConfirmedPacket(PacketBuilder.java:574)
             // so pretend the RI isn't there if there is no keyfile
-            if (infoExists && keysExist) {
+            if (infoExists && (keys2Exist || keysExist)) {
                 fis1 = new BufferedInputStream(new FileInputStream(rif));
                 info = new RouterInfo();
                 info.readBytes(fis1);
@@ -85,29 +99,32 @@ public class LoadRouterInfoJob extends JobImpl {
                 _us = info;
             }
             
-            if (keysExist) {
-                fis2 = new BufferedInputStream(new FileInputStream(rkf));
-                PrivateKey privkey = new PrivateKey();
-                privkey.readBytes(fis2);
-                if (shouldRebuild(privkey)) {
+            if (keys2Exist || keysExist) {
+                KeyData kd = readKeyData(rkf, rkf2);
+                PublicKey pubkey = kd.routerIdentity.getPublicKey();
+                SigningPublicKey signingPubKey = kd.routerIdentity.getSigningPublicKey();
+                PrivateKey privkey = kd.privateKey;
+                SigningPrivateKey signingPrivKey = kd.signingPrivateKey;
+                SigType stype = signingPubKey.getType();
+
+                // check if the sigtype config changed
+                SigType cstype = CreateRouterInfoJob.getSigTypeConfig(getContext());
+                boolean sigTypeChanged = stype != cstype;
+
+                if (sigTypeChanged || shouldRebuild(privkey)) {
+                    if (sigTypeChanged)
+                        _log.logAlways(Log.WARN, "Rebuilding RouterInfo with new signature type " + cstype);
                     _us = null;
                     // windows... close before deleting
                     if (fis1 != null) {
                         try { fis1.close(); } catch (IOException ioe) {}
                         fis1 = null;
                     }
-                    try { fis2.close(); } catch (IOException ioe) {}
-                    fis2 = null;
                     rif.delete();
                     rkf.delete();
+                    rkf2.delete();
                     return;
                 }
-                SigningPrivateKey signingPrivKey = new SigningPrivateKey();
-                signingPrivKey.readBytes(fis2);
-                PublicKey pubkey = new PublicKey();
-                pubkey.readBytes(fis2);
-                SigningPublicKey signingPubKey = new SigningPublicKey();
-                signingPubKey.readBytes(fis2);
                 
                 getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
             }
@@ -119,12 +136,9 @@ public class LoadRouterInfoJob extends JobImpl {
                 try { fis1.close(); } catch (IOException ioe2) {}
                 fis1 = null;
             }
-            if (fis2 != null) {
-                try { fis2.close(); } catch (IOException ioe2) {}
-                fis2 = null;
-            }
             rif.delete();
             rkf.delete();
+            rkf2.delete();
         } catch (DataFormatException dfe) {
             _log.log(Log.CRIT, "Corrupt router info or keys at " + rif.getAbsolutePath() + " / " + rkf.getAbsolutePath(), dfe);
             _us = null;
@@ -133,15 +147,11 @@ public class LoadRouterInfoJob extends JobImpl {
                 try { fis1.close(); } catch (IOException ioe) {}
                 fis1 = null;
             }
-            if (fis2 != null) {
-                try { fis2.close(); } catch (IOException ioe) {}
-                fis2 = null;
-            }
             rif.delete();
             rkf.delete();
+            rkf2.delete();
         } finally {
             if (fis1 != null) try { fis1.close(); } catch (IOException ioe) {}
-            if (fis2 != null) try { fis2.close(); } catch (IOException ioe) {}
         }
     }
 
@@ -174,4 +184,68 @@ public class LoadRouterInfoJob extends JobImpl {
             _log.logAlways(Log.WARN, "Rebuilding RouterInfo with faster key");
         return uselong != haslong;
     }
+
+    /** @since 0.9.16 */
+    public static class KeyData {
+        public final RouterIdentity routerIdentity;
+        public final PrivateKey privateKey;
+        public final SigningPrivateKey signingPrivateKey;
+
+        public KeyData(RouterIdentity ri, PrivateKey pk, SigningPrivateKey spk) {
+            routerIdentity = ri;
+            privateKey = pk;
+            signingPrivateKey = spk;
+        }
+    }
+
+    /**
+     *  @param rkf1 in router.keys format, tried second
+     *  @param rkf2 in eepPriv.dat format, tried first
+     *  @return non-null, throws IOE if neither exisits
+     *  @since 0.9.16
+     */
+    public static KeyData readKeyData(File rkf1, File rkf2) throws DataFormatException, IOException {
+        RouterIdentity ri;
+        PrivateKey privkey;
+        SigningPrivateKey signingPrivKey;
+        if (rkf2.exists()) {
+            RouterPrivateKeyFile pkf = new RouterPrivateKeyFile(rkf2);
+            ri = pkf.getRouterIdentity();
+            if (!pkf.validateKeyPairs())
+                throw new DataFormatException("Key pairs invalid");
+            privkey = pkf.getPrivKey();
+            signingPrivKey = pkf.getSigningPrivKey();
+        } else {
+            InputStream fis = null;
+            try {
+                fis = new BufferedInputStream(new FileInputStream(rkf1));
+                privkey = new PrivateKey();
+                privkey.readBytes(fis);
+                signingPrivKey = new SigningPrivateKey();
+                signingPrivKey.readBytes(fis);
+                PublicKey pubkey = new PublicKey();
+                pubkey.readBytes(fis);
+                SigningPublicKey signingPubKey = new SigningPublicKey();
+                signingPubKey.readBytes(fis);
+
+                // validate
+                try {
+                    if (!pubkey.equals(KeyGenerator.getPublicKey(privkey)))
+                        throw new DataFormatException("Key pairs invalid");
+                    if (!signingPubKey.equals(KeyGenerator.getSigningPublicKey(signingPrivKey)))
+                        throw new DataFormatException("Key pairs invalid");
+                } catch (IllegalArgumentException iae) {
+                    throw new DataFormatException("Key pairs invalid", iae);
+                }
+
+                ri = new RouterIdentity();
+                ri.setPublicKey(pubkey);
+                ri.setSigningPublicKey(signingPubKey);
+                ri.setCertificate(Certificate.NULL_CERT);
+            } finally {
+                if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+            }
+        }
+        return new KeyData(ri, privkey, signingPrivKey);
+    }
 }
diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
index b611114d06bb1953addb61e885873c851c920e00..ef0826cf5fbcb99525f687249bd0319c581260ed 100644
--- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
@@ -9,22 +9,24 @@ package net.i2p.router.startup;
  */
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Properties;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
 import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.startup.LoadRouterInfoJob.KeyData;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
@@ -44,7 +46,7 @@ import net.i2p.util.SecureFileOutputStream;
  * router.info.rebuild file is deleted
  *
  */
-public class RebuildRouterInfoJob extends JobImpl {
+class RebuildRouterInfoJob extends JobImpl {
     private final Log _log;
     
     private final static long REBUILD_DELAY = 45*1000; // every 30 seconds
@@ -57,11 +59,11 @@ public class RebuildRouterInfoJob extends JobImpl {
     public String getName() { return "Rebuild Router Info"; }
     
     public void runJob() {
+        throw new UnsupportedOperationException();
+/****
         _log.debug("Testing to rebuild router info");
-        String infoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File info = new File(getContext().getRouterDir(), infoFile);
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        File keyFile = new File(getContext().getRouterDir(), keyFilename);
+        File info = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
+        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
         
         if (!info.exists() || !keyFile.exists()) {
             _log.info("Router info file [" + info.getAbsolutePath() + "] or private key file [" + keyFile.getAbsolutePath() + "] deleted, rebuilding");
@@ -71,51 +73,37 @@ public class RebuildRouterInfoJob extends JobImpl {
         }
         getTiming().setStartAfter(getContext().clock().now() + REBUILD_DELAY);
         getContext().jobQueue().addJob(this);
+****/
     }
     
     void rebuildRouterInfo() {
         rebuildRouterInfo(true);
     }
 
+    /**
+     *  @param alreadyRunning unused
+     */
     void rebuildRouterInfo(boolean alreadyRunning) {
         _log.debug("Rebuilding the new router info");
         RouterInfo info = null;
-        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File infoFile = new File(getContext().getRouterDir(), infoFilename);
-        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
-        File keyFile = new File(getContext().getRouterDir(), keyFilename);
+        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
+        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
+        File keyFile2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
         
-        if (keyFile.exists()) {
+        if (keyFile2.exists() || keyFile.exists()) {
             // ok, no need to rebuild a brand new identity, just update what we can
             RouterInfo oldinfo = getContext().router().getRouterInfo();
             if (oldinfo == null) {
-                info = new RouterInfo();
-                FileInputStream fis = null;
                 try {
-                    fis = new FileInputStream(keyFile);
-                    PrivateKey privkey = new PrivateKey();
-                    privkey.readBytes(fis);
-                    SigningPrivateKey signingPrivKey = new SigningPrivateKey();
-                    signingPrivKey.readBytes(fis);
-                    PublicKey pubkey = new PublicKey();
-                    pubkey.readBytes(fis);
-                    SigningPublicKey signingPubKey = new SigningPublicKey();
-                    signingPubKey.readBytes(fis);
-                    RouterIdentity ident = new RouterIdentity();
-                    Certificate cert = getContext().router().createCertificate();
-                    ident.setCertificate(cert);
-                    ident.setPublicKey(pubkey);
-                    ident.setSigningPublicKey(signingPubKey);
-                    info.setIdentity(ident);
+                    KeyData kd = LoadRouterInfoJob.readKeyData(keyFile, keyFile2);
+                    info = new RouterInfo();
+                    info.setIdentity(kd.routerIdentity);
                 } catch (Exception e) {
                     _log.log(Log.CRIT, "Error reading in the key data from " + keyFile.getAbsolutePath(), e);
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
-                    fis = null;
                     keyFile.delete();
+                    keyFile2.delete();
                     rebuildRouterInfo(alreadyRunning);
                     return;
-                } finally {
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
                 }
             } else {
                 // Make a new RI from the old identity, or else info.setAddresses() will throw an ISE
@@ -160,12 +148,14 @@ public class RebuildRouterInfoJob extends JobImpl {
             _log.warn("Private key file " + keyFile.getAbsolutePath() + " deleted!  Rebuilding a brand new router identity!");
             // this proc writes the keys and info to the file as well as builds the latest and greatest info
             CreateRouterInfoJob j = new CreateRouterInfoJob(getContext(), null);
-            info = j.createRouterInfo();
+            synchronized (getContext().router().routerInfoFileLock) {
+                info = j.createRouterInfo();
+            }
         }
         
         //MessageHistory.initialize();
         getContext().router().setRouterInfo(info);
-        _log.info("Router info rebuilt and stored at " + infoFilename + " [" + info + "]");
+        _log.info("Router info rebuilt and stored at " + infoFile + " [" + info + "]");
     }
     
 }
diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
index 50d840d4322be182f20990c1d1d584e909cb49c7..672bf702aab4eb6b79ed5da0d54dbd21784d6f01 100644
--- a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
+++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
@@ -12,7 +12,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 
 /** start I2CP interface */
-public class StartAcceptingClientsJob extends JobImpl {
+class StartAcceptingClientsJob extends JobImpl {
     
     public StartAcceptingClientsJob(RouterContext context) {
         super(context);
diff --git a/router/java/src/net/i2p/router/startup/WorkingDir.java b/router/java/src/net/i2p/router/startup/WorkingDir.java
index d44b2a53971054454d61dac7934d983765163b1e..23149c7599697990cde79ee146d655eafea549cc 100644
--- a/router/java/src/net/i2p/router/startup/WorkingDir.java
+++ b/router/java/src/net/i2p/router/startup/WorkingDir.java
@@ -147,7 +147,7 @@ public class WorkingDir {
         // Check for a router.keys file or logs dir, if either exists it's an old install,
         // and only migrate the data files if told to do so
         // (router.keys could be deleted later by a killkeys())
-        test = new File(oldDirf, "router.keys");
+        test = new File(oldDirf, CreateRouterInfoJob.KEYS_FILENAME);
         boolean oldInstall = test.exists();
         if (!oldInstall) {
             test = new File(oldDirf, "logs");
diff --git a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
index c2ded9b49131698f975243a8be8c3b77e66f53ce..5d86a291b05b0c854eb18f673b9553392c25f758 100644
--- a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
+++ b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
@@ -31,7 +31,7 @@ public class GracefulShutdown implements Runnable {
                     else if (gracefulExitCode == Router.EXIT_HARD_RESTART)
                         log.log(Log.CRIT, "Restarting after a brief delay");
                     else
-                        log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die");
+                        log.log(Log.CRIT, "Graceful shutdown progress: No more tunnels, starting final shutdown");
                     // Allow time for a UI reponse
                     try {
                         synchronized (Thread.currentThread()) {
diff --git a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
index c29dd257725eb251c538f9124f2b42b27b13bb36..6a14a51346d510e4da9935e71744b4061d1af620 100644
--- a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
@@ -13,10 +13,10 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 
 import net.i2p.data.DataFormatException;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.JobImpl;
-import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.startup.CreateRouterInfoJob;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
@@ -37,8 +37,7 @@ public class PersistRouterInfoJob extends JobImpl {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Persisting updated router info");
 
-        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
-        File infoFile = new File(getContext().getRouterDir(), infoFilename);
+        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
 
         RouterInfo info = getContext().router().getRouterInfo();
 
diff --git a/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java b/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
index 2952c2eda430fc37df6a487414ea52a48995e838..5b367620be13d277279258cbc486b23c67d3239f 100644
--- a/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
+++ b/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
@@ -8,7 +8,7 @@ package net.i2p.router.tasks;
  *
  */
 
-import net.i2p.data.RoutingKeyGenerator;
+import net.i2p.data.router.RouterKeyGenerator;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
@@ -33,7 +33,7 @@ public class UpdateRoutingKeyModifierJob extends JobImpl {
     public String getName() { return "Update Routing Key Modifier"; }
 
     public void runJob() {
-        RoutingKeyGenerator gen = getContext().routingKeyGenerator();
+        RouterKeyGenerator gen = getContext().routerKeyGenerator();
         // make sure we requeue quickly if just before midnight
         long delay = Math.max(5, Math.min(MAX_DELAY_FAILSAFE, gen.getTimeTillMidnight()));
         // TODO tell netdb if mod data changed?
diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
index bc3ad9543a820e44727230493fb3ab0a4364382c..b9e38f9da3bdb876676410f50ed73c464cdab197 100644
--- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
+++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
@@ -17,8 +17,8 @@ import java.util.Locale;
 import java.util.Vector;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
index e232fa7e46f6b3a944d0553fad3c91a88c54371d..459c846806a0f188bfb4649f0bd40127fc0aeaba 100644
--- a/router/java/src/net/i2p/router/transport/Transport.java
+++ b/router/java/src/net/i2p/router/transport/Transport.java
@@ -14,8 +14,8 @@ import java.util.List;
 import java.util.Vector;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.OutNetMessage;
 
 /**
diff --git a/router/java/src/net/i2p/router/transport/TransportEventListener.java b/router/java/src/net/i2p/router/transport/TransportEventListener.java
index 9a6d80d1ca63945617f9dcbe61927b155e14c035..b8437efd1f72d46031032d36a9c0c761909f0e1b 100644
--- a/router/java/src/net/i2p/router/transport/TransportEventListener.java
+++ b/router/java/src/net/i2p/router/transport/TransportEventListener.java
@@ -9,7 +9,7 @@ package net.i2p.router.transport;
  */
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.I2NPMessage;
 
 public interface TransportEventListener {
diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index 194ae3016d6df0f2227607f39329bba52b752cf2..6dd93b4c59244fa39ab44e403978c174bd980f33 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -30,9 +30,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.Job;
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index d9e0c37985a669087d370e769efdd06c3df3f90b..75e843707cbffa222bb096f379cdad206e19d636 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -22,8 +22,8 @@ import java.util.Vector;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.OutNetMessage;
@@ -59,6 +59,7 @@ public class TransportManager implements TransportEventListener {
         _context = context;
         _log = _context.logManager().getLog(TransportManager.class);
         _context.statManager().createRateStat("transport.banlistOnUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("transport.banlistOnUsupportedSigType", "Add a peer to the banlist since signature type is unsupported", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.noBidsYetNotAllUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailBanlisted", "Could not attempt to bid on message, as they were banlisted", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailSelf", "Could not attempt to bid on message, as it targeted ourselves", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
@@ -499,8 +500,11 @@ public class TransportManager implements TransportEventListener {
             }
         }
         if (unreachableTransports >= _transports.size()) {
-            // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
-            if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
+            if (msg.getTarget().getIdentity().getSigningPublicKey().getType() == null) {
+                _context.statManager().addRateData("transport.banlistOnUnsupportedSigType", 1);
+                _context.banlist().banlistRouterForever(peer, _x("Unsupported signature type"));
+            } else if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
+                // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
                 _context.statManager().addRateData("transport.banlistOnUnreachable", msg.getLifetime(), msg.getLifetime());
                 _context.banlist().banlistRouter(peer, _x("Unreachable on any transport"));
             }
diff --git a/router/java/src/net/i2p/router/transport/TransportUtil.java b/router/java/src/net/i2p/router/transport/TransportUtil.java
index 1682abd91ce0c63ce472c142c626f41503fba6c7..648119f91db09837fdc83fb8e0bfa4c12cc43331 100644
--- a/router/java/src/net/i2p/router/transport/TransportUtil.java
+++ b/router/java/src/net/i2p/router/transport/TransportUtil.java
@@ -13,7 +13,7 @@ import java.net.UnknownHostException;
 import java.util.HashMap;
 import java.util.Map;
 
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.router.RouterContext;
 
 /**
diff --git a/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java b/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
index 0d49a656aa01af69a95c9b27804e836461008e3a..061bbc09e878270a5b16b0fc0ede5424c6d46c10 100644
--- a/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
+++ b/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
@@ -124,14 +124,6 @@ public class DHSessionKeyBuilder {
         if (read != 256) {
             return null;
         }
-        if (1 == (Y[0] & 0x80)) {
-            // high bit set, need to inject an additional byte to keep 2s complement
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("High bit set");
-            byte Y2[] = new byte[257];
-            System.arraycopy(Y, 0, Y2, 1, 256);
-            Y = Y2;
-        }
         return new NativeBigInteger(1, Y);
     }
 ****/
@@ -217,17 +209,7 @@ public class DHSessionKeyBuilder {
     public void setPeerPublicValue(byte val[]) throws InvalidPublicParameterException {
         if (val.length != 256)
             throw new IllegalArgumentException("Peer public value must be exactly 256 bytes");
-
-        if (1 == (val[0] & 0x80)) {
-            // high bit set, need to inject an additional byte to keep 2s complement
-            //if (_log.shouldLog(Log.DEBUG))
-            //    _log.debug("High bit set");
-            byte val2[] = new byte[257];
-            System.arraycopy(val, 0, val2, 1, 256);
-            val = val2;
-        }
         setPeerPublicValue(new NativeBigInteger(1, val));
-        //_peerValue = new NativeBigInteger(val);
     }
 
     public synchronized BigInteger getPeerPublicValue() {
@@ -283,7 +265,7 @@ public class DHSessionKeyBuilder {
      * Side effect - sets extraExchangedBytes to the next 32 bytes.
      */
     private final SessionKey calculateSessionKey(BigInteger myPrivateValue, BigInteger publicPeerValue) {
-        //long start = System.currentTimeMillis();
+        long start = System.currentTimeMillis();
         SessionKey key = new SessionKey();
         BigInteger exchangedKey = publicPeerValue.modPow(myPrivateValue, CryptoConstants.elgp);
         // surprise! leading zero byte half the time!
@@ -312,10 +294,10 @@ public class DHSessionKeyBuilder {
             //    _log.debug("Storing " + remaining.length + " bytes from the end of the DH exchange");
         }
         key.setData(val);
-        //long end = System.currentTimeMillis();
-        //long diff = end - start;
+        long end = System.currentTimeMillis();
+        long diff = end - start;
         
-        //_context.statManager().addRateData("crypto.dhCalculateSessionTime", diff, diff);
+        I2PAppContext.getGlobalContext().statManager().addRateData("crypto.dhCalculateSessionTime", diff);
         //if (diff > 1000) {
         //    if (_log.shouldLog(Log.WARN)) _log.warn("Generating session key took too long (" + diff + " ms");
         //} else {
@@ -436,6 +418,15 @@ public class DHSessionKeyBuilder {
          * or pulls a prebuilt one from the queue.
          */
         public DHSessionKeyBuilder getBuilder();
+
+        /**
+         * Return an unused DH key builder
+         * to be put back onto the queue for reuse.
+         *
+         * @param builder must not have a peerPublicValue set
+         * @since 0.9.16
+         */
+        public void returnUnused(DHSessionKeyBuilder builder);
     }
 
     public static class PrecalcRunner extends I2PThread implements Factory {
@@ -455,8 +446,9 @@ public class DHSessionKeyBuilder {
             _context = ctx;
             _log = ctx.logManager().getLog(DHSessionKeyBuilder.class);
             ctx.statManager().createRateStat("crypto.dhGeneratePublicTime", "How long it takes to create x and X", "Encryption", new long[] { 60*60*1000 });
-            //ctx.statManager().createRateStat("crypto.dhCalculateSessionTime", "How long it takes to create the session key", "Encryption", new long[] { 60*60*1000 });        
+            ctx.statManager().createRateStat("crypto.dhCalculateSessionTime", "How long it takes to create the session key", "Encryption", new long[] { 60*60*1000 });        
             ctx.statManager().createRateStat("crypto.DHUsed", "Need a DH from the queue", "Encryption", new long[] { 60*60*1000 });
+            ctx.statManager().createRateStat("crypto.DHReused", "Unused DH requeued", "Encryption", new long[] { 60*60*1000 });
             ctx.statManager().createRateStat("crypto.DHEmpty", "DH queue empty", "Encryption", new long[] { 60*60*1000 });
 
             // add to the defaults for every 128MB of RAM, up to 512MB
@@ -536,11 +528,11 @@ public class DHSessionKeyBuilder {
          * @since 0.9 moved from DHSKB
          */
         public DHSessionKeyBuilder getBuilder() {
-            _context.statManager().addRateData("crypto.DHUsed", 1, 0);
+            _context.statManager().addRateData("crypto.DHUsed", 1);
             DHSessionKeyBuilder builder = _builders.poll();
             if (builder == null) {
                 if (_log.shouldLog(Log.INFO)) _log.info("No more builders, creating one now");
-                _context.statManager().addRateData("crypto.DHEmpty", 1, 0);
+                _context.statManager().addRateData("crypto.DHEmpty", 1);
                 builder = precalc();
             }
             return builder;
@@ -551,7 +543,7 @@ public class DHSessionKeyBuilder {
             DHSessionKeyBuilder builder = new DHSessionKeyBuilder(_context);
             long end = System.currentTimeMillis();
             long diff = end - start;
-            _context.statManager().addRateData("crypto.dhGeneratePublicTime", diff, diff);
+            _context.statManager().addRateData("crypto.dhGeneratePublicTime", diff);
             if (diff > 1000) {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Took more than a second (" + diff + "ms) to generate local DH value");
@@ -561,6 +553,23 @@ public class DHSessionKeyBuilder {
             return builder;
         }
 
+        /**
+         * Return an unused DH key builder
+         * to be put back onto the queue for reuse.
+         *
+         * @param builder must not have a peerPublicValue set
+         * @since 0.9.16
+         */
+        public void returnUnused(DHSessionKeyBuilder builder) {
+            if (builder.getPeerPublicValue() != null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("builder returned used");
+                return;
+            }
+            _context.statManager().addRateData("crypto.DHReused", 1);
+            _builders.offer(builder);
+        }
+
         /** @return true if successful, false if full */
         private final boolean addBuilder(DHSessionKeyBuilder builder) {
             return _builders.offer(builder);
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
index bc6e37bc4d05289d2dacc1144b9e95fc75a46eea..4697e15f6d83a7a4ac9074c191c8591bd6f33caf 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
@@ -1,5 +1,6 @@
 package net.i2p.router.transport.ntcp;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -7,11 +8,12 @@ import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 
 import net.i2p.I2PAppContext;
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.Signature;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
@@ -62,6 +64,7 @@ import net.i2p.util.SimpleByteCache;
 class EstablishState {
     
     public static final VerifiedEstablishState VERIFIED = new VerifiedEstablishState();
+    public static final FailedEstablishState FAILED = new FailedEstablishState();
     
     private final RouterContext _context;
     private final Log _log;
@@ -70,13 +73,14 @@ class EstablishState {
     private final byte _X[];
     private final byte _hX_xor_bobIdentHash[];
     private int _aliceIdentSize;
+    private RouterIdentity _aliceIdent;
     /** contains the decrypted aliceIndexSize + aliceIdent + tsA + padding + aliceSig */
     private ByteArrayOutputStream _sz_aliceIdent_tsA_padding_aliceSig;
     /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
     private int _sz_aliceIdent_tsA_padding_aliceSigSize;
     // alice receives (and bob sends)
-    private byte _Y[];
-    private transient byte _e_hXY_tsB[];
+    private final byte _Y[];
+    private final byte _e_hXY_tsB[];
     /** Bob's Timestamp in seconds */
     private transient long _tsB;
     /** Alice's Timestamp in seconds */
@@ -85,7 +89,7 @@ class EstablishState {
 
     /** previously received encrypted block (or the IV) */
     private byte _prevEncrypted[];
-    /** current encrypted block we are reading */
+    /** current encrypted block we are reading (IB only) or an IV buf used at the end for OB */
     private byte _curEncrypted[];
     /**
      * next index in _curEncrypted to write to (equals _curEncrypted length if the block is
@@ -103,24 +107,63 @@ class EstablishState {
 
     private final NTCPTransport _transport;
     private final NTCPConnection _con;
-    private boolean _corrupt;
     /** error causing the corruption */
     private String _err;
     /** exception causing the error */
     private Exception _e;
-    private boolean _verified;
-    private boolean _confirmWritten;
     private boolean _failedBySkew;
     
+    private static final int MIN_RI_SIZE = 387;
+    private static final int MAX_RI_SIZE = 2048;
+
+    private static final int AES_SIZE = 16;
+    private static final int XY_SIZE = 256;
+    private static final int HXY_SIZE = 32;  //Hash.HASH_LENGTH;
+    private static final int HXY_TSB_PAD_SIZE = HXY_SIZE + 4 + 12;  // 48
+
+    protected State _state;
+
+    private enum State {
+        OB_INIT,
+        /** sent 1 */
+        OB_SENT_X,
+        /** sent 1, got 2 partial */
+        OB_GOT_Y,
+        /** sent 1, got 2 */
+        OB_GOT_HXY,
+        /** sent 1, got 2, sent 3 */
+        OB_SENT_RI,
+        /** sent 1, got 2, sent 3, got 4 */
+        OB_GOT_SIG,
+
+        IB_INIT,
+        /** got 1 partial */
+        IB_GOT_X,
+        /** got 1 */
+        IB_GOT_HX,
+        /** got 1, sent 2 */
+        IB_SENT_Y,
+        /** got 1, sent 2, got partial 3 */
+        IB_GOT_RI_SIZE,
+        /** got 1, sent 2, got 3 */
+        IB_GOT_RI,
+
+        /** OB: got and verified 4; IB: got and verified 3 and sent 4 */
+        VERIFIED,
+        CORRUPT
+    }
+
     private EstablishState() {
         _context = null;
         _log = null;
         _X = null;
+        _Y = null;
         _hX_xor_bobIdentHash = null;
         _curDecrypted = null;
         _dh = null;
         _transport = null;
         _con = null;
+        _e_hXY_tsB = null;
     }
 
     public EstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
@@ -129,20 +172,25 @@ class EstablishState {
         _transport = transport;
         _con = con;
         _dh = _transport.getDHBuilder();
-        _hX_xor_bobIdentHash = new byte[Hash.HASH_LENGTH];
+        _hX_xor_bobIdentHash = SimpleByteCache.acquire(HXY_SIZE);
         if (_con.isInbound()) {
-            _X = new byte[256];
+            _X = SimpleByteCache.acquire(XY_SIZE);
+            _Y = _dh.getMyPublicValueBytes();
             _sz_aliceIdent_tsA_padding_aliceSig = new ByteArrayOutputStream(512);
+            _prevEncrypted = SimpleByteCache.acquire(AES_SIZE);
+            _state = State.IB_INIT;
         } else {
             _X = _dh.getMyPublicValueBytes();
-            _Y = new byte[256];
-            ctx.sha().calculateHash(_X, 0, _X.length, _hX_xor_bobIdentHash, 0);
+            _Y = SimpleByteCache.acquire(XY_SIZE);
+            ctx.sha().calculateHash(_X, 0, XY_SIZE, _hX_xor_bobIdentHash, 0);
             xor32(con.getRemotePeer().calculateHash().getData(), _hX_xor_bobIdentHash);
+            // _prevEncrypted will be created later
+            _state = State.OB_INIT;
         }
 
-        _prevEncrypted = new byte[16];
-        _curEncrypted = new byte[16];
-        _curDecrypted = new byte[16];
+        _e_hXY_tsB = new byte[HXY_TSB_PAD_SIZE];
+        _curEncrypted = SimpleByteCache.acquire(AES_SIZE);
+        _curDecrypted = SimpleByteCache.acquire(AES_SIZE);
     }
 
     /**
@@ -154,14 +202,14 @@ class EstablishState {
      * All data must be copied out of the buffer as Reader.processRead()
      * will return it to the pool.
      */
-    public void receive(ByteBuffer src) {
-        if (_corrupt || _verified)
-            throw new IllegalStateException(prefix() + "received after completion [corrupt?" + _corrupt + " verified? " + _verified + "] on " + _con);
+    public synchronized void receive(ByteBuffer src) {
+        if (_state == State.VERIFIED || _state == State.CORRUPT)
+            throw new IllegalStateException(prefix() + "received unexpected data on " + _con);
         if (!src.hasRemaining())
             return; // nothing to receive
 
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix()+"receive " + src);
+            _log.debug(prefix() + "Receiving: " + src.remaining() + " Received: " + _received);
         if (_con.isInbound())
             receiveInbound(src);
         else
@@ -169,12 +217,9 @@ class EstablishState {
     }
 
     /**
-     * we have written all of the data required to confirm the connection
-     * establishment
+     *  Was this connection failed because of clock skew?
      */
-    public boolean confirmWritten() { return _confirmWritten; }
-
-    public boolean getFailedBySkew() { return _failedBySkew; }
+    public synchronized boolean getFailedBySkew() { return _failedBySkew; }
 
     /**
      *  we are Bob, so receive these bytes as part of an inbound connection
@@ -182,11 +227,11 @@ class EstablishState {
      *
      *  All data must be copied out of the buffer as Reader.processRead()
      *  will return it to the pool.
+     *
+     *  Caller must synch.
      */
     private void receiveInbound(ByteBuffer src) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug(prefix()+"Receiving inbound: prev received=" + _received + " src.remaining=" + src.remaining());
-        while (_received < _X.length && src.hasRemaining()) {
+        while (_state == State.IB_INIT && src.hasRemaining()) {
             byte c = src.get();
             _X[_received++] = c;
             //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv x" + (int)c + " received=" + _received);
@@ -197,24 +242,28 @@ class EstablishState {
             //        return;
             //    }
             //}
+            if (_received >= XY_SIZE)
+                _state = State.IB_GOT_X;
         }
-        while (_received < _X.length + _hX_xor_bobIdentHash.length && src.hasRemaining()) {
-            int i = _received-_X.length;
+        while (_state == State.IB_GOT_X && src.hasRemaining()) {
+            int i = _received - XY_SIZE;
             _received++;
             byte c = src.get();
             _hX_xor_bobIdentHash[i] = c;
             //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv bih" + (int)c + " received=" + _received);
+            if (i >= HXY_SIZE - 1)
+                _state = State.IB_GOT_HX;
         }
 
-        if (_received >= _X.length + _hX_xor_bobIdentHash.length) {
-            if (_dh.getSessionKey() == null) {
+        if (_state == State.IB_GOT_HX) {
+
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix()+"Enough data for a DH received");
 
                 // first verify that Alice knows who she is trying to talk with and that the X
                 // isn't corrupt
-                byte[] realXor = SimpleByteCache.acquire(Hash.HASH_LENGTH);
-                _context.sha().calculateHash(_X, 0, _X.length, realXor, 0);
+                byte[] realXor = SimpleByteCache.acquire(HXY_SIZE);
+                _context.sha().calculateHash(_X, 0, XY_SIZE, realXor, 0);
                 xor32(_context.routerHash().getData(), realXor);
                 //if (_log.shouldLog(Log.DEBUG)) {
                     //_log.debug(prefix()+"_X = " + Base64.encode(_X));
@@ -240,43 +289,43 @@ class EstablishState {
                     // ok, they're actually trying to talk to us, and we got their (unauthenticated) X
                     _dh.setPeerPublicValue(_X);
                     _dh.getSessionKey(); // force the calc
-                    System.arraycopy(realXor, 16, _prevEncrypted, 0, _prevEncrypted.length);
+                    System.arraycopy(_hX_xor_bobIdentHash, AES_SIZE, _prevEncrypted, 0, AES_SIZE);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
 
                     // now prepare our response: Y+E(H(X+Y)+tsB+padding, sk, Y[239:255])
-                    _Y = _dh.getMyPublicValueBytes();
-                    byte xy[] = new byte[_X.length+_Y.length];
-                    System.arraycopy(_X, 0, xy, 0, _X.length);
-                    System.arraycopy(_Y, 0, xy, _X.length, _Y.length);
-                    byte[] hxy = SimpleByteCache.acquire(Hash.HASH_LENGTH);
-                    _context.sha().calculateHash(xy, 0, xy.length, hxy, 0);
+                    byte xy[] = new byte[XY_SIZE + XY_SIZE];
+                    System.arraycopy(_X, 0, xy, 0, XY_SIZE);
+                    System.arraycopy(_Y, 0, xy, XY_SIZE, XY_SIZE);
+                    byte[] hxy = SimpleByteCache.acquire(HXY_SIZE);
+                    _context.sha().calculateHash(xy, 0, XY_SIZE + XY_SIZE, hxy, 0);
                     _tsB = (_context.clock().now() + 500) / 1000l; // our (Bob's) timestamp in seconds
-                    byte toEncrypt[] = new byte[hxy.length + (4 + 12)];  // 48
-                    System.arraycopy(hxy, 0, toEncrypt, 0, hxy.length);
+                    byte toEncrypt[] = new byte[HXY_TSB_PAD_SIZE];  // 48
+                    System.arraycopy(hxy, 0, toEncrypt, 0, HXY_SIZE);
                     byte tsB[] = DataHelper.toLong(4, _tsB);
-                    System.arraycopy(tsB, 0, toEncrypt, hxy.length, tsB.length);
+                    System.arraycopy(tsB, 0, toEncrypt, HXY_SIZE, tsB.length);
                     //DataHelper.toLong(toEncrypt, hxy.getData().length, 4, _tsB);
-                    _context.random().nextBytes(toEncrypt, hxy.length + 4, 12);
+                    _context.random().nextBytes(toEncrypt, HXY_SIZE + 4, 12);
                     if (_log.shouldLog(Log.DEBUG)) {
                         //_log.debug(prefix()+"Y="+Base64.encode(_Y));
                         //_log.debug(prefix()+"x+y="+Base64.encode(xy));
                         _log.debug(prefix()+"h(x+y)="+Base64.encode(hxy));
-                        _log.debug(prefix()+"tsb="+Base64.encode(tsB));
+                        _log.debug(prefix() + "tsb = " + _tsB);
                         _log.debug(prefix()+"unencrypted H(X+Y)+tsB+padding: " + Base64.encode(toEncrypt));
-                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, _Y.length-16, 16));
+                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, XY_SIZE-AES_SIZE, AES_SIZE));
                         _log.debug(prefix()+"encryption key= " + _dh.getSessionKey().toBase64());
                     }
                     SimpleByteCache.release(hxy);
-                    _e_hXY_tsB = new byte[toEncrypt.length];
-                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(), _Y, _Y.length-16, toEncrypt.length);
+                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(),
+                                           _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"encrypted H(X+Y)+tsB+padding: " + Base64.encode(_e_hXY_tsB));
-                    byte write[] = new byte[_Y.length + _e_hXY_tsB.length];
-                    System.arraycopy(_Y, 0, write, 0, _Y.length);
-                    System.arraycopy(_e_hXY_tsB, 0, write, _Y.length, _e_hXY_tsB.length);
+                    byte write[] = new byte[XY_SIZE + HXY_TSB_PAD_SIZE];
+                    System.arraycopy(_Y, 0, write, 0, XY_SIZE);
+                    System.arraycopy(_e_hXY_tsB, 0, write, XY_SIZE, HXY_TSB_PAD_SIZE);
 
                     // ok, now that is prepared, we want to actually send it, so make sure we are up for writing
+                    _state = State.IB_SENT_Y;
                     _transport.getPumper().wantsWrite(_con, write);
                     if (!src.hasRemaining()) return;
                 } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
@@ -284,73 +333,116 @@ class EstablishState {
                     fail("Invalid X", e);
                     return;
                 }
-            }
 
-            // ok, we are onto the encrypted area
-            while (src.hasRemaining() && !_corrupt) {
+        }
+
+        // ok, we are onto the encrypted area, i.e. Message #3
+        while ((_state == State.IB_SENT_Y ||
+                _state == State.IB_GOT_RI_SIZE ||
+                _state == State.IB_GOT_RI) && src.hasRemaining()) {
+
                 //if (_log.shouldLog(Log.DEBUG))
                 //    _log.debug(prefix()+"Encrypted bytes available (" + src.hasRemaining() + ")");
-                while (_curEncryptedOffset < _curEncrypted.length && src.hasRemaining()) {
+                // Collect a 16-byte block
+                while (_curEncryptedOffset < AES_SIZE && src.hasRemaining()) {
                     _curEncrypted[_curEncryptedOffset++] = src.get();
                     _received++;
                 }
-                if (_curEncryptedOffset >= _curEncrypted.length) {
-                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(), _prevEncrypted, 0, _curEncrypted.length);
+                // Decrypt the 16-byte block
+                if (_curEncryptedOffset >= AES_SIZE) {
+                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(),
+                                           _prevEncrypted, 0, AES_SIZE);
                     //if (_log.shouldLog(Log.DEBUG))
-                    //    _log.debug(prefix()+"full block read and decrypted: " + Base64.encode(_curDecrypted));
+                    //    _log.debug(prefix() + "full block read and decrypted: ");
 
-                    byte swap[] = new byte[16];
+                    byte swap[] = _prevEncrypted;
                     _prevEncrypted = _curEncrypted;
                     _curEncrypted = swap;
                     _curEncryptedOffset = 0;
 
-                    if (_aliceIdentSize <= 0) { // we are on the first decrypted block
-                        _aliceIdentSize = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
-                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + Signature.SIGNATURE_BYTES;
-                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % 16);
+                    if (_state == State.IB_SENT_Y) { // we are on the first decrypted block
+                        int sz = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
+                        if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE) {
+                            _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
+                            fail("size is invalid", new Exception("size is " + sz));
+                            return;
+                        }
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "got the RI size: " + sz);
+                        _aliceIdentSize  = sz;
+                        _state = State.IB_GOT_RI_SIZE;
+
+                        // We must defer the calculations for total size of the message until
+                        //  we get the full alice ident so
+                        // we can determine how long the signature is.
+                        // See below
+
+                    }
+                    try {
+                        _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
+                    } catch (IOException ioe) {
+                        if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
+                    }
+                    //if (_log.shouldLog(Log.DEBUG))
+                    //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
+
+                    if (_state == State.IB_GOT_RI_SIZE &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= 2 + _aliceIdentSize) {
+                        // we have enough to get Alice's RI and determine the sig+padding length
+                        readAliceRouterIdentity();
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug(prefix() + "got the RI");
+                        if (_aliceIdent == null) {
+                            // readAliceRouterIdentity already called fail
+                            return;
+                        }
+                        SigType type = _aliceIdent.getSigningPublicKey().getType();
+                        if (type == null) {
+                            fail("Unsupported sig type");
+                            return;
+                        }
+                        _state = State.IB_GOT_RI;
+                        // handle variable signature size
+                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + type.getSigLen();
+                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % AES_SIZE);
                         int padding = 0;
                         if (rem > 0)
-                            padding = 16-rem;
+                            padding = AES_SIZE-rem;
                         _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
-                        try {
-                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
-                        } catch (IOException ioe) {
-                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
-                        }
                         if (_log.shouldLog(Log.DEBUG))
-                            _log.debug(prefix()+"alice ident size decrypted as " + _aliceIdentSize + ", making the padding at " + padding + " and total size at " + _sz_aliceIdent_tsA_padding_aliceSigSize);
-                    } else {
-                        // subsequent block...
-                        try {
-                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
-                        } catch (IOException ioe) {
-                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
-                        }
-                        //if (_log.shouldLog(Log.DEBUG))
-                        //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
+                            _log.debug(prefix() + "alice ident size decrypted as " + _aliceIdentSize +
+                                       ", making the padding at " + padding + " and total size at " +
+                                       _sz_aliceIdent_tsA_padding_aliceSigSize);
+                    }
 
-                        if (_sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
+                    if (_state == State.IB_GOT_RI &&
+                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
+                        // we have the remainder of Message #3, i.e. the padding+signature
+                        // Time to verify.
+
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug(prefix() + "got the sig");
                             verifyInbound();
-                            if (!_corrupt && _verified && src.hasRemaining())
+                            if (_state == State.VERIFIED && src.hasRemaining())
                                 prepareExtra(src);
                             if (_log.shouldLog(Log.DEBUG))
                                 _log.debug(prefix()+"verifying size (sz=" + _sz_aliceIdent_tsA_padding_aliceSig.size()
                                            + " expected=" + _sz_aliceIdent_tsA_padding_aliceSigSize
-                                           + " corrupt=" + _corrupt
-                                           + " verified=" + _verified + " extra=" + (_extra != null ? _extra.length : 0) + ")");
+                                           + ' ' + _state
+                                           + " extra=" + (_extra != null ? _extra.length : 0) + ")");
                             return;
-                        }
                     }
                 } else {
                     // no more bytes available in the buffer, and only a partial
                     // block was read, so we can't decrypt it.
                     if (_log.shouldLog(Log.DEBUG))
-                        _log.debug(prefix()+"end of available data with only a partial block read (" + _curEncryptedOffset + ", " + _received + ")");
+                        _log.debug(prefix() + "end of available data with only a partial block read (" +
+                                   _curEncryptedOffset + ", " + _received + ")");
                 }
-            }
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug(prefix()+"done with the data, not yet complete or corrupt");
         }
+
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug(prefix()+"done with the data, not yet complete or corrupt");
     }
 
     /**
@@ -359,22 +451,22 @@ class EstablishState {
      *
      *  All data must be copied out of the buffer as Reader.processRead()
      *  will return it to the pool.
+     *
+     *  Caller must synch.
      */
     private void receiveOutbound(ByteBuffer src) {
-        if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"Receive outbound " + src + " received=" + _received);
-
         // recv Y+E(H(X+Y)+tsB, sk, Y[239:255])
-        while (_received < _Y.length && src.hasRemaining()) {
+        while (_state == State.OB_SENT_X && src.hasRemaining()) {
             byte c = src.get();
             _Y[_received++] = c;
             //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv x" + (int)c + " received=" + _received);
-            if (_received >= _Y.length) {
+            if (_received >= XY_SIZE) {
                 try {
                     _dh.setPeerPublicValue(_Y);
                     _dh.getSessionKey(); // force the calc
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
-                    _e_hXY_tsB = new byte[Hash.HASH_LENGTH+4+12];
+                    _state = State.OB_GOT_Y;
                 } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
                     _context.statManager().addRateData("ntcp.invalidDH", 1);
                     fail("Invalid X", e);
@@ -382,34 +474,34 @@ class EstablishState {
                 }
             }
         }
-        if (_e_hXY_tsB == null) return; // !src.hasRemaining
 
-        while (_received < _Y.length + _e_hXY_tsB.length && src.hasRemaining()) {
-            int i = _received-_Y.length;
+        while (_state == State.OB_GOT_Y && src.hasRemaining()) {
+            int i = _received-XY_SIZE;
             _received++;
             byte c = src.get();
             _e_hXY_tsB[i] = c;
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug(prefix() + "recv _e_hXY_tsB " + (int)c + " received=" + _received);
-            if (i+1 >= _e_hXY_tsB.length) {
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug(prefix() + "recv _e_hXY_tsB " + (int)c + " received=" + _received);
+            if (i+1 >= HXY_TSB_PAD_SIZE) {
                 if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix() + "received _e_hXY_tsB fully");
-                byte hXY_tsB[] = new byte[_e_hXY_tsB.length];
-                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, _Y.length-16, _e_hXY_tsB.length);
-                byte XY[] = new byte[_X.length + _Y.length];
-                System.arraycopy(_X, 0, XY, 0, _X.length);
-                System.arraycopy(_Y, 0, XY, _X.length, _Y.length);
-                byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH);
-                _context.sha().calculateHash(XY, 0, XY.length, h, 0);
+                byte hXY_tsB[] = new byte[HXY_TSB_PAD_SIZE];
+                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
+                byte XY[] = new byte[XY_SIZE + XY_SIZE];
+                System.arraycopy(_X, 0, XY, 0, XY_SIZE);
+                System.arraycopy(_Y, 0, XY, XY_SIZE, XY_SIZE);
+                byte[] h = SimpleByteCache.acquire(HXY_SIZE);
+                _context.sha().calculateHash(XY, 0, XY_SIZE + XY_SIZE, h, 0);
                 //if (_log.shouldLog(Log.DEBUG))
                 //    _log.debug(prefix() + "h(XY)=" + h.toBase64());
-                if (!DataHelper.eq(h, 0, hXY_tsB, 0, Hash.HASH_LENGTH)) {
+                if (!DataHelper.eq(h, 0, hXY_tsB, 0, HXY_SIZE)) {
                     SimpleByteCache.release(h);
                     _context.statManager().addRateData("ntcp.invalidHXY", 1);
                     fail("Invalid H(X+Y) - mitm attack attempted?");
                     return;
                 }
                 SimpleByteCache.release(h);
-                _tsB = DataHelper.fromLong(hXY_tsB, Hash.HASH_LENGTH, 4); // their (Bob's) timestamp in seconds
+                _state = State.OB_GOT_HXY;
+                _tsB = DataHelper.fromLong(hXY_tsB, HXY_SIZE, 4); // their (Bob's) timestamp in seconds
                 _tsA = (_context.clock().now() + 500) / 1000; // our (Alice's) timestamp in seconds
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix()+"h(X+Y) is correct, tsA-tsB=" + (_tsA-_tsB));
@@ -439,13 +531,13 @@ class EstablishState {
 
                 // now prepare and send our response
                 // send E(#+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])
-                int sigSize = _X.length+_Y.length+Hash.HASH_LENGTH+4+4;//+12;
+                int sigSize = XY_SIZE + XY_SIZE + HXY_SIZE + 4+4;//+12;
                 byte preSign[] = new byte[sigSize];
-                System.arraycopy(_X, 0, preSign, 0, _X.length);
-                System.arraycopy(_Y, 0, preSign, _X.length, _Y.length);
-                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, _X.length+_Y.length, Hash.HASH_LENGTH);
-                DataHelper.toLong(preSign, _X.length+_Y.length+Hash.HASH_LENGTH, 4, _tsA);
-                DataHelper.toLong(preSign, _X.length+_Y.length+Hash.HASH_LENGTH+4, 4, _tsB);
+                System.arraycopy(_X, 0, preSign, 0, XY_SIZE);
+                System.arraycopy(_Y, 0, preSign, XY_SIZE, XY_SIZE);
+                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, XY_SIZE + XY_SIZE, HXY_SIZE);
+                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE, 4, _tsA);
+                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE + 4, 4, _tsB);
                 // hXY_tsB has 12 bytes of padding (size=48, tsB=4 + hXY=32)
                 //System.arraycopy(hXY_tsB, hXY_tsB.length-12, preSign, _X.length+_Y.length+Hash.HASH_LENGTH+4+4, 12);
                 //byte sigPad[] = new byte[padSig];
@@ -458,80 +550,99 @@ class EstablishState {
                 //}
 
                 byte ident[] = _context.router().getRouterInfo().getIdentity().toByteArray();
-                int min = 2+ident.length+4+Signature.SIGNATURE_BYTES;
-                int rem = min % 16;
+                // handle variable signature size
+                int min = 2 + ident.length + 4 + sig.length();
+                int rem = min % AES_SIZE;
                 int padding = 0;
                 if (rem > 0)
-                    padding = 16 - rem;
+                    padding = AES_SIZE - rem;
                 byte preEncrypt[] = new byte[min+padding];
                 DataHelper.toLong(preEncrypt, 0, 2, ident.length);
                 System.arraycopy(ident, 0, preEncrypt, 2, ident.length);
                 DataHelper.toLong(preEncrypt, 2+ident.length, 4, _tsA);
                 if (padding > 0)
                     _context.random().nextBytes(preEncrypt, 2 + ident.length + 4, padding);
-                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, Signature.SIGNATURE_BYTES);
+                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, sig.length());
 
                 _prevEncrypted = new byte[preEncrypt.length];
-                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(), _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-16, preEncrypt.length);
+                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(),
+                                       _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-AES_SIZE, preEncrypt.length);
 
                 //if (_log.shouldLog(Log.DEBUG)) {
                     //_log.debug(prefix() + "unencrypted response to Bob: " + Base64.encode(preEncrypt));
                     //_log.debug(prefix() + "encrypted response to Bob: " + Base64.encode(_prevEncrypted));
                 //}
                 // send 'er off (when the bw limiter says, etc)
+                _state = State.OB_SENT_RI;
                 _transport.getPumper().wantsWrite(_con, _prevEncrypted);
             }
         }
-        if (_received >= _Y.length + _e_hXY_tsB.length && src.hasRemaining()) {
+        if (_state == State.OB_SENT_RI && src.hasRemaining()) {
             // we are receiving their confirmation
 
             // recv E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
             int off = 0;
             if (_e_bobSig == null) {
-                _e_bobSig = new byte[48];
+                // handle variable signature size
+                int siglen = _con.getRemotePeer().getSigningPublicKey().getType().getSigLen();
+                int rem = siglen % AES_SIZE;
+                int padding;
+                if (rem > 0)
+                    padding = AES_SIZE - rem;
+                else
+                    padding = 0;
+                _e_bobSig = new byte[siglen + padding];
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + ")");
+                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + ")");
             } else {
-                off = _received - _Y.length - _e_hXY_tsB.length;
+                off = _received - XY_SIZE - HXY_TSB_PAD_SIZE;
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
+                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
+                               src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
             }
-            while (src.hasRemaining() && off < _e_bobSig.length) {
-                if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"recv bobSig received=" + _received);
+            while (_state == State.OB_SENT_RI && src.hasRemaining()) {
+                //if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"recv bobSig received=" + _received);
                 _e_bobSig[off++] = src.get();
                 _received++;
 
                 if (off >= _e_bobSig.length) {
+                    _state = State.OB_GOT_SIG;
                     //if (_log.shouldLog(Log.DEBUG))
                     //    _log.debug(prefix() + "received E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev): " + Base64.encode(_e_bobSig));
                     byte bobSig[] = new byte[_e_bobSig.length];
-                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
+                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(),
+                                           _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
                     // ignore the padding
-                    byte bobSigData[] = new byte[Signature.SIGNATURE_BYTES];
-                    System.arraycopy(bobSig, 0, bobSigData, 0, Signature.SIGNATURE_BYTES);
-                    Signature sig = new Signature(bobSigData);
-
-                    byte toVerify[] = new byte[_X.length+_Y.length+Hash.HASH_LENGTH+4+4];
+                    // handle variable signature size
+                    SigType type = _con.getRemotePeer().getSigningPublicKey().getType();
+                    int siglen = type.getSigLen();
+                    byte bobSigData[] = new byte[siglen];
+                    System.arraycopy(bobSig, 0, bobSigData, 0, siglen);
+                    Signature sig = new Signature(type, bobSigData);
+
+                    byte toVerify[] = new byte[XY_SIZE + XY_SIZE + HXY_SIZE +4+4];
                     int voff = 0;
-                    System.arraycopy(_X, 0, toVerify, voff, _X.length); voff += _X.length;
-                    System.arraycopy(_Y, 0, toVerify, voff, _Y.length); voff += _Y.length;
-                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, Hash.HASH_LENGTH); voff += Hash.HASH_LENGTH;
+                    System.arraycopy(_X, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
+                    System.arraycopy(_Y, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
+                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, HXY_SIZE); voff += HXY_SIZE;
                     DataHelper.toLong(toVerify, voff, 4, _tsA); voff += 4;
                     DataHelper.toLong(toVerify, voff, 4, _tsB); voff += 4;
 
-                    _verified = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
-                    if (!_verified) {
+                    boolean ok = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
+                    if (!ok) {
                         _context.statManager().addRateData("ntcp.invalidSignature", 1);
                         fail("Signature was invalid - attempt to spoof " + _con.getRemotePeer().calculateHash().toBase64() + "?");
                     } else {
+                        _state = State.VERIFIED;
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug(prefix() + "signature verified from Bob.  done!");
                         prepareExtra(src);
-                        byte nextWriteIV[] = new byte[16];
-                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-16, nextWriteIV, 0, 16);
-                        byte nextReadIV[] = new byte[16];
-                        System.arraycopy(_e_bobSig, _e_bobSig.length-16, nextReadIV, 0, nextReadIV.length);
-                        _con.finishOutboundEstablishment(_dh.getSessionKey(), (_tsA-_tsB), nextWriteIV, nextReadIV); // skew in seconds
+                        byte nextWriteIV[] = _curEncrypted; // reuse buf
+                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-AES_SIZE, nextWriteIV, 0, AES_SIZE);
+                        // this does not copy the nextWriteIV, do not release to cache
+                        _con.finishOutboundEstablishment(_dh.getSessionKey(), (_tsA-_tsB), nextWriteIV, _e_bobSig); // skew in seconds
+                        releaseBufs();
                         // if socket gets closed this will be null - prevent NPE
                         InetAddress ia = _con.getChannel().socket().getInetAddress();
                         if (ia != null)
@@ -544,9 +655,10 @@ class EstablishState {
     }
 
     /** did the handshake fail for some reason? */
-    public boolean isCorrupt() { return _err != null; }
+    public synchronized boolean isCorrupt() { return _state == State.CORRUPT; }
+
     /** @return is the handshake complete and valid? */
-    public boolean isComplete() { return _verified; }
+    public synchronized boolean isComplete() { return _state == State.VERIFIED; }
 
     /**
      * We are Alice.
@@ -554,47 +666,90 @@ class EstablishState {
      * queueing up the write of the first part of the handshake
      * This method sends message #1 to Bob.
      */
-    public void prepareOutbound() {
-        if (_received <= 0) {
+    public synchronized void prepareOutbound() {
+        if (_state == State.OB_INIT) {
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug(prefix() + "write out the first part of our handshake");
-            byte toWrite[] = new byte[_X.length + _hX_xor_bobIdentHash.length];
-            System.arraycopy(_X, 0, toWrite, 0, _X.length);
-            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, _X.length, _hX_xor_bobIdentHash.length);
+                _log.debug(prefix() + "send X");
+            byte toWrite[] = new byte[XY_SIZE + _hX_xor_bobIdentHash.length];
+            System.arraycopy(_X, 0, toWrite, 0, XY_SIZE);
+            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, XY_SIZE, _hX_xor_bobIdentHash.length);
+            _state = State.OB_SENT_X;
             _transport.getPumper().wantsWrite(_con, toWrite);
         } else {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug(prefix()+"prepare outbound with received=" + _received);
+            if (_log.shouldLog(Log.WARN))
+                _log.warn(prefix() + "unexpected prepareOutbound()");
         }
     }
 
     /**
-     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
+     * We are Bob. We have received enough of message #3 from Alice
+     * to get Alice's RouterIdentity.
      *
-     * Make sure the signatures are correct, and if they are, update the
-     * NIOConnection with the session key / peer ident / clock skew / iv.
-     * The NIOConnection itself is responsible for registering with the
-     * transport
+     * _aliceIdentSize must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least 2 + _aliceIdentSize bytes.
+     *
+     * Sets _aliceIdent so that we
+     * may determine the signature and padding sizes.
+     *
+     * After all of message #3 is received including the signature and
+     * padding, verifyIdentity() must be called.
+     *
+     *  State must be IB_GOT_RI_SIZE.
+     *  Caller must synch.
+     *
+     * @since 0.9.16 pulled out of verifyInbound()
      */
-    private void verifyInbound() {
-        if (_corrupt) return;
+    private void readAliceRouterIdentity() {
         byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
         //if (_log.shouldLog(Log.DEBUG))
         //    _log.debug(prefix()+"decrypted sz(etc) data: " + Base64.encode(b));
 
         try {
-            RouterIdentity alice = new RouterIdentity();
-            int sz = (int)DataHelper.fromLong(b, 0, 2); // TO-DO: Hey zzz... Throws an NPE for me... see below, for my "quick fix", need to find out the real reason
-            if ( (sz <= 0) || (sz > b.length-2-4-Signature.SIGNATURE_BYTES) ) {
+            int sz = _aliceIdentSize;
+            if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE ||
+                sz > b.length-2) {
                 _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
                 fail("size is invalid", new Exception("size is " + sz));
                 return;
             }
-            byte aliceData[] = new byte[sz];
-            System.arraycopy(b, 2, aliceData, 0, sz);
-            alice.fromByteArray(aliceData);
-            long tsA = DataHelper.fromLong(b, 2+sz, 4);
+            RouterIdentity alice = new RouterIdentity();
+            ByteArrayInputStream bais = new ByteArrayInputStream(b, 2, sz);
+            alice.readBytes(bais);
+            _aliceIdent = alice;
+        } catch (IOException ioe) {
+            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
+            fail("Error verifying peer", ioe);
+        } catch (DataFormatException dfe) {
+            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
+            fail("Error verifying peer", dfe);
+        }
+    }
 
+
+    /**
+     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
+     *
+     * _aliceIdentSize and _aliceIdent must be set.
+     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
+     *  (2 + _aliceIdentSize + 4 + padding + sig) bytes.
+     *
+     * Sets _aliceIdent so that we
+     *
+     * readAliceRouterIdentity() must have been called previously
+     *
+     * Make sure the signatures are correct, and if they are, update the
+     * NIOConnection with the session key / peer ident / clock skew / iv.
+     * The NIOConnection itself is responsible for registering with the
+     * transport
+     *
+     *  State must be IB_GOT_RI.
+     *  Caller must synch.
+     */
+    private void verifyInbound() {
+        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
+        try {
+            int sz = _aliceIdentSize;
+            long tsA = DataHelper.fromLong(b, 2+sz, 4);
             ByteArrayOutputStream baos = new ByteArrayOutputStream(768);
             baos.write(_X);
             baos.write(_Y);
@@ -604,31 +759,37 @@ class EstablishState {
             //baos.write(b, 2+sz+4, b.length-2-sz-4-Signature.SIGNATURE_BYTES);
 
             byte toVerify[] = baos.toByteArray();
-            if (_log.shouldLog(Log.DEBUG)) {
-                _log.debug(prefix()+"checking " + Base64.encode(toVerify, 0, 16));
-                //_log.debug(prefix()+"check pad " + Base64.encode(b, 2+sz+4, 12));
-            }
+            //if (_log.shouldLog(Log.DEBUG)) {
+            //    _log.debug(prefix()+"checking " + Base64.encode(toVerify, 0, AES_SIZE));
+            //    //_log.debug(prefix()+"check pad " + Base64.encode(b, 2+sz+4, 12));
+            //}
 
-            byte s[] = new byte[Signature.SIGNATURE_BYTES];
+            // handle variable signature size
+            SigType type = _aliceIdent.getSigningPublicKey().getType();
+            if (type == null) {
+                fail("unsupported sig type");
+                return;
+            }
+            byte s[] = new byte[type.getSigLen()];
             System.arraycopy(b, b.length-s.length, s, 0, s.length);
-            Signature sig = new Signature(s);
-            _verified = _context.dsa().verifySignature(sig, toVerify, alice.getSigningPublicKey());
-            if (_verified) {
+            Signature sig = new Signature(type, s);
+            boolean ok = _context.dsa().verifySignature(sig, toVerify, _aliceIdent.getSigningPublicKey());
+            if (ok) {
                 // get inet-addr
                 InetAddress addr = this._con.getChannel().socket().getInetAddress();
                 byte[] ip = (addr == null) ? null : addr.getAddress();
-                if (_context.banlist().isBanlistedForever(alice.calculateHash())) {
+                if (_context.banlist().isBanlistedForever(_aliceIdent.calculateHash())) {
                     if (_log.shouldLog(Log.WARN))
-                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + alice.calculateHash().toBase64());
+                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + _aliceIdent.calculateHash());
                     // So next time we will not accept the con from this IP,
                     // rather than doing the whole handshake
                     if(ip != null)
                        _context.blocklist().add(ip);
-                    fail("Peer is banlisted forever: " + alice.calculateHash().toBase64());
+                    fail("Peer is banlisted forever: " + _aliceIdent.calculateHash());
                     return;
                 }
                 if(ip != null)
-                   _transport.setIP(alice.calculateHash(), ip);
+                   _transport.setIP(_aliceIdent.calculateHash(), ip);
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix() + "verification successful for " + _con);
 
@@ -642,10 +803,10 @@ class EstablishState {
                         _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
                 } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
                     _context.statManager().addRateData("ntcp.invalidInboundSkew", diff);
-                    _transport.markReachable(alice.calculateHash(), true);
+                    _transport.markReachable(_aliceIdent.calculateHash(), true);
                     // Only banlist if we know what time it is
                     _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
-                                                       alice.calculateHash(),
+                                                       _aliceIdent.calculateHash(),
                                                        _x("Excessive clock skew: {0}"));
                     _transport.setLastBadSkew(tsA- _tsB);
                     fail("Clocks too skewed (" + diff + " ms)", null, true);
@@ -654,50 +815,60 @@ class EstablishState {
                     _log.debug(prefix()+"Clock skew: " + diff + " ms");
                 }
 
-                sendInboundConfirm(alice, tsA);
-                _con.setRemotePeer(alice);
+                _state = State.VERIFIED;
+                sendInboundConfirm(_aliceIdent, tsA);
+                _con.setRemotePeer(_aliceIdent);
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix()+"e_bobSig is " + _e_bobSig.length + " bytes long");
-                byte iv[] = new byte[16];
-                System.arraycopy(_e_bobSig, _e_bobSig.length-16, iv, 0, 16);
+                byte iv[] = _curEncrypted;  // reuse buf
+                System.arraycopy(_e_bobSig, _e_bobSig.length-AES_SIZE, iv, 0, AES_SIZE);
+                // this does not copy the IV, do not release to cache
                 _con.finishInboundEstablishment(_dh.getSessionKey(), (tsA-_tsB), iv, _prevEncrypted); // skew in seconds
+                releaseBufs();
                 if (_log.shouldLog(Log.INFO))
-                    _log.info(prefix()+"Verified remote peer as " + alice.calculateHash().toBase64());
+                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
             } else {
                 _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
-                fail("Peer verification failed - spoof of " + alice.calculateHash().toBase64() + "?");
+                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
             }
         } catch (IOException ioe) {
             _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
             fail("Error verifying peer", ioe);
-        } catch (DataFormatException dfe) {
-            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
-            fail("Error verifying peer", dfe);
-        } catch(NullPointerException npe) {
-            fail("Error verifying peer", npe); // TO-DO: zzz This is that quick-fix. -- Sponge
         }
     }
 
     /**
      *  We are Bob. Send message #4 to Alice.
+     *
+     *  State must be VERIFIED.
+     *  Caller must synch.
      */
     private void sendInboundConfirm(RouterIdentity alice, long tsA) {
         // send Alice E(S(X+Y+Alice.identHash+tsA+tsB), sk, prev)
-        byte toSign[] = new byte[256+256+32+4+4];
+        byte toSign[] = new byte[XY_SIZE + XY_SIZE + 32+4+4];
         int off = 0;
-        System.arraycopy(_X, 0, toSign, off, 256); off += 256;
-        System.arraycopy(_Y, 0, toSign, off, 256); off += 256;
+        System.arraycopy(_X, 0, toSign, off, XY_SIZE); off += XY_SIZE;
+        System.arraycopy(_Y, 0, toSign, off, XY_SIZE); off += XY_SIZE;
         Hash h = alice.calculateHash();
         System.arraycopy(h.getData(), 0, toSign, off, 32); off += 32;
         DataHelper.toLong(toSign, off, 4, tsA); off += 4;
         DataHelper.toLong(toSign, off, 4, _tsB); off += 4;
 
+        // handle variable signature size
         Signature sig = _context.dsa().sign(toSign, _context.keyManager().getSigningPrivateKey());
-        byte preSig[] = new byte[Signature.SIGNATURE_BYTES+8];
-        System.arraycopy(sig.getData(), 0, preSig, 0, Signature.SIGNATURE_BYTES);
-        _context.random().nextBytes(preSig, Signature.SIGNATURE_BYTES, 8);
+        int siglen = sig.length();
+        int rem = siglen % AES_SIZE;
+        int padding;
+        if (rem > 0)
+            padding = AES_SIZE - rem;
+        else
+            padding = 0;
+        byte preSig[] = new byte[siglen + padding];
+        System.arraycopy(sig.getData(), 0, preSig, 0, siglen);
+        if (padding > 0)
+            _context.random().nextBytes(preSig, siglen, padding);
         _e_bobSig = new byte[preSig.length];
-        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
+        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug(prefix() + "Sending encrypted inbound confirmation");
@@ -708,6 +879,9 @@ class EstablishState {
      *
      *  All data must be copied out of the buffer as Reader.processRead()
      *  will return it to the pool.
+     *
+     *  State must be VERIFIED.
+     *  Caller must synch.
      */
     private void prepareExtra(ByteBuffer buf) {
         int remaining = buf.remaining();
@@ -724,21 +898,61 @@ class EstablishState {
      * if complete, this will contain any bytes received as part of the
      * handshake that were after the actual handshake.  This may return null.
      */
-    public byte[] getExtraBytes() { return _extra; }
+    public synchronized byte[] getExtraBytes() { return _extra; }
 
+    /**
+     *  Release resources on timeout.
+     *  @param e may be null
+     *  @since 0.9.16
+     */
+    public synchronized void close(String reason, Exception e) {
+        fail(reason, e);
+    }
+
+    /** Caller must synch. */
     private void fail(String reason) { fail(reason, null); }
+
+    /** Caller must synch. */
     private void fail(String reason, Exception e) { fail(reason, e, false); }
+
+    /** Caller must synch. */
     private void fail(String reason, Exception e, boolean bySkew) {
-        _corrupt = true;
+        if (_state == State.CORRUPT || _state == State.VERIFIED)
+            return;
+        _state = State.CORRUPT;
         _failedBySkew = bySkew;
         _err = reason;
         _e = e;
         if (_log.shouldLog(Log.WARN))
             _log.warn(prefix()+"Failed to establish: " + _err, e);
+        releaseBufs();
     }
 
-    public String getError() { return _err; }
-    public Exception getException() { return _e; }
+    /**
+     *  Only call once. Caller must synch.
+     *  @since 0.9.16
+     */
+    private void releaseBufs() {
+        // null or longer for OB
+        if (_prevEncrypted != null && _prevEncrypted.length == AES_SIZE)
+            SimpleByteCache.release(_prevEncrypted);
+        // Do not release _curEncrypted if verified, it is passed to
+        // NTCPConnection to use as the IV
+        if (_state != State.VERIFIED)
+            SimpleByteCache.release(_curEncrypted);
+        SimpleByteCache.release(_curDecrypted);
+        SimpleByteCache.release(_hX_xor_bobIdentHash);
+        if (_dh.getPeerPublicValue() == null)
+            _transport.returnUnused(_dh);
+        if (_con.isInbound())
+            SimpleByteCache.release(_X);
+        else
+            SimpleByteCache.release(_Y);
+    }
+
+    public synchronized String getError() { return _err; }
+
+    public synchronized Exception getException() { return _e; }
     
     /**
      *  XOR a into b. Modifies b. a is unmodified.
@@ -757,11 +971,12 @@ class EstablishState {
     @Override
     public String toString() {
         StringBuilder buf = new StringBuilder(64);
-        buf.append("est").append(System.identityHashCode(this));
-        if (_con.isInbound()) buf.append(" inbound");
-        else buf.append(" outbound");
-        if (_corrupt) buf.append(" corrupt");
-        if (_verified) buf.append(" verified");
+        if (_con.isInbound())
+            buf.append("IBES ");
+        else
+            buf.append("OBES ");
+        buf.append(System.identityHashCode(this));
+        buf.append(' ').append(_state);
         if (_con.isEstablished()) buf.append(" established");
         buf.append(": ");
         return buf.toString();
@@ -828,12 +1043,36 @@ class EstablishState {
      *  @since 0.9.8
      */
     private static class VerifiedEstablishState extends EstablishState {
-        @Override public boolean isComplete() { return true; }
+
+        public VerifiedEstablishState() {
+            super();
+            _state = State.VERIFIED;
+        }
+
         @Override public void prepareOutbound() {
             Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
             log.warn("prepareOutbound() on verified state, doing nothing!");
         }
-        @Override public String toString() { return "VerfiedEstablishState";}
+
+        @Override public String toString() { return "VerifiedEstablishState";}
+    }
+
+    /**
+     *  @since 0.9.16
+     */
+    private static class FailedEstablishState extends EstablishState {
+
+        public FailedEstablishState() {
+            super();
+            _state = State.CORRUPT;
+        }
+
+        @Override public void prepareOutbound() {
+            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
+            log.warn("prepareOutbound() on verified state, doing nothing!");
+        }
+
+        @Override public String toString() { return "FailedEstablishState";}
     }
 
     /** @deprecated unused */
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
index d4fd7521acaab9ba5719aa4be396cf2e8e0ef8dd..b6f539c962c014db3e417a0caeedae749f4cc170 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
@@ -20,8 +20,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.I2PAppContext;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
@@ -532,14 +532,14 @@ class EventPumper implements Runnable {
                 con.outboundConnected();
                 _context.statManager().addRateData("ntcp.connectSuccessful", 1);
             } else {
-                con.close();
+                con.closeOnTimeout("connect failed", null);
                 _transport.markUnreachable(con.getRemotePeer().calculateHash());
                 _context.statManager().addRateData("ntcp.connectFailedTimeout", 1);
             }
         } catch (IOException ioe) {   // this is the usual failure path for a timeout or connect refused
             if (_log.shouldLog(Log.INFO))
                 _log.info("Failed outbound " + con, ioe);
-            con.close();
+            con.closeOnTimeout("connect failed", ioe);
             //_context.banlist().banlistRouter(con.getRemotePeer().calculateHash(), "Error connecting", NTCPTransport.STYLE);
             _transport.markUnreachable(con.getRemotePeer().calculateHash());
             _context.statManager().addRateData("ntcp.connectFailedTimeoutIOE", 1);
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
index 2b2ae6a442acb257b44c09b2f536b49e8fdaf302..a9752a286250a2ec406c749c25aed8f71257b184 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
@@ -17,9 +17,9 @@ import java.util.zip.Adler32;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -266,6 +266,8 @@ class NTCPConnection {
     /** 
      * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
      *                  be under 1 minute)
+     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
+     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
      */
     public void finishInboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
         NTCPConnection toClose = locked_finishInboundEstablishment(key, clockSkew, prevWriteEnd, prevReadEnd);
@@ -278,6 +280,12 @@ class NTCPConnection {
         enqueueInfoMessage();
     }
     
+    /** 
+     * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
+     *                  be under 1 minute)
+     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
+     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
+     */
     private synchronized NTCPConnection locked_finishInboundEstablishment(
             SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
         _sessionKey = key;
@@ -371,10 +379,21 @@ class NTCPConnection {
         }
     }
     
+    /**
+     *  Close and release EstablishState resources.
+     *  @param e may be null
+     *  @since 0.9.16
+     */
+    public void closeOnTimeout(String cause, Exception e) {
+        EstablishState es = _establishState;
+        close();
+        es.close(cause, e);
+    }
+
     private synchronized NTCPConnection locked_close(boolean allowRequeue) {
         if (_chan != null) try { _chan.close(); } catch (IOException ioe) { }
         if (_conKey != null) _conKey.cancel();
-        _establishState = EstablishState.VERIFIED;
+        _establishState = EstablishState.FAILED;
         NTCPConnection old = _transport.removeCon(this);
         _transport.getReader().connectionClosed(this);
         _transport.getWriter().connectionClosed(this);
@@ -571,6 +590,8 @@ class NTCPConnection {
     /** 
      * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
      *                  be under 1 minute)
+     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
+     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
      */
     public synchronized void finishOutboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
         if (_log.shouldLog(Log.DEBUG))
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 0ad8d88a9460c6b5f999f94bdda8c2d3db9f3c9e..f1269c772f09707b5732c1f2c914653ab6484163 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -25,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.CommSystemFacade;
@@ -362,6 +362,12 @@ public class NTCPTransport extends TransportImpl {
             return null;
         }
 
+        // Check for supported sig type
+        if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
+            markUnreachable(peer);
+            return null;
+        }
+
         if (!allowConnection()) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("no bid when trying to send to " + peer + ", max connection limit reached");
@@ -769,6 +775,17 @@ public class NTCPTransport extends TransportImpl {
         return _dhFactory.getBuilder();
     }
 
+    /**
+     * Return an unused DH key builder
+     * to be put back onto the queue for reuse.
+     *
+     * @param builder must not have a peerPublicValue set
+     * @since 0.9.16
+     */
+    void returnUnused(DHSessionKeyBuilder builder) {
+        _dhFactory.returnUnused(builder);
+    }
+
     /**
      * how long from initial connection attempt (accept() or connect()) until
      * the con must be established to avoid premature close()ing
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
index c8b7c96309e414a9b14954f1bf090fd3e3b5aa19..b8c1a5f7e1e773ed96f76e5531a1858e30ed7db5 100644
--- a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
+++ b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
@@ -5,12 +5,30 @@ package net.i2p.router.transport.udp;
  * received messages
  */
 interface ACKBitfield {
+
     /** what message is this partially ACKing? */
     public long getMessageId(); 
+
     /** how many fragments are covered in this bitfield? */
     public int fragmentCount();
+
     /** has the given fragment been received? */
     public boolean received(int fragmentNum);
+
     /** has the entire message been received completely? */
     public boolean receivedComplete();
+
+    /**
+     *  Number of fragments acked in this bitfield.
+     *  Faster than looping through received()
+     *  @since 0.9.16
+     */
+    public int ackCount();
+
+    /**
+     *  Highest fragment number acked in this bitfield.
+     *  @return highest fragment number acked, or -1 if none
+     *  @since 0.9.16
+     */
+    public int highestReceived();
 }
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
index 7d935530263069a1cd67a087d8afbd7f987e96b9..b7cb7d6cbe6034a15451dd7c772e11ce0ad4b798 100644
--- a/router/java/src/net/i2p/router/transport/udp/ACKSender.java
+++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
@@ -177,7 +177,7 @@ class ACKSender implements Runnable {
                     ack.setMessageType(PacketBuilder.TYPE_ACK);
                     
                     if (_log.shouldLog(Log.INFO))
-                        _log.info("Sending ACK for " + ackBitfields);
+                        _log.info("Sending " + ackBitfields + " to " + peer);
                     // locking issues, we ignore the result, and acks are small,
                     // so don't even bother allocating
                     //peer.allocateSendingBytes(ack.getPacket().getLength(), true);
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 267c4fc2a5b6846f8ac48b67752c72ffb49be768..96c4282c8cdebd371049cb034d236681e5d5a77f 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -10,9 +10,9 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
@@ -1363,6 +1363,7 @@ class EstablishmentManager {
             _transport.markUnreachable(peer);
             _transport.dropPeer(peer, false, err);
             //_context.profileManager().commErrorOccurred(peer);
+            outboundState.fail();
         } else {
             OutNetMessage msg;
             while ((msg = outboundState.getNextQueuedMessage()) != null) {
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
index abdd59e15dab5871ded6b824979fc5e9b6e0a56c..6d1f1775549bb7f9f16c6322252f631b40bb9508 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -5,11 +5,12 @@ import java.io.IOException;
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.router.OutNetMessage;
@@ -47,6 +48,9 @@ class InboundEstablishState {
     private long _receivedSignedOnTime;
     private byte _receivedSignature[];
     private boolean _verificationAttempted;
+    // sig not verified
+    private RouterIdentity _receivedUnconfirmedIdentity;
+    // identical to uncomfirmed, but sig now verified
     private RouterIdentity _receivedConfirmedIdentity;
     // general status 
     private final long _establishBegin;
@@ -295,9 +299,28 @@ class InboundEstablishState {
         
         if (cur == _receivedIdentity.length-1) {
             _receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
-            if (_receivedSignature == null)
-                _receivedSignature = new byte[Signature.SIGNATURE_BYTES];
-            conf.readFinalSignature(_receivedSignature, 0);
+            // TODO verify time to prevent replay attacks
+            buildIdentity();
+            if (_receivedUnconfirmedIdentity != null) {
+                SigType type = _receivedUnconfirmedIdentity.getSigningPublicKey().getType();
+                if (type != null) {
+                    int sigLen = type.getSigLen();
+                    if (_receivedSignature == null)
+                        _receivedSignature = new byte[sigLen];
+                    conf.readFinalSignature(_receivedSignature, 0, sigLen);
+                } else {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Unsupported sig type from: " + toString());
+                    // _x() in UDPTransport
+                    _context.banlist().banlistRouterForever(_receivedUnconfirmedIdentity.calculateHash(),
+                                                            "Unsupported signature type");
+                    fail();
+                }
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Bad ident from: " + toString());
+                fail();
+            }
         }
         
         if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || 
@@ -318,9 +341,10 @@ class InboundEstablishState {
      */
     private boolean confirmedFullyReceived() {
         if (_receivedIdentity != null) {
-            for (int i = 0; i < _receivedIdentity.length; i++)
+            for (int i = 0; i < _receivedIdentity.length; i++) {
                 if (_receivedIdentity[i] == null)
                     return false;
+            }
             return true;
         } else {
             return false;
@@ -339,7 +363,51 @@ class InboundEstablishState {
         }
         return _receivedConfirmedIdentity;
     }
-    
+
+    /**
+     *  Construct Alice's RouterIdentity.
+     *  Must have received all fragments.
+     *  Sets _receivedUnconfirmedIdentity, unless invalid.
+     *
+     *  Caller must synch on this.
+     *
+     *  @since 0.9.16 was in verifyIdentity()
+     */
+    private void buildIdentity() {
+        if (_receivedUnconfirmedIdentity != null)
+            return;   // dup pkt?
+        int frags = _receivedIdentity.length;
+        byte[] ident;
+        if (frags > 1) {
+            int identSize = 0;
+            for (int i = 0; i < _receivedIdentity.length; i++)
+                identSize += _receivedIdentity[i].length;
+            ident = new byte[identSize];
+            int off = 0;
+            for (int i = 0; i < _receivedIdentity.length; i++) {
+                int len = _receivedIdentity[i].length;
+                System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
+                off += len;
+            }
+        } else {
+            // no need to copy
+            ident = _receivedIdentity[0];
+        }
+        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
+        RouterIdentity peer = new RouterIdentity();
+        try {
+            peer.readBytes(in);
+            _receivedUnconfirmedIdentity = peer;
+        } catch (DataFormatException dfe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", dfe);
+        } catch (IOException ioe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", ioe);
+        }
+    }
+            
+
     /**
      * Determine if Alice sent us a valid confirmation packet.  The 
      * identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
@@ -351,21 +419,11 @@ class InboundEstablishState {
      * Caller must synch on this.
      */
     private void verifyIdentity() {
-        int identSize = 0;
-        for (int i = 0; i < _receivedIdentity.length; i++)
-            identSize += _receivedIdentity[i].length;
-        byte ident[] = new byte[identSize];
-        int off = 0;
-        for (int i = 0; i < _receivedIdentity.length; i++) {
-            int len = _receivedIdentity[i].length;
-            System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
-            off += len;
-        }
-        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
-        RouterIdentity peer = new RouterIdentity();
-        try {
-            peer.readBytes(in);
-            
+            if (_receivedUnconfirmedIdentity == null)
+                return;   // either not yet recvd or bad ident
+            if (_receivedSignature == null)
+                return;   // either not yet recvd or bad sig
+
             byte signed[] = new byte[256+256 // X + Y
                                      + _aliceIP.length + 2
                                      + _bobIP.length + 2
@@ -373,7 +431,7 @@ class InboundEstablishState {
                                      + 4 // signed on time
                                      ];
 
-            off = 0;
+            int off = 0;
             System.arraycopy(_receivedX, 0, signed, off, _receivedX.length);
             off += _receivedX.length;
             getSentY();
@@ -391,22 +449,15 @@ class InboundEstablishState {
             off += 4;
             DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
             Signature sig = new Signature(_receivedSignature);
-            boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
+            boolean ok = _context.dsa().verifySignature(sig, signed, _receivedUnconfirmedIdentity.getSigningPublicKey());
             if (ok) {
                 // todo partial spoof detection - get peer.calculateHash(),
                 // lookup in netdb locally, if not equal, fail?
-                _receivedConfirmedIdentity = peer;
+                _receivedConfirmedIdentity = _receivedUnconfirmedIdentity;
             } else {
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("Signature failed from " + peer);
+                    _log.warn("Signature failed from " + _receivedUnconfirmedIdentity);
             }
-        } catch (DataFormatException dfe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Improperly formatted yet fully received ident", dfe);
-        } catch (IOException ioe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Improperly formatted yet fully received ident", ioe);
-        }
     }
     
     private void packetReceived() {
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
index 78e41112f27d07e52edc950f4bbd501d06fd8ad1..f69ecb0c521deba9965bce5e290a6a69a8d1c983 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
@@ -74,6 +74,21 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
      * Pull the fragments and ACKs out of the authenticated data packet
      */
     public void receiveData(PeerState from, UDPPacketReader.DataReader data) {
+        try {
+            rcvData(from, data);
+        } catch (DataFormatException dfe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Bad pkt from: " + from, dfe);
+        } catch (IndexOutOfBoundsException ioobe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Bad pkt from: " + from, ioobe);
+        }
+    }
+
+    /**
+     * Pull the fragments and ACKs out of the authenticated data packet
+     */
+    private void rcvData(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
         //long beforeMsgs = _context.clock().now();
         int fragmentsIncluded = receiveMessages(from, data);
         //long afterMsgs = _context.clock().now();
@@ -95,7 +110,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
      *
      * @return number of data fragments included
      */
-    private int receiveMessages(PeerState from, UDPPacketReader.DataReader data) {
+    private int receiveMessages(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
         int fragments = data.readFragmentCount();
         if (fragments <= 0) return fragments;
         Hash fromPeer = from.getRemotePeer();
@@ -132,11 +147,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
                 boolean isNew = false;
                 state = messages.get(messageId);
                 if (state == null) {
-                    try {
-                        state = new InboundMessageState(_context, mid, fromPeer, data, i);
-                    } catch (DataFormatException dfe) {
-                        break;
-                    }
+                    state = new InboundMessageState(_context, mid, fromPeer, data, i);
                     isNew = true;
                     fragmentOK = true;
                     // we will add to messages shortly if it isn't complete
@@ -199,7 +210,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
     /**
      *  @return the number of bitfields in the ack? why?
      */
-    private int receiveACKs(PeerState from, UDPPacketReader.DataReader data) {
+    private int receiveACKs(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
         int rv = 0;
         boolean newAck = false;
         if (data.readACKsIncluded()) {
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
index 4c11aa0f4700c0e48532eca4d73522901345b4d0..fcaae36dd05f10a1ac301e6466f42f358e3f4da9 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
@@ -39,6 +39,9 @@ class InboundMessageState implements CDQEntry {
     private static final long MAX_RECEIVE_TIME = 10*1000;
     public static final int MAX_FRAGMENTS = 64;
     
+    /** 10 */
+    public static final int MAX_PARTIAL_BITFIELD_BYTES = (MAX_FRAGMENTS / 7) + 1;
+
     private static final int MAX_FRAGMENT_SIZE = UDPPacket.MAX_PACKET_SIZE;
     private static final ByteCache _fragmentCache = ByteCache.getInstance(64, MAX_FRAGMENT_SIZE);
     
@@ -86,7 +89,7 @@ class InboundMessageState implements CDQEntry {
      *
      * @return true if the data was ok, false if it was corrupt
      */
-    public boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) {
+    public boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) throws DataFormatException {
         int fragmentNum = data.readMessageFragmentNum(dataFragment);
         if ( (fragmentNum < 0) || (fragmentNum >= _fragments.length)) {
             if (_log.shouldLog(Log.WARN))
@@ -220,57 +223,79 @@ class InboundMessageState implements CDQEntry {
     }
 
     public ACKBitfield createACKBitfield() {
-        return new PartialBitfield(_messageId, _fragments);
+        int sz = (_lastFragment >= 0) ? _lastFragment + 1 : _fragments.length;
+        return new PartialBitfield(_messageId, _fragments, sz);
     }
     
     /**
-     *  A true partial bitfield that is not complete.
+     *  A true partial bitfield that is probably not complete.
+     *  fragmentCount() will return 64 if unknown.
      */
     private static final class PartialBitfield implements ACKBitfield {
         private final long _bitfieldMessageId;
-        private final boolean _fragmentsReceived[];
+        private final int _ackCount;
+        private final int _highestReceived;
+        // bitfield, 1 for acked
+        private final long _fragmentAcks;
         
         /**
          *  @param data each element is non-null or null for received or not
+         *  @param size size of data to use
          */
-        public PartialBitfield(long messageId, Object data[]) {
+        public PartialBitfield(long messageId, Object data[], int size) {
+            if (size > MAX_FRAGMENTS)
+                throw new IllegalArgumentException();
             _bitfieldMessageId = messageId;
-            boolean fragmentsRcvd[] = null;
-            for (int i = data.length - 1; i >= 0; i--) {
+            int ackCount = 0;
+            int highestReceived = -1;
+            long acks = 0;
+            for (int i = 0; i < size; i++) {
                 if (data[i] != null) {
-                    if (fragmentsRcvd == null)
-                        fragmentsRcvd = new boolean[i+1];
-                    fragmentsRcvd[i] = true;
+                    acks |= mask(i);
+                    ackCount++;
+                    highestReceived = i;
                 }
             }
-            if (fragmentsRcvd == null)
-                _fragmentsReceived = new boolean[0];
-            else
-                _fragmentsReceived = fragmentsRcvd;
+            _fragmentAcks = acks;
+            _ackCount = ackCount;
+            _highestReceived = highestReceived;
+        }
+
+        /**
+         *  @param fragment 0-63
+         */
+        private static long mask(int fragment) {
+            return 1L << fragment;
         }
 
-        public int fragmentCount() { return _fragmentsReceived.length; }
+        public int fragmentCount() { return _highestReceived + 1; }
+
+        public int ackCount() { return _ackCount; }
+
+        public int highestReceived() { return _highestReceived; }
 
         public long getMessageId() { return _bitfieldMessageId; }
 
         public boolean received(int fragmentNum) { 
-            if ( (fragmentNum < 0) || (fragmentNum >= _fragmentsReceived.length) )
+            if (fragmentNum < 0 || fragmentNum > _highestReceived)
                 return false;
-            return _fragmentsReceived[fragmentNum];
+            return (_fragmentAcks & mask(fragmentNum)) != 0;
         }
 
-        /** @return false always */
-        public boolean receivedComplete() { return false; }
+        public boolean receivedComplete() { return _ackCount == _highestReceived + 1; }
         
         @Override
         public String toString() { 
             StringBuilder buf = new StringBuilder(64);
-            buf.append("Partial ACK of ");
+            buf.append("OB Partial ACK of ");
             buf.append(_bitfieldMessageId);
-            buf.append(" with ACKs for: ");
-            for (int i = 0; i < _fragmentsReceived.length; i++)
-                if (_fragmentsReceived[i])
-                    buf.append(i).append(" ");
+            buf.append(" highest: ").append(_highestReceived);
+            buf.append(" with ").append(_ackCount).append(" ACKs for: [");
+            for (int i = 0; i <= _highestReceived; i++) {
+                if (received(i))
+                    buf.append(i).append(' ');
+            }
+            buf.append("] / ").append(_highestReceived + 1);
             return buf.toString();
         }
     }
diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
index 37db45880148bc5aaa060610382cc9a63701fe23..7e55cf1ce3e19dcdc769a4369187eeb422711aa0 100644
--- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
@@ -11,8 +11,8 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Base64;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Addresses;
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
index ba4375dbdb8f8ec34623bd162a7c38df0a7b9d5c..9f519f5ae7ee84cda6a75efdf6e5372ea2c0522e 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -3,10 +3,11 @@ package net.i2p.router.transport.udp;
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
 
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.data.i2np.DatabaseStoreMessage;
@@ -41,6 +42,7 @@ class OutboundEstablishState {
     private SessionKey _sessionKey;
     private SessionKey _macKey;
     private Signature _receivedSignature;
+    // includes trailing padding to mod 16
     private byte[] _receivedEncryptedSignature;
     private byte[] _receivedIV;
     // SessionConfirmed messages
@@ -104,6 +106,7 @@ class OutboundEstablishState {
     /**
      *  @param claimedAddress an IP/port based RemoteHostId, or null if unknown
      *  @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect
+     *  @param remotePeer must have supported sig type
      *  @param introKey Bob's introduction key, as published in the netdb
      *  @param addr non-null
      */
@@ -198,14 +201,16 @@ class OutboundEstablishState {
     /** caller must synch - only call once */
     private void prepareSessionRequest() {
         _keyBuilder = _keyFactory.getBuilder();
-        _sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
         byte X[] = _keyBuilder.getMyPublicValue().toByteArray();
-        if (X.length == 257)
+        if (X.length == 257) {
+            _sentX = new byte[256];
             System.arraycopy(X, 1, _sentX, 0, _sentX.length);
-        else if (X.length == 256)
-            System.arraycopy(X, 0, _sentX, 0, _sentX.length);
-        else
+        } else if (X.length == 256) {
+            _sentX = X;
+        } else {
+            _sentX = new byte[256];
             System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length);
+        }
     }
 
     public synchronized byte[] getSentX() {
@@ -216,7 +221,7 @@ class OutboundEstablishState {
         return _sentX;
     }
 
-    /**
+    /**x
      * The remote side (Bob) - note that in some places he's called Charlie.
      * Warning - may change after introduction. May be null before introduction.
      */
@@ -247,8 +252,20 @@ class OutboundEstablishState {
         _alicePort = reader.readPort();
         _receivedRelayTag = reader.readRelayTag();
         _receivedSignedOnTime = reader.readSignedOnTime();
-        _receivedEncryptedSignature = new byte[Signature.SIGNATURE_BYTES + 8];
-        reader.readEncryptedSignature(_receivedEncryptedSignature, 0);
+        // handle variable signature size
+        SigType type = _remotePeer.getSigningPublicKey().getType();
+        if (type == null) {
+            // shouldn't happen, we only connect to supported peers
+            fail();
+            packetReceived();
+            return;
+        }
+        int sigLen = type.getSigLen();
+        int mod = sigLen % 16;
+        int pad = (mod == 0) ? 0 : (16 - mod);
+        int esigLen = sigLen + pad;
+        _receivedEncryptedSignature = new byte[esigLen];
+        reader.readEncryptedSignature(_receivedEncryptedSignature, 0, esigLen);
         _receivedIV = new byte[UDPPacket.IV_SIZE];
         reader.readIV(_receivedIV, 0);
         
@@ -324,6 +341,11 @@ class OutboundEstablishState {
         _receivedEncryptedSignature = null;
         _receivedIV = null;
         _receivedSignature = null;
+        if (_keyBuilder != null) {
+            if (_keyBuilder.getPeerPublicValue() == null)
+                _keyFactory.returnUnused(_keyBuilder);
+            _keyBuilder = null;
+        }
         // sure, there's a chance the packet was corrupted, but in practice
         // this means that Bob doesn't know his external port, so give up.
         _currentState = OutboundState.OB_STATE_VALIDATION_FAILED;
@@ -353,7 +375,9 @@ class OutboundEstablishState {
      * decrypt the signature (and subsequent pad bytes) with the 
      * additional layer of encryption using the negotiated key along side
      * the packet's IV
+     *
      *  Caller must synch on this.
+     *  Only call this once! Decrypts in-place.
      */
     private void decryptSignature() {
         if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString());
@@ -361,11 +385,20 @@ class OutboundEstablishState {
         if (_receivedIV == null) throw new NullPointerException("IV is null!");
         _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0, 
                                _sessionKey, _receivedIV, _receivedEncryptedSignature.length);
-        byte signatureBytes[] = new byte[Signature.SIGNATURE_BYTES];
-        System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, Signature.SIGNATURE_BYTES);
-        _receivedSignature = new Signature(signatureBytes);
+        // handle variable signature size
+        SigType type = _remotePeer.getSigningPublicKey().getType();
+        // if type == null throws NPE
+        int sigLen = type.getSigLen();
+        int mod = sigLen % 16;
+        if (mod != 0) {
+            byte signatureBytes[] = new byte[sigLen];
+            System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, sigLen);
+            _receivedSignature = new Signature(type, signatureBytes);
+        } else {
+            _receivedSignature = new Signature(type, _receivedEncryptedSignature);
+        }
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Decrypted received signature: " + Base64.encode(signatureBytes));
+            _log.debug("Decrypted received signature: " + Base64.encode(_receivedSignature.getData()));
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
index 034f3e60548e10671f2ca013e842cbe211a9e2ed..ab91478f171b5e7642370e2d85d0ae42638bb4ee 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
@@ -1,14 +1,19 @@
 package net.i2p.router.transport.udp;
 
+import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
+import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
+import net.i2p.router.transport.udp.PacketBuilder.Fragment;
 import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.Log;
 
@@ -74,6 +79,7 @@ class OutboundMessageFragments {
         _context.statManager().createRateStat("udp.sendVolleyTime", "Long it takes to send a full volley", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmTime", "How long it takes to send a message and get the ACK", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmFragments", "How many fragments are included in a fully ACKed message", "udp", UDPTransport.RATES);
+        _context.statManager().createRateStat("udp.sendFragmentsPerPacket", "How many fragments are sent in a data packet", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendConfirmVolley", "How many times did fragments need to be sent before ACK", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendFailed", "How many sends a failed message was pushed", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendAggressiveFailed", "How many volleys was a packet sent before we gave up", "udp", UDPTransport.RATES);
@@ -81,7 +87,7 @@ class OutboundMessageFragments {
         _context.statManager().createRateStat("udp.outboundActivePeers", "How many peers we are actively sending to", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendRejected", "What volley are we on when the peer was throttled", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.partialACKReceived", "How many fragments were partially ACKed", "udp", UDPTransport.RATES);
-        _context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
+        //_context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendPiggyback", "How many acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.sendPiggybackPartial", "How many partial acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
         _context.statManager().createRequiredRateStat("udp.packetsRetransmitted", "Lifetime of packets during retransmission (ms)", "udp", UDPTransport.RATES);
@@ -236,19 +242,17 @@ class OutboundMessageFragments {
     /**
      * Fetch all the packets for a message volley, blocking until there is a
      * message which can be fully transmitted (or the transport is shut down).
-     * The returned array may be sparse, with null packets taking the place of
-     * already ACKed fragments.
      *
      * NOT thread-safe. Called by the PacketPusher thread only.
      *
      * @return null only on shutdown
      */
-    public UDPPacket[] getNextVolley() {
+    public List<UDPPacket> getNextVolley() {
         PeerState peer = null;
-        OutboundMessageState state = null;
+        List<OutboundMessageState> states = null;
         // Keep track of how many we've looked at, since we don't start the iterator at the beginning.
         int peersProcessed = 0;
-        while (_alive && (state == null) ) {
+        while (_alive && (states == null) ) {
             int nextSendDelay = Integer.MAX_VALUE;
             // no, not every time - O(n**2) - do just before waiting below
             //finishMessages();
@@ -275,8 +279,8 @@ class OutboundMessageFragments {
                             continue;
                         }
                         peersProcessed++;
-                        state = peer.allocateSend();
-                        if (state != null) {
+                        states = peer.allocateSend();
+                        if (states != null) {
                             // we have something to send and we will be returning it
                             break;
                         } else if (peersProcessed >= _activePeers.size()) {
@@ -292,13 +296,13 @@ class OutboundMessageFragments {
                         }
                     }
 
-                    if (peer != null && _log.shouldLog(Log.DEBUG))
-                        _log.debug("Done looping, next peer we are sending for: " +
-                                   peer.getRemotePeer());
+                    //if (peer != null && _log.shouldLog(Log.DEBUG))
+                    //    _log.debug("Done looping, next peer we are sending for: " +
+                    //               peer.getRemotePeer());
 
                     // if we've gone all the way through the loop, wait
                     // ... unless nextSendDelay says we have more ready now
-                    if (state == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
+                    if (states == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
                         _isWaiting = true;
                         peersProcessed = 0;
                         // why? we do this in the loop one at a time
@@ -328,9 +332,9 @@ class OutboundMessageFragments {
         } // while alive && state == null
 
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending " + state);
+            _log.debug("Sending " + DataHelper.toString(states));
 
-        UDPPacket packets[] = preparePackets(state, peer);
+        List<UDPPacket> packets = preparePackets(states, peer);
 
       /****
         if ( (state != null) && (state.getMessage() != null) ) {
@@ -352,58 +356,108 @@ class OutboundMessageFragments {
     /**
      *  @return null if state or peer is null
      */
-    private UDPPacket[] preparePackets(OutboundMessageState state, PeerState peer) {
-        if ( (state != null) && (peer != null) ) {
+    private List<UDPPacket> preparePackets(List<OutboundMessageState> states, PeerState peer) {
+        if (states == null || peer == null)
+            return null;
+
+        // ok, simplest possible thing is to always tack on the bitfields if
+        List<Long> msgIds = peer.getCurrentFullACKs();
+        int newFullAckCount = msgIds.size();
+        msgIds.addAll(peer.getCurrentResendACKs());
+        List<ACKBitfield> partialACKBitfields = new ArrayList<ACKBitfield>();
+        peer.fetchPartialACKs(partialACKBitfields);
+        int piggybackedPartialACK = partialACKBitfields.size();
+        // getCurrentFullACKs() already makes a copy, do we need to copy again?
+        // YES because buildPacket() now removes them (maybe)
+        List<Long> remaining = new ArrayList<Long>(msgIds);
+
+        // build the list of fragments to send
+        List<Fragment> toSend = new ArrayList<Fragment>(8);
+        for (OutboundMessageState state : states) {
             int fragments = state.getFragmentCount();
-            if (fragments < 0)
-                return null;
-
-            // ok, simplest possible thing is to always tack on the bitfields if
-            List<Long> msgIds = peer.getCurrentFullACKs();
-            int newFullAckCount = msgIds.size();
-            msgIds.addAll(peer.getCurrentResendACKs());
-            List<ACKBitfield> partialACKBitfields = new ArrayList<ACKBitfield>();
-            peer.fetchPartialACKs(partialACKBitfields);
-            int piggybackedPartialACK = partialACKBitfields.size();
-            // getCurrentFullACKs() already makes a copy, do we need to copy again?
-            // YES because buildPacket() now removes them (maybe)
-            List<Long> remaining = new ArrayList<Long>(msgIds);
-            int sparseCount = 0;
-            UDPPacket rv[] = new UDPPacket[fragments]; //sparse
+            int queued = 0;
             for (int i = 0; i < fragments; i++) {
                 if (state.needsSending(i)) {
-                    int before = remaining.size();
-                    try {
-                        rv[i] = _builder.buildPacket(state, i, peer, remaining, newFullAckCount, partialACKBitfields);
-                    } catch (ArrayIndexOutOfBoundsException aioobe) {
-                        _log.log(Log.CRIT, "Corrupt trying to build a packet - please tell jrandom: " +
-                                 partialACKBitfields + " / " + remaining + " / " + msgIds);
-                        sparseCount++;
-                        continue;
-                    }
-                    int after = remaining.size();
-                    newFullAckCount = Math.max(0, newFullAckCount - (before - after));
-                    if (rv[i] == null) {
-                        sparseCount++;
-                        continue;
-                    }
-                    rv[i].setFragmentCount(fragments);
-                    OutNetMessage msg = state.getMessage();
-                    if (msg != null)
-                        rv[i].setMessageType(msg.getMessageTypeId());
-                    else
-                        rv[i].setMessageType(-1);
-                } else {
-                    sparseCount++;
+                    toSend.add(new Fragment(state, i));
+                    queued++;
+                }
+            }
+            // per-state stats
+            if (queued > 0 && state.getPushCount() > 1) {
+                peer.messageRetransmitted(queued);
+                // _packetsRetransmitted += toSend; // lifetime for the transport
+                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
+                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Retransmitting " + state + " to " + peer);
+                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), queued);
+            }
+        }
+
+        if (toSend.isEmpty())
+            return null;
+
+        int fragmentsToSend = toSend.size();
+        // sort by size, biggest first
+        // don't bother unless more than one state (fragments are already sorted within a state)
+        if (fragmentsToSend > 1 && states.size() > 1)
+            Collections.sort(toSend, new FragmentComparator());
+
+        List<Fragment> sendNext = new ArrayList<Fragment>(Math.min(toSend.size(), 4));
+        List<UDPPacket> rv = new ArrayList<UDPPacket>(toSend.size());
+        for (int i = 0; i < toSend.size(); i++) {
+            Fragment next = toSend.get(i);
+            sendNext.add(next);
+            OutboundMessageState state = next.state;
+            OutNetMessage msg = state.getMessage();
+            int msgType = (msg != null) ? msg.getMessageTypeId() : -1;
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Building packet for " + next + " to " + peer);
+            int curTotalDataSize = state.fragmentSize(next.num);
+            // now stuff in more fragments if they fit
+            if (i +1 < toSend.size()) {
+                int maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
+                for (int j = i + 1; j < toSend.size(); j++) {
+                    next = toSend.get(j);
+                    int nextDataSize = next.state.fragmentSize(next.num);
+                    //if (PacketBuilder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
+                    //if (_builder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
+                    if (nextDataSize <= maxAvail) {
+                        // add it
+                        toSend.remove(j);
+                        j--;
+                        sendNext.add(next);
+                        curTotalDataSize += nextDataSize;
+                        maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info("Adding in additional " + next + " to " + peer);
+                    }  // else too big
                 }
             }
-            if (sparseCount > 0)
-                remaining.clear();
+
+            int before = remaining.size();
+            UDPPacket pkt = _builder.buildPacket(sendNext, peer, remaining, newFullAckCount, partialACKBitfields);
+            if (pkt != null) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Built packet with " + sendNext.size() + " fragments totalling " + curTotalDataSize +
+                              " data bytes to " + peer);
+                _context.statManager().addRateData("udp.sendFragmentsPerPacket", sendNext.size());
+            }
+            sendNext.clear();
+            if (pkt == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.info("Build packet FAIL for " + DataHelper.toString(sendNext) + " to " + peer);
+                continue;
+            }
+            rv.add(pkt);
+
+            int after = remaining.size();
+            newFullAckCount = Math.max(0, newFullAckCount - (before - after));
 
             int piggybackedAck = 0;
             if (msgIds.size() != remaining.size()) {
-                for (int i = 0; i < msgIds.size(); i++) {
-                    Long id = msgIds.get(i);
+                for (int j = 0; j < msgIds.size(); j++) {
+                    Long id = msgIds.get(j);
                     if (!remaining.contains(id)) {
                         peer.removeACKMessage(id);
                         piggybackedAck++;
@@ -411,29 +465,36 @@ class OutboundMessageFragments {
                 }
             }
 
-            if (sparseCount > 0)
-                _context.statManager().addRateData("udp.sendSparse", sparseCount, state.getLifetime());
             if (piggybackedAck > 0)
-                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck, state.getLifetime());
+                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck);
             if (piggybackedPartialACK - partialACKBitfields.size() > 0)
                 _context.statManager().addRateData("udp.sendPiggybackPartial", piggybackedPartialACK - partialACKBitfields.size(), state.getLifetime());
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Building packet for " + state + " to " + peer + " with sparse count: " + sparseCount);
-            peer.packetsTransmitted(fragments - sparseCount);
-            if (state.getPushCount() > 1) {
-                int toSend = fragments-sparseCount;
-                peer.messageRetransmitted(toSend);
-                // _packetsRetransmitted += toSend; // lifetime for the transport
-                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
-                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("Retransmitting " + state + " to " + peer);
-                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), toSend);
-            }
-            return rv;
-        } else {
-            // !alive
-            return null;
+
+            // following for debugging and stats
+            pkt.setFragmentCount(sendNext.size());
+            pkt.setMessageType(msgType);  //type of first fragment
+        }
+
+
+
+        int sent = rv.size();
+        peer.packetsTransmitted(sent);
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Sent " + fragmentsToSend + " fragments of " + states.size() +
+                      " messages in " + sent + " packets to " + peer);
+
+        return rv;
+    }
+
+    /**
+     *  Biggest first
+     *  @since 0.9.16
+     */
+    private static class FragmentComparator implements Comparator<Fragment>, Serializable {
+
+        public int compare(Fragment l, Fragment r) {
+            // reverse
+            return r.state.fragmentSize(r.num) - l.state.fragmentSize(l.num);
         }
     }
 
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
index 39746e737f0531a9b6d66d2adbff387d5b214a1c..bc625bea417a9d4e10b6e1d4d54a36b3263d69cb 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
@@ -4,16 +4,16 @@ import java.util.Date;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
-import net.i2p.data.ByteArray;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.util.CDPQEntry;
-import net.i2p.util.ByteCache;
 import net.i2p.util.Log;
 
 /**
  * Maintain the outbound fragmentation for resending, for a single message.
  *
+ * All methods are thread-safe.
+ *
  */
 class OutboundMessageState implements CDPQEntry {
     private final I2PAppContext _context;
@@ -21,50 +21,32 @@ class OutboundMessageState implements CDPQEntry {
     /** may be null if we are part of the establishment */
     private final OutNetMessage _message;
     private final I2NPMessage _i2npMessage;
-    private final long _messageId;
     /** will be null, unless we are part of the establishment */
     private final PeerState _peer;
     private final long _expiration;
-    private ByteArray _messageBuf;
+    private final byte[] _messageBuf;
     /** fixed fragment size across the message */
-    private int _fragmentSize;
-    /** size of the I2NP message */
-    private int _totalSize;
-    /** sends[i] is how many times the fragment has been sent, or -1 if ACKed */
-    private short _fragmentSends[];
+    private final int _fragmentSize;
+    /** bitmask, 0 if acked, all 0 = complete */
+    private long _fragmentAcks;
+    private final int _numFragments;
     private final long _startedOn;
     private long _nextSendTime;
     private int _pushCount;
-    private short _maxSends;
-    // private int _nextSendFragment;
-    /** for tracking use-after-free bugs */
-    private boolean _released;
-    private Exception _releasedBy;
+    private int _maxSends;
     // we can't use the ones in _message since it is null for injections
     private long _enqueueTime;
     private long _seqNum;
     
     public static final int MAX_MSG_SIZE = 32 * 1024;
-    private static final int CACHE4_BYTES = MAX_MSG_SIZE;
-    private static final int CACHE3_BYTES = CACHE4_BYTES / 4;
-    private static final int CACHE2_BYTES = CACHE3_BYTES / 4;
-    private static final int CACHE1_BYTES = CACHE2_BYTES / 4;
-
-    private static final int CACHE1_MAX = 256;
-    private static final int CACHE2_MAX = CACHE1_MAX / 4;
-    private static final int CACHE3_MAX = CACHE2_MAX / 4;
-    private static final int CACHE4_MAX = CACHE3_MAX / 4;
-
-    private static final ByteCache _cache1 = ByteCache.getInstance(CACHE1_MAX, CACHE1_BYTES);
-    private static final ByteCache _cache2 = ByteCache.getInstance(CACHE2_MAX, CACHE2_BYTES);
-    private static final ByteCache _cache3 = ByteCache.getInstance(CACHE3_MAX, CACHE3_BYTES);
-    private static final ByteCache _cache4 = ByteCache.getInstance(CACHE4_MAX, CACHE4_BYTES);
 
     private static final long EXPIRATION = 10*1000;
     
 
     /**
-     *  Called from UDPTransport
+     *  "injected" message from the establisher.
+     *
+     *  Called from UDPTransport.
      *  @throws IAE if too big or if msg or peer is null
      */
     public OutboundMessageState(I2PAppContext context, I2NPMessage msg, PeerState peer) {
@@ -72,7 +54,9 @@ class OutboundMessageState implements CDPQEntry {
     }
     
     /**
-     *  Called from OutboundMessageFragments
+     *  Normal constructor.
+     *
+     *  Called from OutboundMessageFragments.
      *  @throws IAE if too big or if msg or peer is null
      */
     public OutboundMessageState(I2PAppContext context, OutNetMessage m, PeerState peer) {
@@ -92,160 +76,87 @@ class OutboundMessageState implements CDPQEntry {
         _message = m;
         _i2npMessage = msg;
         _peer = peer;
-        _messageId = msg.getUniqueId();
         _startedOn = _context.clock().now();
         _nextSendTime = _startedOn;
         _expiration = _startedOn + EXPIRATION;
         //_expiration = msg.getExpiration();
 
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("Raw byte array for " + _messageId + ": " + Base64.encode(_messageBuf.getData(), 0, len));
-    }
-    
-    /**
-     * lazily inits the message buffer unless already inited
-     */
-    private synchronized void initBuf() {
-        if (_messageBuf != null)
-            return;
-        final int size = _i2npMessage.getRawMessageSize();
-        acquireBuf(size);
-        _totalSize = _i2npMessage.toRawByteArray(_messageBuf.getData());
-        _messageBuf.setValid(_totalSize);
-    }
-    
-    /**
-     *  @throws IAE if too big
-     *  @since 0.9.3
-     */
-    private void acquireBuf(int size) {
-        if (_messageBuf != null)
-            releaseBuf();
-        if (size <= CACHE1_BYTES)
-            _messageBuf =  _cache1.acquire();
-        else if (size <= CACHE2_BYTES)
-            _messageBuf = _cache2.acquire();
-        else if (size <= CACHE3_BYTES)
-            _messageBuf = _cache3.acquire();
-        else if (size <= CACHE4_BYTES)
-            _messageBuf = _cache4.acquire();
-        else
-            throw new IllegalArgumentException("Size too large! " + size);
+        // now "fragment" it
+        int totalSize = _i2npMessage.getRawMessageSize();
+        if (totalSize > MAX_MSG_SIZE)
+            throw new IllegalArgumentException("Size too large! " + totalSize);
+        _messageBuf = new byte[totalSize];
+        _i2npMessage.toRawByteArray(_messageBuf);
+        _fragmentSize = _peer.fragmentSize();
+        int numFragments = totalSize / _fragmentSize;
+        if (numFragments * _fragmentSize < totalSize)
+            numFragments++;
+        // This should never happen, as 534 bytes * 64 fragments > 32KB, and we won't bid on > 32KB
+        if (numFragments > InboundMessageState.MAX_FRAGMENTS)
+            throw new IllegalArgumentException("Fragmenting a " + totalSize + " message into " + numFragments + " fragments - too many!");
+        _numFragments = numFragments;
+        // all 1's where we care
+        _fragmentAcks = _numFragments < 64 ? mask(_numFragments) - 1L : -1L;
     }
     
     /**
-     *  @since 0.9.3
+     *  @param fragment 0-63
      */
-    private void releaseBuf() {
-        if (_messageBuf == null)
-            return;
-        int size = _messageBuf.getData().length;
-        if (size == CACHE1_BYTES)
-            _cache1.release(_messageBuf);
-        else if (size == CACHE2_BYTES)
-            _cache2.release(_messageBuf);
-        else if (size == CACHE3_BYTES)
-            _cache3.release(_messageBuf);
-        else if (size == CACHE4_BYTES)
-            _cache4.release(_messageBuf);
-        _messageBuf = null;
-        _released = true;
+    private static long mask(int fragment) {
+        return 1L << fragment;
     }
 
-    /**
-     *  This is synchronized with writeFragment(),
-     *  so we do not release (probably due to an ack) while we are retransmitting.
-     *  Also prevent double-free
-     */
-    public synchronized void releaseResources() { 
-        if (_messageBuf != null && !_released) {
-            releaseBuf();
-            if (_log.shouldLog(Log.WARN))
-                _releasedBy = new Exception ("Released on " + new Date() + " by:");
-        }
-        //_messageBuf = null;
-    }
-    
     public OutNetMessage getMessage() { return _message; }
-    public long getMessageId() { return _messageId; }
+
+    public long getMessageId() { return _i2npMessage.getUniqueId(); }
+
     public PeerState getPeer() { return _peer; }
 
     public boolean isExpired() {
         return _expiration < _context.clock().now(); 
     }
 
-    public boolean isComplete() {
-        short sends[] = _fragmentSends;
-        if (sends == null) return false;
-        for (int i = 0; i < sends.length; i++)
-            if (sends[i] >= 0)
-                return false;
-        // nothing else pending ack
-        return true;
+    public synchronized boolean isComplete() {
+        return _fragmentAcks == 0;
     }
 
     public synchronized int getUnackedSize() {
-        short fragmentSends[] = _fragmentSends;
-        ByteArray messageBuf = _messageBuf;
         int rv = 0;
-        if ( (messageBuf != null) && (fragmentSends != null) ) {
-            int lastSize = _totalSize % _fragmentSize;
-            if (lastSize == 0)
-                lastSize = _fragmentSize;
-            for (int i = 0; i < fragmentSends.length; i++) {
-                if (fragmentSends[i] >= (short)0) {
-                    if (i + 1 == fragmentSends.length)
-                        rv += lastSize;
-                    else
-                        rv += _fragmentSize;
-                }
+        if (isComplete())
+            return rv;
+        int lastSize = _messageBuf.length % _fragmentSize;
+        if (lastSize == 0)
+            lastSize = _fragmentSize;
+        for (int i = 0; i < _numFragments; i++) {
+            if (needsSending(i)) {
+                if (i + 1 == _numFragments)
+                    rv += lastSize;
+                else
+                    rv += _fragmentSize;
             }
         }
         return rv;
     }
 
-    public boolean needsSending(int fragment) {
-        
-        short sends[] = _fragmentSends;
-        if ( (sends == null) || (fragment >= sends.length) || (fragment < 0) )
-            return false;
-        return (sends[fragment] >= (short)0);
+    public synchronized boolean needsSending(int fragment) {
+        return (_fragmentAcks & mask(fragment)) != 0;
     }
 
     public long getLifetime() { return _context.clock().now() - _startedOn; }
     
     /**
-     * Ack all the fragments in the ack list.  As a side effect, if there are
-     * still unacked fragments, the 'next send' time will be updated under the
-     * assumption that that all of the packets within a volley would reach the
-     * peer within that ack frequency (2-400ms).
+     * Ack all the fragments in the ack list.
      *
      * @return true if the message was completely ACKed
      */
-    public boolean acked(ACKBitfield bitfield) {
+    public synchronized boolean acked(ACKBitfield bitfield) {
         // stupid brute force, but the cardinality should be trivial
-        short sends[] = _fragmentSends;
-        if (sends != null)
-            for (int i = 0; i < bitfield.fragmentCount() && i < sends.length; i++)
-                if (bitfield.received(i))
-                    sends[i] = (short)-1;
-        
-        boolean rv = isComplete();
-      /****
-        if (!rv && false) { // don't do the fast retransmit... lets give it time to get ACKed
-            long nextTime = _context.clock().now() + Math.max(_peer.getRTT(), ACKSender.ACK_FREQUENCY);
-            //_nextSendTime = Math.max(now, _startedOn+PeerState.MIN_RTO);
-            if (_nextSendTime <= 0)
-                _nextSendTime = nextTime;
-            else
-                _nextSendTime = Math.min(_nextSendTime, nextTime);
-            
-            //if (now + 100 > _nextSendTime)
-            //    _nextSendTime = now + 100;
-            //_nextSendTime = now;
+        int highest = bitfield.highestReceived();
+        for (int i = 0; i <= highest && i < _numFragments; i++) {
+            if (bitfield.received(i))
+                _fragmentAcks &= ~mask(i);
         }
-      ****/
-        return rv;
+        return isComplete();
     }
     
     public long getNextSendTime() { return _nextSendTime; }
@@ -255,105 +166,45 @@ class OutboundMessageState implements CDPQEntry {
      *  The max number of sends for any fragment, which is the
      *  same as the push count, at least as it's coded now.
      */
-    public int getMaxSends() { return _maxSends; }
+    public synchronized int getMaxSends() { return _maxSends; }
 
     /**
      *  The number of times we've pushed some fragments, which is the
      *  same as the max sends, at least as it's coded now.
      */
-    public int getPushCount() { return _pushCount; }
-
-    /** note that we have pushed the message fragments */
-    public void push() { 
-        // these will never be different...
-        _pushCount++; 
-        if (_pushCount > _maxSends)
-            _maxSends = (short)_pushCount;
-        if (_fragmentSends != null)
-            for (int i = 0; i < _fragmentSends.length; i++)
-                if (_fragmentSends[i] >= (short)0)
-                    _fragmentSends[i] = (short)(1 + _fragmentSends[i]);
-        
-    }
+    public synchronized int getPushCount() { return _pushCount; }
 
     /**
-     * Whether fragment() has been called.
-     * NOT whether it has more than one fragment.
-     *
-     * Caller should synchronize
-     *
-     * @return true iff fragment() has been called previously
+     * Note that we have pushed the message fragments.
+     * Increments push count (and max sends... why?)
      */
-    public boolean isFragmented() { return _fragmentSends != null; }
-
-    /**
-     * Prepare the message for fragmented delivery, using no more than
-     * fragmentSize bytes per fragment.
-     *
-     * Caller should synchronize
-     *
-     * @throws IllegalStateException if called more than once
-     */
-    public void fragment(int fragmentSize) {
-        if (_fragmentSends != null)
-            throw new IllegalStateException();
-        initBuf();
-        int numFragments = _totalSize / fragmentSize;
-        if (numFragments * fragmentSize < _totalSize)
-            numFragments++;
-        // This should never happen, as 534 bytes * 64 fragments > 32KB, and we won't bid on > 32KB
-        if (numFragments > InboundMessageState.MAX_FRAGMENTS)
-            throw new IllegalArgumentException("Fragmenting a " + _totalSize + " message into " + numFragments + " fragments - too many!");
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Fragmenting a " + _totalSize + " message into " + numFragments + " fragments");
-        
-        //_fragmentEnd = new int[numFragments];
-        _fragmentSends = new short[numFragments];
-        //Arrays.fill(_fragmentEnd, -1);
-        //Arrays.fill(_fragmentSends, (short)0);
-        
-        _fragmentSize = fragmentSize;
+    public synchronized void push() { 
+        // these will never be different...
+        _pushCount++; 
+        _maxSends = _pushCount;
     }
 
     /**
      * How many fragments in the message.
-     * Only valid after fragment() has been called.
-     * Returns -1 before then.
-     *
-     * Caller should synchronize
      */
     public int getFragmentCount() { 
-        if (_fragmentSends == null) 
-            return -1;
-        else
-            return _fragmentSends.length; 
+            return _numFragments; 
     }
 
     /**
      * The size of the I2NP message. Does not include any SSU overhead.
-     *
-     * Caller should synchronize
      */
-    public int getMessageSize() { return _totalSize; }
+    public int getMessageSize() { return _messageBuf.length; }
 
     /**
-     * Should we continue sending this fragment?
-     * Only valid after fragment() has been called.
-     * Throws NPE before then.
+     * The size in bytes of the fragment
      *
-     * Caller should synchronize
-     */
-    public boolean shouldSend(int fragmentNum) { return _fragmentSends[fragmentNum] >= (short)0; }
-
-    /**
-     * This assumes fragment(int size) has been called
      * @param fragmentNum the number of the fragment 
      * @return the size of the fragment specified by the number
      */
     public int fragmentSize(int fragmentNum) {
-        if (_messageBuf == null) return -1;
-        if (fragmentNum + 1 == _fragmentSends.length) {
-            int valid = _totalSize;
+        if (fragmentNum + 1 == _numFragments) {
+            int valid = _messageBuf.length;
             if (valid <= _fragmentSize)
                 return valid;
             // bugfix 0.8.12
@@ -366,63 +217,19 @@ class OutboundMessageState implements CDPQEntry {
 
     /**
      * Write a part of the the message onto the specified buffer.
-     * See releaseResources() above for synchronization information.
-     * This assumes fragment(int size) has been called.
      *
      * @param out target to write
      * @param outOffset into outOffset to begin writing
      * @param fragmentNum fragment to write (0 indexed)
      * @return bytesWritten
      */
-    public synchronized int writeFragment(byte out[], int outOffset, int fragmentNum) {
-        if (_messageBuf == null) return -1;
-        if (_released) {
-            /******
-                Solved by synchronization with releaseResources() and simply returning -1.
-                Previous output:
-
-                23:50:57.013 ERROR [acket pusher] sport.udp.OutboundMessageState: SSU OMS Use after free
-                java.lang.Exception: Released on Wed Dec 23 23:50:57 GMT 2009 by:
-                	at net.i2p.router.transport.udp.OutboundMessageState.releaseResources(OutboundMessageState.java:133)
-                	at net.i2p.router.transport.udp.PeerState.acked(PeerState.java:1391)
-                	at net.i2p.router.transport.udp.OutboundMessageFragments.acked(OutboundMessageFragments.java:404)
-                	at net.i2p.router.transport.udp.InboundMessageFragments.receiveACKs(InboundMessageFragments.java:191)
-                	at net.i2p.router.transport.udp.InboundMessageFragments.receiveData(InboundMessageFragments.java:77)
-                	at net.i2p.router.transport.udp.PacketHandler$Handler.handlePacket(PacketHandler.java:485)
-                	at net.i2p.router.transport.udp.PacketHandler$Handler.receivePacket(PacketHandler.java:282)
-                	at net.i2p.router.transport.udp.PacketHandler$Handler.handlePacket(PacketHandler.java:231)
-                	at net.i2p.router.transport.udp.PacketHandler$Handler.run(PacketHandler.java:136)
-                	at java.lang.Thread.run(Thread.java:619)
-                	at net.i2p.util.I2PThread.run(I2PThread.java:71)
-                23:50:57.014 ERROR [acket pusher] ter.transport.udp.PacketPusher: SSU Output Queue Error
-                java.lang.RuntimeException: SSU OMS Use after free: Message 2381821417 with 4 fragments of size 0 volleys: 2 lifetime: 1258 pending fragments: 0 1 2 3 
-                	at net.i2p.router.transport.udp.OutboundMessageState.writeFragment(OutboundMessageState.java:298)
-                	at net.i2p.router.transport.udp.PacketBuilder.buildPacket(PacketBuilder.java:170)
-                	at net.i2p.router.transport.udp.OutboundMessageFragments.preparePackets(OutboundMessageFragments.java:332)
-                	at net.i2p.router.transport.udp.OutboundMessageFragments.getNextVolley(OutboundMessageFragments.java:297)
-                	at net.i2p.router.transport.udp.PacketPusher.run(PacketPusher.java:38)
-                	at java.lang.Thread.run(Thread.java:619)
-                	at net.i2p.util.I2PThread.run(I2PThread.java:71)
-            *******/
-            if (_log.shouldLog(Log.WARN))
-                _log.log(Log.WARN, "SSU OMS Use after free: " + toString(), _releasedBy);
-            return -1;
-            //throw new RuntimeException("SSU OMS Use after free: " + toString());
-        }
+    public int writeFragment(byte out[], int outOffset, int fragmentNum) {
         int start = _fragmentSize * fragmentNum;
-        int end = start + fragmentSize(fragmentNum);
-        int toSend = end - start;
-        byte buf[] = _messageBuf.getData();
-        if ( (buf != null) && (start + toSend <= buf.length) && (outOffset + toSend <= out.length) ) {
-            System.arraycopy(buf, start, out, outOffset, toSend);
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Raw fragment[" + fragmentNum + "] for " + _messageId 
-                           + "[" + start + "-" + (start+toSend) + "/" + _totalSize + "/" + _fragmentSize + "]: " 
-                           + Base64.encode(out, outOffset, toSend));
+        int toSend = fragmentSize(fragmentNum);
+        int end = start + toSend;
+        if (end <= _messageBuf.length && outOffset + toSend <= out.length) {
+            System.arraycopy(_messageBuf, start, out, outOffset, toSend);
             return toSend;
-        } else if (buf == null) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Error: null buf");
         } else {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Error: " + start + '/' + end + '/' + outOffset + '/' + out.length);
@@ -452,7 +259,6 @@ class OutboundMessageState implements CDPQEntry {
      */
     public void drop() {
         _peer.getTransport().failed(this, false);
-        releaseResources();
     }
 
     /**
@@ -482,19 +288,18 @@ class OutboundMessageState implements CDPQEntry {
 
     @Override
     public String toString() {
-        short sends[] = _fragmentSends;
         StringBuilder buf = new StringBuilder(256);
-        buf.append("OB Message ").append(_messageId);
-        if (sends != null)
-            buf.append(" with ").append(sends.length).append(" fragments");
-        buf.append(" of size ").append(_totalSize);
+        buf.append("OB Message ").append(_i2npMessage.getUniqueId());
+        buf.append(" with ").append(_numFragments).append(" fragments");
+        buf.append(" of size ").append(_messageBuf.length);
         buf.append(" volleys: ").append(_maxSends);
         buf.append(" lifetime: ").append(getLifetime());
-        if (sends != null) {
+        if (!isComplete()) {
             buf.append(" pending fragments: ");
-            for (int i = 0; i < sends.length; i++)
-                if (sends[i] >= 0)
+            for (int i = 0; i < _numFragments; i++) {
+                if (needsSending(i))
                     buf.append(i).append(' ');
+            }
         }
         return buf.toString();
     }
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
index 053703cb1c2021e6d045ef87114db9d4b7b70d62..feb2b42403a091cdf05ba20532dbcb5c2a34b781 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -13,7 +13,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
+import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
 import net.i2p.util.Addresses;
@@ -130,8 +130,10 @@ class PacketBuilder {
     /** if no extended options or rekey data, which we don't support  = 37 */
     public static final int HEADER_SIZE = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE + 1 + 4;
 
+    /** 4 byte msg ID + 3 byte fragment info */
+    public static final int FRAGMENT_HEADER_SIZE = 7;
     /** not including acks. 46 */
-    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 9;
+    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 2 + FRAGMENT_HEADER_SIZE;
 
     /** IPv4 only */
     public static final int IP_HEADER_SIZE = 20;
@@ -178,6 +180,49 @@ class PacketBuilder {
     }
 ****/
 
+    /**
+     *  Class for passing multiple fragments to buildPacket()
+     *
+     *  @since 0.9.16
+     */
+    public static class Fragment {
+        public final OutboundMessageState state;
+        public final int num;
+
+        public Fragment(OutboundMessageState state, int num) {
+            this.state = state;
+            this.num = num;
+        }
+
+        @Override
+        public String toString() {
+            return "Fragment " + num + " (" + state.fragmentSize(num) + " bytes) of " + state;
+        }
+    }
+
+    /**
+     *  Will a packet to 'peer' that already has 'numFragments' fragments
+     *  totalling 'curDataSize' bytes fit another fragment of size 'newFragSize' ??
+     *
+     *  This doesn't leave anything for acks.
+     *
+     *  @param numFragments >= 1
+     *  @since 0.9.16
+     */
+    public static int getMaxAdditionalFragmentSize(PeerState peer, int numFragments, int curDataSize) {
+        int available = peer.getMTU() - curDataSize;
+        if (peer.isIPv6())
+            available -= MIN_IPV6_DATA_PACKET_OVERHEAD;
+        else
+            available -= MIN_DATA_PACKET_OVERHEAD;
+        // OVERHEAD above includes 1 * FRAGMENT+HEADER_SIZE;
+        // this adds for the others, plus the new one.
+        available -= numFragments * FRAGMENT_HEADER_SIZE;
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("now: " + numFragments + " / " + curDataSize + " avail: " + available);
+        return available;
+    }
+
     /**
      * This builds a data packet (PAYLOAD_TYPE_DATA).
      * See the methods below for the other message types.
@@ -231,37 +276,65 @@ class PacketBuilder {
     public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer,
                                  List<Long> ackIdsRemaining, int newAckCount,
                                  List<ACKBitfield> partialACKsRemaining) {
-        UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
-        DatagramPacket pkt = packet.getPacket();
-        byte data[] = pkt.getData();
-        int off = HEADER_SIZE;
+        List<Fragment> frags = Collections.singletonList(new Fragment(state, fragment));
+        return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining);
+    }
 
+    /*
+     *  Multiple fragments
+     *
+     *  @since 0.9.16
+     */
+    public UDPPacket buildPacket(List<Fragment> fragments, PeerState peer,
+                                 List<Long> ackIdsRemaining, int newAckCount,
+                                 List<ACKBitfield> partialACKsRemaining) {
         StringBuilder msg = null;
         if (_log.shouldLog(Log.INFO)) {
-            msg = new StringBuilder(128);
+            msg = new StringBuilder(256);
             msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64());
-            msg.append(" msg ").append(state.getMessageId()).append(" frag:").append(fragment);
-            msg.append('/').append(state.getFragmentCount());
+        }
+
+        // calculate data size
+        int numFragments = fragments.size();
+        int dataSize = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int fragment = frag.num;
+            int sz = state.fragmentSize(fragment);
+            dataSize += sz;
+            if (msg != null) {
+                msg.append(" Fragment ").append(i);
+                msg.append(": msg ").append(state.getMessageId()).append(' ').append(fragment);
+                msg.append('/').append(state.getFragmentCount());
+                msg.append(' ').append(sz);
+            }
         }
         
-        int dataSize = state.fragmentSize(fragment);
-        if (dataSize < 0) {
-            packet.release();
+        if (dataSize < 0)
             return null;
-        }
 
+        // calculate size available for acks
         int currentMTU = peer.getMTU();
         int availableForAcks = currentMTU - dataSize;
         int ipHeaderSize;
-        if (peer.getRemoteIP().length == 4) {
-            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
-            ipHeaderSize = IP_HEADER_SIZE;
-        } else {
+        if (peer.isIPv6()) {
             availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
             ipHeaderSize = IPV6_HEADER_SIZE;
+        } else {
+            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
+            ipHeaderSize = IP_HEADER_SIZE;
         }
+        if (numFragments > 1)
+            availableForAcks -= (numFragments - 1) * FRAGMENT_HEADER_SIZE;
         int availableForExplicitAcks = availableForAcks;
 
+        // make the packet
+        UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
+        DatagramPacket pkt = packet.getPacket();
+        byte data[] = pkt.getData();
+        int off = HEADER_SIZE;
+
         // ok, now for the body...
         
         // just always ask for an ACK for now...
@@ -276,7 +349,15 @@ class PacketBuilder {
                     break;  // ack count
                 if (bf.receivedComplete())
                     continue;
-                int acksz = 4 + (bf.fragmentCount() / 7) + 1;
+                // only send what we have to
+                //int acksz = 4 + (bf.fragmentCount() / 7) + 1;
+                int bits = bf.highestReceived() + 1;
+                if (bits <= 0)
+                    continue;
+                int acksz = bits / 7;
+                if (bits % 7 > 0)
+                    acksz++;
+                acksz += 4;
                 if (partialAcksToSend == 0)
                     acksz++;  // ack count
                 if (availableForExplicitAcks >= acksz) {
@@ -299,7 +380,7 @@ class PacketBuilder {
         off++;
 
         if (msg != null) {
-            msg.append(" data: ").append(dataSize).append(" bytes, mtu: ")
+            msg.append(" Total data: ").append(dataSize).append(" bytes, mtu: ")
                .append(currentMTU).append(", ")
                .append(newAckCount).append(" new full acks requested, ")
                .append(ackIdsRemaining.size() - newAckCount).append(" resend acks requested, ")
@@ -325,7 +406,7 @@ class PacketBuilder {
                 DataHelper.toLong(data, off, 4, ackId.longValue());
                 off += 4;        
                 if (msg != null) // logging it
-                    msg.append(" full ack: ").append(ackId.longValue());
+                    msg.append(' ').append(ackId.longValue());
             }
             //acksIncluded = true;
         }
@@ -341,10 +422,16 @@ class PacketBuilder {
             for (int i = 0; i < partialAcksToSend && iter.hasNext(); i++) {
                 ACKBitfield bitfield = iter.next();
                 if (bitfield.receivedComplete()) continue;
+                // only send what we have to
+                //int bits = bitfield.fragmentCount();
+                int bits = bitfield.highestReceived() + 1;
+                if (bits <= 0)
+                    continue;
+                int size = bits / 7;
+                if (bits % 7 > 0)
+                    size++;
                 DataHelper.toLong(data, off, 4, bitfield.getMessageId());
                 off += 4;
-                int bits = bitfield.fragmentCount();
-                int size = (bits / 7) + 1;
                 for (int curByte = 0; curByte < size; curByte++) {
                     if (curByte + 1 < size)
                         data[off] |= (byte)(1 << 7);
@@ -357,7 +444,7 @@ class PacketBuilder {
                 }
                 iter.remove();
                 if (msg != null) // logging it
-                    msg.append(" partial ack: ").append(bitfield);
+                    msg.append(' ').append(bitfield).append(" with ack bytes: ").append(size);
             }
             //acksIncluded = true;
             // now jump back and fill in the number of bitfields *actually* included
@@ -367,30 +454,42 @@ class PacketBuilder {
         //if ( (msg != null) && (acksIncluded) )
         //  _log.debug(msg.toString());
         
-        DataHelper.toLong(data, off, 1, 1); // only one fragment in this message
+        DataHelper.toLong(data, off, 1, numFragments);
         off++;
         
-        DataHelper.toLong(data, off, 4, state.getMessageId());
-        off += 4;
+        // now write each fragment
+        int sizeWritten = 0;
+        for (int i = 0; i < numFragments; i++) {
+            Fragment frag = fragments.get(i);
+            OutboundMessageState state = frag.state;
+            int fragment = frag.num;
+
+            DataHelper.toLong(data, off, 4, state.getMessageId());
+            off += 4;
         
-        data[off] |= fragment << 1;
-        if (fragment == state.getFragmentCount() - 1)
-            data[off] |= 1; // isLast
-        off++;
+            data[off] |= fragment << 1;
+            if (fragment == state.getFragmentCount() - 1)
+                data[off] |= 1; // isLast
+            off++;
         
-        DataHelper.toLong(data, off, 2, dataSize);
-        data[off] &= (byte)0x3F; // 2 highest bits are reserved
-        off += 2;
+            int fragSize = state.fragmentSize(fragment);
+            DataHelper.toLong(data, off, 2, fragSize);
+            data[off] &= (byte)0x3F; // 2 highest bits are reserved
+            off += 2;
         
-        int sizeWritten = state.writeFragment(data, off, fragment);
+            int sz = state.writeFragment(data, off, fragment);
+            off += sz;
+            sizeWritten += sz;
+        }
+
         if (sizeWritten != dataSize) {
             if (sizeWritten < 0) {
                 // probably already freed from OutboundMessageState
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("Write failed for fragment " + fragment + " of " + state.getMessageId());
+                    _log.warn("Write failed for " + DataHelper.toString(fragments));
             } else {
-                _log.error("Size written: " + sizeWritten + " but size: " + dataSize 
-                           + " for fragment " + fragment + " of " + state.getMessageId());
+                _log.error("Size written: " + sizeWritten + " but size: " + dataSize +
+                           " for " + DataHelper.toString(fragments));
             }
             packet.release();
             return null;
@@ -398,31 +497,44 @@ class PacketBuilder {
         //    _log.debug("Size written: " + sizeWritten + " for fragment " + fragment 
         //               + " of " + state.getMessageId());
         }
+
         // put this after writeFragment() since dataSize will be zero for use-after-free
         if (dataSize == 0) {
             // OK according to the protocol but if we send it, it's a bug
-            _log.error("Sending zero-size fragment " + fragment + " of " + state + " for " + peer);
+            _log.error("Sending zero-size fragment??? for " + DataHelper.toString(fragments));
         }
-        off += dataSize;
-
         
         // pad up so we're on the encryption boundary
         off = pad1(data, off);
         off = pad2(data, off, currentMTU - (ipHeaderSize + UDP_HEADER_SIZE));
         pkt.setLength(off);
 
-        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
-        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
-        
-        if (_log.shouldLog(Log.INFO)) {
+        if (msg != null) {
+            // verify multi-fragment packet
+            //if (numFragments > 1) {
+            //    msg.append("\nDataReader dump\n:");
+            //    UDPPacketReader reader = new UDPPacketReader(_context);
+            //    reader.initialize(packet);
+            //    UDPPacketReader.DataReader dreader = reader.getDataReader();
+            //    try {
+            //        msg.append(dreader.toString());
+            //    } catch (Exception e) {
+            //        _log.info("blowup, dump follows", e);
+            //        msg.append('\n');
+            //        msg.append(net.i2p.util.HexDump.dump(data, 0, off));
+            //    }
+            //}
             msg.append(" pkt size ").append(off + (ipHeaderSize + UDP_HEADER_SIZE));
             _log.info(msg.toString());
         }
+
+        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
+        
         // the packet could have been built before the current mtu got lowered, so
         // compare to LARGE_MTU
         if (off + (ipHeaderSize + UDP_HEADER_SIZE) > PeerState.LARGE_MTU) {
             _log.error("Size is " + off + " for " + packet +
-                       " fragment " + fragment +
                        " data size " + dataSize +
                        " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) +
                        " MTU " + currentMTU +
@@ -430,7 +542,7 @@ class PacketBuilder {
                        availableForExplicitAcks + " for full acks " + 
                        explicitToSend + " full acks included " +
                        partialAcksToSend + " partial acks included " +
-                       " OMS " + state, new Exception());
+                       " Fragments: " + DataHelper.toString(fragments), new Exception());
         }
         
         return packet;
@@ -509,11 +621,16 @@ class PacketBuilder {
             off++;
             for (int i = 0; i < ackBitfields.size(); i++) {
                 ACKBitfield bitfield = ackBitfields.get(i);
-                if (bitfield.receivedComplete()) continue;
+                // no, this will corrupt the packet
+                //if (bitfield.receivedComplete()) continue;
                 DataHelper.toLong(data, off, 4, bitfield.getMessageId());
                 off += 4;
-                int bits = bitfield.fragmentCount();
-                int size = (bits / 7) + 1;
+                // only send what we have to
+                //int bits = bitfield.fragmentCount();
+                int bits = bitfield.highestReceived() + 1;
+                int size = bits / 7;
+                if (bits == 0 || bits % 7 > 0)
+                    size++;
                 for (int curByte = 0; curByte < size; curByte++) {
                     if (curByte + 1 < size)
                         data[off] |= (byte)(1 << 7);
@@ -526,7 +643,7 @@ class PacketBuilder {
                 }
                 
                 if (msg != null) // logging it
-                    msg.append(" partial ack: ").append(bitfield);
+                    msg.append(" partial ack: ").append(bitfield).append(" with ack bytes: ").append(size);
             }
         }
         
@@ -596,14 +713,22 @@ class PacketBuilder {
         off += 4;
         DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
         off += 4;
-        System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
-        off += Signature.SIGNATURE_BYTES;
-        // ok, we need another 8 bytes of random padding
-        // (ok, this only gives us 63 bits, not 64)
-        long l = _context.random().nextLong();
-        if (l < 0) l = 0 - l;
-        DataHelper.toLong(data, off, 8, l);
-        off += 8;
+
+        // handle variable signature size
+        Signature sig = state.getSentSignature();
+        int siglen = sig.length();
+        System.arraycopy(sig.getData(), 0, data, off, siglen);
+        off += siglen;
+        // ok, we need another few bytes of random padding
+        int rem = siglen % 16;
+        int padding;
+        if (rem > 0) {
+            padding = 16 - rem;
+            _context.random().nextBytes(data, off, padding);
+            off += padding;
+        } else {
+            padding = 0;
+        }
         
         if (_log.shouldLog(Log.DEBUG)) {
             StringBuilder buf = new StringBuilder(128);
@@ -612,9 +737,9 @@ class PacketBuilder {
             buf.append(" Bob: ").append(Addresses.toString(state.getReceivedOurIP(), externalPort));
             buf.append(" RelayTag: ").append(state.getSentRelayTag());
             buf.append(" SignedOn: ").append(state.getSentSignedOnTime());
-            buf.append(" signature: ").append(Base64.encode(state.getSentSignature().getData()));
+            buf.append(" signature: ").append(Base64.encode(sig.getData()));
             buf.append("\nRawCreated: ").append(Base64.encode(data, 0, off)); 
-            buf.append("\nsignedTime: ").append(Base64.encode(data, off-8-Signature.SIGNATURE_BYTES-4, 4));
+            buf.append("\nsignedTime: ").append(Base64.encode(data, off - padding - siglen - 4, 4));
             _log.debug(buf.toString());
         }
         
@@ -623,7 +748,7 @@ class PacketBuilder {
         byte[] iv = SimpleByteCache.acquire(UDPPacket.IV_SIZE);
         _context.random().nextBytes(iv);
         
-        int encrWrite = Signature.SIGNATURE_BYTES + 8;
+        int encrWrite = siglen + padding;
         int sigBegin = off - encrWrite;
         _context.aes().encrypt(data, sigBegin, data, sigBegin, state.getCipherKey(), iv, encrWrite);
         
@@ -774,8 +899,11 @@ class PacketBuilder {
             DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
             off += 4;
             
+            // handle variable signature size
             // we need to pad this so we're at the encryption boundary
-            int mod = (off + Signature.SIGNATURE_BYTES) & 0x0f;
+            Signature sig = state.getSentSignature();
+            int siglen = sig.length();
+            int mod = (off + siglen) & 0x0f;
             if (mod != 0) {
                 int paddingRequired = 16 - mod;
                 // add an arbitrary number of 16byte pad blocks too ???
@@ -787,8 +915,8 @@ class PacketBuilder {
             // so trailing non-mod-16 data is ignored. That truncates the sig.
             
             // BUG: NPE here if null signature
-            System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
-            off += Signature.SIGNATURE_BYTES;
+            System.arraycopy(sig.getData(), 0, data, off, siglen);
+            off += siglen;
         } else {
             // We never get here (see above)
 
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
index 8ad73330e47858314452db2ee4e5eaccec407729..206e9c44f7aea9964f813d7fcd9b7ba0c8ae73d4 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -8,6 +8,7 @@ import java.util.concurrent.BlockingQueue;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.util.CoDelBlockingQueue;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.util.I2PThread;
 import net.i2p.util.LHMCache;
@@ -691,33 +692,35 @@ class PacketHandler {
                         state = _establisher.receiveData(outState);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug("Received new DATA packet from " + state + ": " + packet);
+                    UDPPacketReader.DataReader dr = reader.getDataReader();
                     if (state != null) {
-                        UDPPacketReader.DataReader dr = reader.getDataReader();
                         if (_log.shouldLog(Log.DEBUG)) {
                             StringBuilder msg = new StringBuilder(512);
                             msg.append("Receive ").append(System.identityHashCode(packet));
                             msg.append(" from ").append(state.getRemotePeer().toBase64()).append(" ").append(state.getRemoteHostId());
-                            for (int i = 0; i < dr.readFragmentCount(); i++) {
-                                msg.append(" msg ").append(dr.readMessageId(i));
-                                msg.append(":").append(dr.readMessageFragmentNum(i));
-                                if (dr.readMessageIsLast(i))
-                                    msg.append("*");
-                            }
+                            try {
+                                int count = dr.readFragmentCount();
+                                for (int i = 0; i < count; i++) {
+                                    msg.append(" msg ").append(dr.readMessageId(i));
+                                    msg.append(":").append(dr.readMessageFragmentNum(i));
+                                    if (dr.readMessageIsLast(i))
+                                        msg.append("*");
+                                }
+                            } catch (DataFormatException dfe) {}
                             msg.append(": ").append(dr.toString());
                             _log.debug(msg.toString());
                         }
                         //packet.beforeReceiveFragments();
                         _inbound.receiveData(state, dr);
                         _context.statManager().addRateData("udp.receivePacketSize.dataKnown", packet.getPacket().getLength(), packet.getLifetime());
-                        if (dr.readFragmentCount() <= 0)
-                            _context.statManager().addRateData("udp.receivePacketSize.dataKnownAck", packet.getPacket().getLength(), packet.getLifetime());
                     } else {
                         // doesn't happen
                         _context.statManager().addRateData("udp.receivePacketSize.dataUnknown", packet.getPacket().getLength(), packet.getLifetime());
-                        UDPPacketReader.DataReader dr = reader.getDataReader();
+                    }
+                    try {
                         if (dr.readFragmentCount() <= 0)
                             _context.statManager().addRateData("udp.receivePacketSize.dataUnknownAck", packet.getPacket().getLength(), packet.getLifetime());
-                    }
+                    } catch (DataFormatException dfe) {}
                     break;
                 case UDPPacket.PAYLOAD_TYPE_TEST:
                     _state = 51;
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
index db7fb4ea34f400f6b1a0b2bbea8a4ddd1a82bc96..f28ee96450360ba618761ccf55d79563dba01313 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
@@ -37,11 +37,10 @@ class PacketPusher implements Runnable {
     public void run() {
         while (_alive) {
             try {
-                UDPPacket packets[] = _fragments.getNextVolley();
+                List<UDPPacket> packets = _fragments.getNextVolley();
                 if (packets != null) {
-                    for (int i = 0; i < packets.length; i++) {
-                        if (packets[i] != null) // null for ACKed fragments
-                            send(packets[i]);
+                    for (int i = 0; i < packets.size(); i++) {
+                         send(packets.get(i));
                     }
                 }
             } catch (Exception e) {
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
index 06a440ada5facda25868a0715eb43f11dc887dbe..22a92f22eb595a82c381886e5bca642e9cb25e0c 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -242,6 +242,9 @@ class PeerState {
     private static final int MINIMUM_WINDOW_BYTES = DEFAULT_SEND_WINDOW_BYTES;
     private static final int MAX_SEND_WINDOW_BYTES = 1024*1024;
 
+    /** max number of msgs returned from allocateSend() */
+    private static final int MAX_ALLOCATE_SEND = 2;
+
     /**
      *  Was 32 before 0.9.2, but since the streaming lib goes up to 128,
      *  we would just drop our own msgs right away during slow start.
@@ -1032,7 +1035,7 @@ class PeerState {
      *            no full bitfields are included.
      */
     void fetchPartialACKs(List<ACKBitfield> rv) {
-        InboundMessageState states[] = null;
+        List<InboundMessageState> states = null;
         int curState = 0;
         synchronized (_inboundMessages) {
             int numMessages = _inboundMessages.size();
@@ -1049,17 +1052,17 @@ class PeerState {
                 } else {
                     if (!state.isComplete()) {
                         if (states == null)
-                            states = new InboundMessageState[numMessages];
-                        states[curState++] = state;
+                            states = new ArrayList<InboundMessageState>(numMessages);
+                        states.add(state);
                     }
                 }
             }
         }
         if (states != null) {
-            // _inboundMessages is a Map (unordered), so why bother going backwards?
-            for (int i = curState-1; i >= 0; i--) {
-                if (states[i] != null)
-                    rv.add(states[i].createACKBitfield());
+            for (InboundMessageState ims : states) {
+                ACKBitfield abf = ims.createACKBitfield();
+                if (!abf.receivedComplete())
+                    rv.add(abf);
             }
         }
     }
@@ -1072,7 +1075,9 @@ class PeerState {
 
         public FullACKBitfield(long id) { _msgId = id; }
 
-        public int fragmentCount() { return 0; }
+        public int fragmentCount() { return 1; }
+        public int ackCount() { return 1; }
+        public int highestReceived() { return 0; }
         public long getMessageId() { return _msgId; }
         public boolean received(int fragmentNum) { return true; }
         public boolean receivedComplete() { return true; }
@@ -1084,7 +1089,7 @@ class PeerState {
             return _msgId == ((ACKBitfield)o).getMessageId();
         }
         @Override
-        public String toString() { return "Full ACK of " + _msgId; }
+        public String toString() { return "Full ACK " + _msgId; }
     }
         
     /**
@@ -1538,7 +1543,6 @@ class PeerState {
         for (int i = 0; succeeded != null && i < succeeded.size(); i++) {
             OutboundMessageState state = succeeded.get(i);
             _transport.succeeded(state);
-            state.releaseResources();
             OutNetMessage msg = state.getMessage();
             if (msg != null)
                 msg.timestamp("sending complete");
@@ -1556,22 +1560,22 @@ class PeerState {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Unable to send a direct message: " + state);
             }
-            state.releaseResources();
         }
         
         return rv + _outboundQueue.size();
     }
     
     /**
-     * Pick a message we want to send and allocate it out of our window
+     * Pick one or more messages we want to send and allocate them out of our window
      * High usage -
      * OutboundMessageFragments.getNextVolley() calls this 2nd, if finishMessages() returned > 0.
      * TODO combine finishMessages(), allocateSend(), and getNextDelay() so we don't iterate 3 times.
      *
-     * @return allocated message to send, or null if no messages or no resources
+     * @return allocated messages to send (never empty), or null if no messages or no resources
      */
-    public OutboundMessageState allocateSend() {
+    public List<OutboundMessageState> allocateSend() {
         if (_dead) return null;
+        List<OutboundMessageState> rv = null;
         synchronized (_outboundMessages) {
             for (OutboundMessageState state : _outboundMessages) {
                 // We have 3 return values, because if allocateSendingBytes() returns false,
@@ -1588,44 +1592,54 @@ class PeerState {
                             msg.timestamp("not reached for allocation " + msgs.size() + " other peers");
                     }
                      */
-                    return state;
+                    if (rv == null)
+                        rv = new ArrayList<OutboundMessageState>(MAX_ALLOCATE_SEND);
+                    rv.add(state);
+                    if (rv.size() >= MAX_ALLOCATE_SEND)
+                        return rv;
                 } else if (should == ShouldSend.NO_BW) {
                     // no more bandwidth available
                     // we don't bother looking for a smaller msg that would fit.
                     // By not looking further, we keep strict sending order, and that allows
                     // some efficiency in acked() below.
-                    if (_log.shouldLog(Log.DEBUG))
+                    if (rv == null && _log.shouldLog(Log.DEBUG))
                         _log.debug("Nothing to send (BW) to " + _remotePeer + ", with " + _outboundMessages.size() +
                                    " / " + _outboundQueue.size() + " remaining");
-                    return null;
+                    return rv;
                 } /* else {
                     OutNetMessage msg = state.getMessage();
                     if (msg != null)
                         msg.timestamp("passed over for allocation with " + msgs.size() + " peers");
                 } */
             }
+
             // Peek at head of _outboundQueue and see if we can send it.
             // If so, pull it off, put it in _outbundMessages, test
             // again for bandwidth if necessary, and return it.
-            OutboundMessageState state = _outboundQueue.peek();
-            if (state != null && ShouldSend.YES == locked_shouldSend(state)) {
+            OutboundMessageState state;
+            while ((state = _outboundQueue.peek()) != null &&
+                   ShouldSend.YES == locked_shouldSend(state)) {
                 // we could get a different state, or null, when we poll,
                 // due to AQM drops, so we test again if necessary
                 OutboundMessageState dequeuedState = _outboundQueue.poll();
                 if (dequeuedState != null) {
                     _outboundMessages.add(dequeuedState);
-                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(dequeuedState)) {
+                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(state)) {
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug("Allocate sending (NEW) to " + _remotePeer + ": " + dequeuedState.getMessageId());
-                        return dequeuedState;
+                        if (rv == null)
+                            rv = new ArrayList<OutboundMessageState>(MAX_ALLOCATE_SEND);
+                        rv.add(state);
+                        if (rv.size() >= MAX_ALLOCATE_SEND)
+                            return rv;
                     }
                 }
             }
         }
-        if (_log.shouldLog(Log.DEBUG))
+        if ( rv == null && _log.shouldLog(Log.DEBUG))
             _log.debug("Nothing to send to " + _remotePeer + ", with " + _outboundMessages.size() +
                        " / " + _outboundQueue.size() + " remaining");
-        return null;
+        return rv;
     }
     
     /**
@@ -1694,9 +1708,9 @@ class PeerState {
      *  how much payload data can we shove in there?
      *  @return MTU - 87, i.e. 533 or 1397 (IPv4), MTU - 107 (IPv6)
      */
-    private int fragmentSize() {
+    public int fragmentSize() {
         // 46 + 20 + 8 + 13 = 74 + 13 = 87 (IPv4)
-        // 46 + 40 + 8 + 13 = 74 + 13 = 107 (IPv6)
+        // 46 + 40 + 8 + 13 = 94 + 13 = 107 (IPv6)
         return _mtu -
                (_remoteIP.length == 4 ? PacketBuilder.MIN_DATA_PACKET_OVERHEAD : PacketBuilder.MIN_IPV6_DATA_PACKET_OVERHEAD) -
                MIN_ACK_SIZE;
@@ -1713,16 +1727,6 @@ class PeerState {
     private ShouldSend locked_shouldSend(OutboundMessageState state) {
         long now = _context.clock().now();
         if (state.getNextSendTime() <= now) {
-            if (!state.isFragmented()) {
-                state.fragment(fragmentSize());
-                if (state.getMessage() != null)
-                    state.getMessage().timestamp("fragment into " + state.getFragmentCount());
-
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("Fragmenting " + state);
-            }
-
-            
             OutboundMessageState retrans = _retransmitter;
             if ( (retrans != null) && ( (retrans.isExpired() || retrans.isComplete()) ) ) {
                 _retransmitter = null;
@@ -1844,7 +1848,6 @@ class PeerState {
             //if (getSendWindowBytesRemaining() > 0)
             //    _throttle.unchoke(peer.getRemotePeer());
             
-            state.releaseResources();
         } else {
             // dupack, likely
             //if (_log.shouldLog(Log.DEBUG))
@@ -1894,12 +1897,7 @@ class PeerState {
         if (state != null) {
             int numSends = state.getMaxSends();
                         
-            int bits = bitfield.fragmentCount();
-            int numACKed = 0;
-            for (int i = 0; i < bits; i++)
-                if (bitfield.received(i))
-                    numACKed++;
-            
+            int numACKed = bitfield.ackCount();
             _context.statManager().addRateData("udp.partialACKReceived", numACKed);
             
             if (_log.shouldLog(Log.INFO))
@@ -1921,7 +1919,6 @@ class PeerState {
                 //if (state.getPeer().getSendWindowBytesRemaining() > 0)
                 //    _throttle.unchoke(state.getPeer().getRemotePeer());
 
-                state.releaseResources();
             } else {
                 //if (state.getMessage() != null)
                 //    state.getMessage().timestamp("partial ack after " + numSends + ": " + bitfield.toString());
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index 48a5e082256ffafdf0cec0505b33b5660116a549..bc47fba87c793c2d9e7509e7a8adf971400bbea1 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -9,8 +9,8 @@ import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
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 f05205582d4eb43df5309dc7fa4354d84ee4e3bd..88551d588c029612ca498fab1ea2133acb6d63cd 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
@@ -5,7 +5,7 @@ import java.net.UnknownHostException;
 import java.util.Map;
 
 import net.i2p.data.Base64;
-import net.i2p.data.RouterAddress;
+import net.i2p.data.router.RouterAddress;
 import net.i2p.data.SessionKey;
 import net.i2p.util.LHMCache;
 import net.i2p.util.SystemVersion;
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
index 252490cfc666cad5fcbdf4c10bbafbeb1b268e51..1f5f65a6a7c78d2b22657e7e9688551ff27df820 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
@@ -166,7 +166,11 @@ class UDPPacket implements CDQEntry {
     int getMessageType() { return _messageType; }
     /** only for debugging and stats, does not go on the wire */
     void setMessageType(int type) { _messageType = type; }
+
+    /** only for debugging and stats */
     int getFragmentCount() { return _fragmentCount; }
+
+    /** only for debugging and stats */
     void setFragmentCount(int count) { _fragmentCount = count; }
 
     RemoteHostId getRemoteHost() {
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
index affaf09df8cd4655c33cb8fd75d7ee84068f590b..1f61cb197a27824fd62ceb32989c03c1de230ed0 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
@@ -2,6 +2,7 @@ package net.i2p.router.transport.udp;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.SessionKey;
 import net.i2p.data.Signature;
@@ -12,6 +13,9 @@ import net.i2p.util.Log;
  * the appropriate fields.  If the interesting bits are in message specific
  * elements, grab the appropriate subreader.
  *
+ * Many of the methods here and in the subclasses will throw AIOOBE on
+ * malformed packets, that should be caught also.
+ *
  */
 class UDPPacketReader {
     private final I2PAppContext _context;
@@ -203,9 +207,10 @@ class UDPPacketReader {
             return rv;
         }
         
-        public void readEncryptedSignature(byte target[], int targetOffset) {
+        /** @param size the amount to be copied, including padding to mod 16 */
+        public void readEncryptedSignature(byte target[], int targetOffset, int size) {
             int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4 + 4;
-            System.arraycopy(_message, offset, target, targetOffset, Signature.SIGNATURE_BYTES + 8);
+            System.arraycopy(_message, offset, target, targetOffset, size);
         }
         
         public void readIV(byte target[], int targetOffset) {
@@ -239,7 +244,11 @@ class UDPPacketReader {
             System.arraycopy(_message, readOffset, target, targetOffset, len);
         }
         
-        /** read the time at which the signature was generated */
+        /**
+         *  Read the time at which the signature was generated.
+         *  TODO must be completely in final fragment.
+         *  Time and sig cannot be split across fragments.
+         */
         public long readFinalFragmentSignedOnTime() {
             if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                 throw new IllegalStateException("This is not the final fragment");
@@ -247,12 +256,19 @@ class UDPPacketReader {
             return DataHelper.fromLong(_message, readOffset, 4);
         }
         
-        /** read the signature from the final sessionConfirmed packet */
-        public void readFinalSignature(byte target[], int targetOffset) {
+        /**
+         *  Read the signature from the final sessionConfirmed packet.
+         *  TODO must be completely in final fragment.
+         *  Time and sig cannot be split across fragments.
+         *  @param size not including padding
+         */
+        public void readFinalSignature(byte target[], int targetOffset, int size) {
             if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                 throw new IllegalStateException("This is not the final fragment");
-            int readOffset = _payloadBeginOffset + _payloadLength - Signature.SIGNATURE_BYTES;
-            System.arraycopy(_message, readOffset, target, targetOffset, Signature.SIGNATURE_BYTES);
+            int readOffset = _payloadBeginOffset + _payloadLength - size;
+            if (readOffset < readBodyOffset() + (1 + 2 + 4))
+                throw new IllegalStateException("Sig split across fragments");
+            System.arraycopy(_message, readOffset, target, targetOffset, size);
         }
     }
     
@@ -267,26 +283,33 @@ class UDPPacketReader {
         public boolean readACKsIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_ACK);
         }
+
         public boolean readACKBitfieldsIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_ACK_BITFIELDS);
         }
+
         public boolean readECN() {
             return flagSet(UDPPacket.DATA_FLAG_ECN);
         }
+
         public boolean readWantPreviousACKs() {
             return flagSet(UDPPacket.DATA_FLAG_WANT_ACKS);
         }
+
         public boolean readReplyRequested() { 
             return flagSet(UDPPacket.DATA_FLAG_WANT_REPLY);
         }
+
         public boolean readExtendedDataIncluded() {
             return flagSet(UDPPacket.DATA_FLAG_EXTENDED);
         }
+
         public int readACKCount() {
             if (!readACKsIncluded()) return 0;
             int off = readBodyOffset() + 1;
             return (int)DataHelper.fromLong(_message, off, 1);
         }
+
         public long readACK(int index) {
             if (!readACKsIncluded()) return -1;
             int off = readBodyOffset() + 1;
@@ -294,7 +317,8 @@ class UDPPacketReader {
             off++;
             return DataHelper.fromLong(_message, off + (4 * index), 4);
         }
-        public ACKBitfield[] readACKBitfields() {
+
+        public ACKBitfield[] readACKBitfields() throws DataFormatException {
             if (!readACKBitfieldsIncluded()) return null;
             int off = readBodyOffset() + 1;
             if (readACKsIncluded()) {
@@ -314,7 +338,7 @@ class UDPPacketReader {
             return rv;
         }
         
-        public int readFragmentCount() {
+        public int readFragmentCount() throws DataFormatException {
             int off = readBodyOffset() + 1;
             if (readACKsIncluded()) {
                 int numACKs = (int)DataHelper.fromLong(_message, off, 1);
@@ -338,38 +362,39 @@ class UDPPacketReader {
             return _message[off];
         }
         
-        public long readMessageId(int fragmentNum) {
+        public long readMessageId(int fragmentNum) throws DataFormatException {
             int fragmentBegin = getFragmentBegin(fragmentNum);
             return DataHelper.fromLong(_message, fragmentBegin, 4);
         }
-        public int readMessageFragmentNum(int fragmentNum) {
+
+        public int readMessageFragmentNum(int fragmentNum) throws DataFormatException {
             int off = getFragmentBegin(fragmentNum);
             off += 4; // messageId
             return (_message[off] & 0xFF) >>> 1;
         }
-        public boolean readMessageIsLast(int fragmentNum) {
+
+        public boolean readMessageIsLast(int fragmentNum) throws DataFormatException {
             int off = getFragmentBegin(fragmentNum);
             off += 4; // messageId
             return ((_message[off] & 1) != 0);
         }
-        public int readMessageFragmentSize(int fragmentNum) {
+
+        public int readMessageFragmentSize(int fragmentNum) throws DataFormatException {
             int off = getFragmentBegin(fragmentNum);
-            off += 4; // messageId
-            off++; // fragment info
+            off += 5; // messageId + fragment info
             return ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
         }
 
         public void readMessageFragment(int fragmentNum, byte target[], int targetOffset)
-                                                      throws ArrayIndexOutOfBoundsException {
+                                                      throws DataFormatException {
             int off = getFragmentBegin(fragmentNum);
-            off += 4; // messageId
-            off++; // fragment info
+            off += 5; // messageId + fragment info
             int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
             off += 2;
             System.arraycopy(_message, off, target, targetOffset, size);
         }
         
-        private int getFragmentBegin(int fragmentNum) {
+        private int getFragmentBegin(int fragmentNum) throws DataFormatException {
             int off = readBodyOffset() + 1;
             if (readACKsIncluded()) {
                 int numACKs = (int)DataHelper.fromLong(_message, off, 1);
@@ -393,16 +418,14 @@ class UDPPacketReader {
             }
             off++; // # fragments
             
-            if (fragmentNum == 0) {
-                return off;
-            } else {
+            if (fragmentNum > 0) {
                 for (int i = 0; i < fragmentNum; i++) {
                     off += 5; // messageId+info
                     off += ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
                     off += 2;
                 }
-                return off;
             }
+            return off;
         }
 
         private boolean flagSet(byte flag) {
@@ -433,10 +456,15 @@ class UDPPacketReader {
                 off++;
                 buf.append("with partial ACKs for ");
 
-                for (int i = 0; i < numBitfields; i++) {
-                    PacketACKBitfield bf = new PacketACKBitfield(off);
-                    buf.append(bf.getMessageId()).append(' ');
-                    off += bf.getByteLength();
+                try {
+                    for (int i = 0; i < numBitfields; i++) {
+                        PacketACKBitfield bf = new PacketACKBitfield(off);
+                        buf.append(bf.getMessageId()).append(' ');
+                        off += bf.getByteLength();
+                    }
+                } catch (DataFormatException dfe) {
+                    buf.append("CORRUPT");
+                    return buf.toString();
                 }
             }
             if (readExtendedDataIncluded()) {
@@ -465,16 +493,15 @@ class UDPPacketReader {
                 buf.append(" isLast? ").append(isLast);
                 buf.append(" info ").append(_message[off-1]);
                 int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
-                buf.append(" with ").append(size).append(" bytes");
-                buf.append(' ');
-                off += size;
                 off += 2;
+                buf.append(" with ").append(size).append(" bytes; ");
+                off += size;
             }
             
             return buf.toString();
         }
         
-        public void toRawString(StringBuilder buf) { 
+        public void toRawString(StringBuilder buf) throws DataFormatException { 
             UDPPacketReader.this.toRawString(buf); 
             buf.append(" payload: ");
                   
@@ -495,14 +522,19 @@ class UDPPacketReader {
         private final int _bitfieldStart;
         private final int _bitfieldSize;
 
-        public PacketACKBitfield(int start) {
+        public PacketACKBitfield(int start) throws DataFormatException {
             _start = start;
             _bitfieldStart = start + 4;
             int bfsz = 1;
             // bitfield is an array of bytes where the high bit is 1 if 
             // further bytes in the bitfield follow
-            while ((_message[_bitfieldStart + bfsz - 1] & UDPPacket.BITFIELD_CONTINUATION) != 0x0)
+            while ((_message[_bitfieldStart + bfsz - 1] & UDPPacket.BITFIELD_CONTINUATION) != 0x0) {
                 bfsz++;
+                //if (bfsz > InboundMessageState.MAX_PARTIAL_BITFIELD_BYTES)
+                //    throw new DataFormatException();
+            }
+            if (bfsz > InboundMessageState.MAX_PARTIAL_BITFIELD_BYTES)
+                throw new DataFormatException("bitfield size: " + bfsz);
             _bitfieldSize = bfsz;
         }
 
@@ -511,6 +543,46 @@ class UDPPacketReader {
         public int fragmentCount() { return _bitfieldSize * 7; }
         public boolean receivedComplete() { return false; }
 
+        /**
+         *  Number of fragments acked in this bitfield.
+         *  Faster than looping through received()
+         *  @since 0.9.16
+         */
+        public int ackCount() {
+            int rv = 0;
+            for (int i = _bitfieldStart; i < _bitfieldStart + _bitfieldSize; i++) {
+                byte b = _message[i];
+                if ((b & 0x7f) != 0) {
+                    for (int j = 0; j < 7; j++) {
+                        if ((b & 0x01) != 0)
+                            rv++;
+                        b >>= 1;
+                    }
+                }
+            }
+            return rv;
+        }
+
+        /**
+         *  Highest fragment number acked in this bitfield.
+         *  @return highest fragment number acked, or -1 if none
+         *  @since 0.9.16
+         */
+        public int highestReceived() {
+            int count = fragmentCount();
+            for (int i = _bitfieldSize - 1; i >= 0; i--) {
+                byte b = _message[_bitfieldStart + i];
+                if ((b & 0x7f) == 0)
+                    continue;
+                for (int j = 6; j >= 0; j--) {
+                    if ((b & 0x40) != 0)
+                        return (7 * i) + j;
+                    b <<= 1;
+                }
+            }
+            return -1;
+        }
+
         public boolean received(int fragmentNum) {
             if ( (fragmentNum < 0) || (fragmentNum >= _bitfieldSize*7) )
                 return false;
@@ -523,16 +595,17 @@ class UDPPacketReader {
         @Override
         public String toString() { 
             StringBuilder buf = new StringBuilder(64);
-            buf.append("Read partial ACK of ");
+            buf.append("IB Partial ACK of ");
             buf.append(getMessageId());
-            buf.append(" with ACKs for: ");
+            buf.append(" highest: ").append(highestReceived());
+            buf.append(" with ACKs for: [");
             int numFrags = fragmentCount();
             for (int i = 0; i < numFrags; i++) {
-                if (received(i))
-                    buf.append(i).append(" ");
-                else
-                    buf.append('!').append(i).append(" ");
+                if (!received(i))
+                    buf.append('!');
+                buf.append(i).append(' ');
             }
+            buf.append("] / ").append(numFrags);
             return buf.toString();
         }
     }
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index b9c2ad80241662b04a07e6561e46db56e3777b9a..63fc9576000bb0108eaec71d6105e5de9754fadc 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -25,9 +25,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import net.i2p.data.DatabaseEntry;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterAddress;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.I2NPMessage;
@@ -1550,6 +1550,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 return null;
             }
 
+            // Check for supported sig type
+            if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
+                markUnreachable(to);
+                return null;
+            }
+
             if (!allowConnection())
                 return _cachedBid[TRANSIENT_FAIL_BID];
 
diff --git a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
index 3fa40c984e32432483f974bc375bc69d353770b6..bb18c44b229a2f3a0f52c6bfeb32393d9169c9f0 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.TunnelDataMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
index ec3529b48f79f38b699c194acdd26f0ae70a215c..660aedab81d93e06dc02088d96d57c8d1e9e7aa2 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
@@ -4,7 +4,7 @@ import net.i2p.data.DatabaseEntry;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.Payload;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DataMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
index 364c5970ab12a850e0e5e26cf1b8ae6e02d580ab..3961af80fc5ffef73481dafa687146126356e4ec 100644
--- a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
@@ -4,7 +4,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.TunnelGatewayMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
index 03923ecfeb71db4fc9107d0d7247e9a94cec090b..4aef149028e1dfa9ac37f1846d80625620ff1443 100644
--- a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
+++ b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.i2np.TunnelDataMessage;
 import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
index b53d8a6430291d8d9892285477b44e38f0dba346..d5806027932d300e56340678637475a3e9b4341c 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
@@ -1,7 +1,7 @@
 package net.i2p.router.tunnel;
 
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.TunnelDataMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
index b771589a1f7474f0d408be4a5b52571007bcc039..42cff28c7a3dbb9b37a7d9b91660e110719b89b9 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
@@ -10,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
 import net.i2p.data.i2np.I2NPMessage;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelManagerFacade;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index afe3940bff989a407f7f0952afa6e4b57d4fc5d5..0f914fbcc00575cc94f51f6a2d90c7c6fcad4f53 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -9,8 +9,8 @@ import net.i2p.data.Base64;
 import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterIdentity;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.BuildRequestRecord;
 import net.i2p.data.i2np.BuildResponseRecord;
@@ -85,6 +85,11 @@ class BuildHandler implements Runnable {
      */
     private static final int NEXT_HOP_SEND_TIMEOUT = 25*1000;
 
+    private static final long MAX_REQUEST_FUTURE = 5*60*1000;
+    /** must be > 1 hour due to rouding down */
+    private static final long MAX_REQUEST_AGE = 65*60*1000;
+
+
     public BuildHandler(RouterContext ctx, TunnelPoolManager manager, BuildExecutor exec) {
         _context = ctx;
         _log = ctx.logManager().getLog(getClass());
@@ -101,8 +106,10 @@ class BuildHandler implements Runnable {
         _context.statManager().createRateStat("tunnel.reject.50", "How often we reject a tunnel because of a critical issue (shutdown, etc)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
 
         _context.statManager().createRequiredRateStat("tunnel.decryptRequestTime", "Time to decrypt a build request (ms)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTooOld", "Reject tunnel count (too old)", "Tunnels", new long[] { 3*60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectFuture", "Reject tunnel count (time in future)", "Tunnels", new long[] { 3*60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.rejectDupID", "Part. tunnel dup ID", "Tunnels", new long[] { 24*60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.ownDupID", "Our tunnel dup. ID", "Tunnels", new long[] { 24*60*60*1000 });
         _context.statManager().createRequiredRateStat("tunnel.rejectHostile", "Reject malicious tunnel", "Tunnels", new long[] { 24*60*60*1000 });
@@ -587,11 +594,24 @@ class BuildHandler implements Runnable {
             }
         }
 
-        // time is in hours, and only for log below - what's the point?
-        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it is not.
+        // time is in hours, rounded down.
+        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
+        // As of 0.9.16, allow + 5 minutes to - 65 minutes.
         long time = req.readRequestTime();
         long now = (_context.clock().now() / (60l*60l*1000l)) * (60*60*1000);
-        int ourSlot = -1;
+        long timeDiff = now - time;
+        if (timeDiff > MAX_REQUEST_AGE) {
+            _context.statManager().addRateData("tunnel.rejectTooOld", 1);
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Dropping build request too old... replay attack? " + DataHelper.formatDuration(timeDiff));
+            return;
+        }
+        if (timeDiff < 0 - MAX_REQUEST_FUTURE) {
+            _context.statManager().addRateData("tunnel.rejectFuture", 1);
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Dropping build request too far in future " + DataHelper.formatDuration(0 - timeDiff));
+            return;
+        }
 
         int response;
         if (_context.router().isHidden()) {
@@ -764,6 +784,7 @@ class BuildHandler implements Runnable {
 
         byte reply[] = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
         int records = state.msg.getRecordCount();
+        int ourSlot = -1;
         for (int j = 0; j < records; j++) {
             if (state.msg.getRecord(j) == null) {
                 ourSlot = j;
@@ -780,7 +801,7 @@ class BuildHandler implements Runnable {
                       + " accepted? " + response + " receiving on " + ourId 
                       + " sending to " + nextId
                       + " on " + nextPeer
-                      + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time)
+                      + " inGW? " + isInGW + " outEnd? " + isOutEnd
                       + " recvDelay " + recvDelay + " replyMessage " + req.readReplyMessageId()
                       + " replyKey " + req.readReplyKey() + " replyIV " + Base64.encode(req.readReplyIV()));
 
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index fbfca2bcc9b879063329679b560526534db6dc17..31aaa86654af300d55b77b5c90ce5160715e91cf 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -8,7 +8,7 @@ import net.i2p.data.ByteArray;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.PublicKey;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.TunnelBuildMessage;
 import net.i2p.data.i2np.VariableTunnelBuildMessage;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
index 14eab47e2cc20db9f30a5494778c13952fcee51f..8bb657831c2b2e112bb7ef086f564908bf514d1d 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -16,7 +16,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Hash;
-import net.i2p.data.RouterInfo;
+import net.i2p.data.router.RouterInfo;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;