diff --git a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
index 1a203eec00fa92653905fe46dfc58e4d8f739f56..42248f2dba5aa9b8c200a520920ecfc04bd506e0 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -510,6 +510,11 @@ public final class ECIESAEADEngine {
             if (_log.shouldWarn())
                 _log.warn("No garlic block in ES payload");
         }
+        if (pc.nextKeys != null) {
+            for (NextSessionKey nextKey : pc.nextKeys) {
+                keyManager.nextKeyReceived(remote, nextKey);
+            }
+        }
         if (pc.ackRequested) {
             keyManager.ackRequested(remote, key.getID(), nonce);
         }
@@ -667,7 +672,7 @@ public final class ECIESAEADEngine {
         if (_log.shouldDebug())
             _log.debug("State before encrypt new session: " + state);
 
-        byte[] payload = createPayload(cloves, cloves.getExpiration(), false, null, null);
+        byte[] payload = createPayload(cloves, cloves.getExpiration());
 
         byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN];
         try {
@@ -726,7 +731,7 @@ public final class ECIESAEADEngine {
         if (_log.shouldDebug())
             _log.debug("State after mixhash tag before encrypt new session reply: " + state);
 
-        byte[] payload = createPayload(cloves, 0, false, null, null);
+        byte[] payload = createPayload(cloves, 0);
 
         // part 1 - tag and empty payload
         byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN];
@@ -793,7 +798,7 @@ public final class ECIESAEADEngine {
                                           RatchetSKM keyManager) {
         boolean ackreq = callback != null || ACKREQ_IN_ES;
         byte rawTag[] = re.tag.getData();
-        byte[] payload = createPayload(cloves, 0, ackreq, re.nextKey, re.acksToSend);
+        byte[] payload = createPayload(cloves, 0, ackreq, re.nextForwardKey, re.nextReverseKey, re.acksToSend);
         SessionKeyAndNonce key = re.key;
         int nonce = key.getNonce();
         byte encr[] = encryptAEADBlock(rawTag, payload, key, nonce);
@@ -823,7 +828,7 @@ public final class ECIESAEADEngine {
      */
     public byte[] encrypt(CloveSet cloves, SessionKey key, RatchetSessionTag tag) {
         byte rawTag[] = tag.getData();
-        byte[] payload = createPayload(cloves, 0, false, null, null);
+        byte[] payload = createPayload(cloves, 0);
         byte encr[] = encryptAEADBlock(rawTag, payload, key, 0);
         System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
         return encr;
@@ -857,7 +862,7 @@ public final class ECIESAEADEngine {
         return enc;
     }
 
-    private static final PrivateKey doDH(PrivateKey privkey, PublicKey pubkey) {
+    static final PrivateKey doDH(PrivateKey privkey, PublicKey pubkey) {
         byte[] dh = new byte[KEYLEN];
         Curve25519.eval(dh, 0, privkey.getData(), pubkey.getData());
         return new PrivateKey(EncType.ECIES_X25519, dh);
@@ -868,11 +873,13 @@ public final class ECIESAEADEngine {
     /////////////////////////////////////////////////////////
 
     private class PLCallback implements RatchetPayload.PayloadCallback {
+        /** non null, may be empty */
         public final List<GarlicClove> cloveSet = new ArrayList<GarlicClove>(3);
         private final RatchetSKM skm;
         private final PublicKey remote;
         public long datetime;
-        public NextSessionKey nextKey;
+        /** null or non-empty */
+        public List<NextSessionKey> nextKeys;
         public boolean ackRequested;
 
         /**
@@ -920,7 +927,11 @@ public final class ECIESAEADEngine {
         public void gotNextKey(NextSessionKey next) {
             if (_log.shouldDebug())
                 _log.debug("Got NEXTKEY block: " + next);
-            nextKey = next;
+            // could have both a forward and reverse.
+            // shouldn't have two forwards or two reverses
+            if (nextKeys == null)
+                nextKeys = new ArrayList<NextSessionKey>(2);
+            nextKeys.add(next);
         }
 
         public void gotAck(int id, int n) {
@@ -954,21 +965,33 @@ public final class ECIESAEADEngine {
         }
     }
 
+    /**
+     *  @param expiration if greater than zero, add a DateTime block
+     *  @since 0.9.46
+     */
+    private byte[] createPayload(CloveSet cloves, long expiration) {
+        return createPayload(cloves, expiration, false, null, null, null);
+    }
+
     /**
      *  @param expiration if greater than zero, add a DateTime block
      *  @param ackreq to request an ack, must be false for NS/NSR
+     *  @param nextKey1 may be null
+     *  @param nextKey2 may be null
      *  @param acksTOSend may be null
      */
     private byte[] createPayload(CloveSet cloves, long expiration,
-                                 boolean ackreq, NextSessionKey nextKey,
-                                 List<Integer> acksToSend) {
+                                 boolean ackreq, NextSessionKey nextKey1,
+                                 NextSessionKey nextKey2, List<Integer> acksToSend) {
         int count = cloves.getCloveCount();
         int numblocks = count + 1;
         if (expiration > 0)
             numblocks++;
         if (ackreq)
             numblocks++;
-        if (nextKey != null)
+        if (nextKey1 != null)
+            numblocks++;
+        if (nextKey2 != null)
             numblocks++;
         if (acksToSend != null)
             numblocks++;
@@ -979,8 +1002,13 @@ public final class ECIESAEADEngine {
             blocks.add(block);
             len += block.getTotalLength();
         }
-        if (nextKey != null) {
-            Block block = new NextKeyBlock(nextKey);
+        if (nextKey1 != null) {
+            Block block = new NextKeyBlock(nextKey1);
+            blocks.add(block);
+            len += block.getTotalLength();
+        }
+        if (nextKey2 != null) {
+            Block block = new NextKeyBlock(nextKey2);
             blocks.add(block);
             len += block.getTotalLength();
         }
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
index 03cd18d35d80b2b67e131fdde7d51655ce6a8c05..e6b37071d5c4e5eba52a09581e51b7e3e58ca339 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/NextSessionKey.java
@@ -40,7 +40,7 @@ class NextSessionKey extends PublicKey {
     public String toString() {
         StringBuilder buf = new StringBuilder(64);
         buf.append("[NextSessionKey: ");
-        buf.append(toBase64());
+        buf.append(super.toString());
         buf.append(" ID: ").append(_id);
         buf.append(" reverse? ").append(_isReverse);
         buf.append(" request? ").append(_isRequest);
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java
index 5403575ab243bbb7f3af1e99fef6529c155aee7a..76c356de82968a3103b80b6fd56db7a7a199a8ed 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetEntry.java
@@ -6,7 +6,7 @@ import net.i2p.data.SessionKey;
 
 /**
  *  Simple object with outbound tag, key, and nonce,
- *  and an optional next key.
+ *  and an optional next keys.
  *  The object returned from SKM.consumeNextAvailableTag() to the engine encrypt.
  *
  *  @since 0.9.44
@@ -16,21 +16,23 @@ class RatchetEntry {
     public final SessionKeyAndNonce key;
     public final int keyID;
     public final int pn;
-    public final NextSessionKey nextKey;
+    public final NextSessionKey nextForwardKey;
+    public final NextSessionKey nextReverseKey;
     public final List<Integer> acksToSend;
 
     /** outbound - calculated key */
     public RatchetEntry(RatchetSessionTag tag, SessionKeyAndNonce key, int keyID, int pn) {
-        this(tag, key, keyID, pn, null, null);
+        this(tag, key, keyID, pn, null, null, null);
     }
 
     public RatchetEntry(RatchetSessionTag tag, SessionKeyAndNonce key, int keyID, int pn,
-                        NextSessionKey nextKey, List<Integer> acksToSend) {
+                        NextSessionKey nextFwdKey, NextSessionKey nextRevKey, List<Integer> acksToSend) {
         this.tag = tag;
         this.key = key;
         this.keyID = keyID;
         this.pn = pn;
-        this.nextKey = nextKey;
+        this.nextForwardKey = nextFwdKey;
+        this.nextReverseKey = nextRevKey;
         this.acksToSend = acksToSend;
     }
 
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
index 2330c84db6385a91f414d3cdf94be8b27535ee5f..125a0d4441419b0cd2421d88ffdc1f089d7da7aa 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetSKM.java
@@ -22,10 +22,12 @@ import com.southernstorm.noise.protocol.HandshakeState;
 import net.i2p.I2PAppContext;
 import net.i2p.crypto.EncType;
 import net.i2p.crypto.HKDF;
+import net.i2p.crypto.KeyPair;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.crypto.TagSetHandle;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
+import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
@@ -241,8 +243,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                 _log.info("Session " + state.hashCode() + " update as Bob. Alice: " + toString(target));
             OutboundSession sess = getSession(target);
             if (sess == null) {
-                if (_log.shouldDebug())
-                    _log.debug("Update Bob session but no session found for "  + target);
+                if (_log.shouldWarn())
+                    _log.warn("Update Bob session but no session found for "  + target);
                 // TODO can we recover?
                 return false;
             }
@@ -304,6 +306,19 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
         return true;
     }
 
+    /**
+     * @since 0.9.46
+     */
+    public void nextKeyReceived(PublicKey target, NextSessionKey key) {
+        OutboundSession sess = getSession(target);
+        if (sess == null) {
+            if (_log.shouldWarn())
+                _log.warn("Got NextKey but no session found for "  + target);
+            return;
+        }
+        sess.nextKeyReceived(key);
+    }
+
     /**
      * @throws UnsupportedOperationException always
      */
@@ -793,7 +808,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             for (RatchetTagSet ts : sets) {
                 int size = ts.size();
                 total += size;
-                buf.append("<li><b>ID: ").append(ts.getID());
+                buf.append("<li><b>ID: ").append(ts.getID())
+                   .append(" / ").append(ts.getDebugID());
                 buf.append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()))
                    .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate()));
                 long expires = ts.getExpiration() - now;
@@ -835,6 +851,7 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             for (RatchetTagSet ts : sets) {
                 int size = ts.remaining();
                 buf.append("<li><b>ID: ").append(ts.getID())
+                   .append(" / ").append(ts.getDebugID())
                    .append(" created:</b> ").append(DataHelper.formatTime(ts.getCreated()))
                    .append(" <b>last use:</b> ").append(DataHelper.formatTime(ts.getDate()));
                 long expires = ts.getExpiration() - now;
@@ -914,6 +931,14 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
          */
         private int _consecutiveFailures;
 
+        // next key
+        private int _myOBKeyID = -1;
+        private int _hisOBKeyID = -1;
+        private int _currentOBTagSetID;
+        private int _myIBKeyID = -1;
+        private int _hisIBKeyID = -1;
+        private int _currentIBTagSetID;
+
         private static final int MAX_FAILS = 2;
         private static final int MAX_SEND_ACKS = 8;
         private static final int DEBUG_OB_NSR = 0x10001;
@@ -1024,6 +1049,76 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
             //state.destroy();
         }
 
+        /**
+         * @since 0.9.46
+         */
+        public void nextKeyReceived(NextSessionKey key) {
+            boolean isReverse = key.isReverse();
+            boolean isRequest = key.isRequest();
+            boolean hasKey = key.getData() != null;
+            int id = key.getID();
+            synchronized (_tagSets) {
+                if (isReverse) {
+                    // this is about my outbound tag set,
+                    // and is an ack of new key sent
+                    if (_hisIBKeyID != id) {
+                        if (_log.shouldWarn())
+                            _log.warn("Got new key id, ratchet OB " + id);
+                        if (_hisIBKeyID != id + 1) {
+                            if (_log.shouldWarn())
+                                _log.warn("Got bad new key id OB? " + id);
+                        }
+                        if (hasKey) {
+                            KeyPair nextKeys = _context.keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
+                            PublicKey pub = nextKeys.getPublic();
+                            PrivateKey priv = nextKeys.getPrivate();
+                            PrivateKey sharedSecret = ECIESAEADEngine.doDH(priv, key);
+                            // create new OB TS
+                            // find current OB TS, and delete it
+                        } else {
+                            // TODO get it from above
+                            if (_log.shouldWarn())
+                                _log.warn("Got nextkey w/o key but we don't have it " + id);
+                        }
+                    } else {
+                        if (_log.shouldWarn())
+                            _log.warn("Got dup new key id for OB " + id);
+                    }
+                    if (isRequest) {
+                        if (_log.shouldWarn())
+                            _log.warn("invalid req+rev in nextkey");
+                        // ignore
+                    }
+                } else {
+                    // this is about my inbound tag set
+                    if (_hisOBKeyID != id) {
+                        if (_log.shouldWarn())
+                            _log.warn("Got new key id, ratchet IB " + id);
+                        if (_hisOBKeyID != id + 1) {
+                            if (_log.shouldWarn())
+                                _log.warn("Got bad new key id IB? " + id);
+                        }
+                        if (!hasKey) {
+                            if (_log.shouldWarn())
+                                _log.warn("Got nextkey w/o key but we don't have it " + id);
+                        }
+                        // find current OB TS, tell him to send ack
+                        // create new IB TS
+                    } else {
+                        if (_log.shouldWarn())
+                            _log.warn("Got dup new key id for IB " + id);
+                        // find current OB TS, tell him to send ack if nec.
+                        // create new IB TS if nec.
+                    }
+                    if (!isRequest) {
+                        if (_log.shouldWarn())
+                            _log.warn("invalid fwd w/o req in nextkey");
+                        // ignore
+                    }
+                }
+            }
+        }
+
         /**
          * First tag was received for this inbound (ES) tagset.
          * Find the corresponding outbound (ES) tagset in _unackedTagSets,
@@ -1190,7 +1285,8 @@ public class RatchetSKM extends SessionKeyManager implements SessionTagListener
                                 set.setDate(now);
                                 SessionKeyAndNonce skn = set.consumeNextKey();
                                 // TODO PN
-                                return new RatchetEntry(tag, skn, set.getID(), 0, set.getNextKey(), getAcksToSend());
+                                // TODO reverse next key
+                                return new RatchetEntry(tag, skn, set.getID(), 0, set.getNextKey(), null, getAcksToSend());
                             } else if (_log.shouldInfo()) {
                                 _log.info("Removing empty " + set);
                             }
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
index bf2716404ddfc4f7ca474dd965ce1225fbcdad59..246981e8eb7d67d1956f31941d0033f43cb5c2dd 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetTagSet.java
@@ -263,6 +263,7 @@ class RatchetTagSet implements TagSetHandle {
      *  @since 0.9.46
      */
     public synchronized long getExpiration() {
+        // TODO return shorter if not acked?
         return _date + _timeout;
     }
 
@@ -299,8 +300,9 @@ class RatchetTagSet implements TagSetHandle {
     }
 
     /**
-     *  Next Key if applicable
-     *  null if remaining is sufficient
+     *  Next Forward Key if applicable (we're running low).
+     *  Null if remaining is sufficient.
+     *  Once non-null, will be constant for the remaining life of the tagset.
      *
      *  @return key or null
      *  @since 0.9.46
@@ -308,8 +310,6 @@ class RatchetTagSet implements TagSetHandle {
     public NextSessionKey getNextKey() {
         if (remaining() > LOW)
             return null;
-        if (_nextKeyAcked)  // maybe not needed, keep sending until unused
-            return null;
         if (_nextKeys == null) {
             _nextKeys = I2PAppContext.getGlobalContext().keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
             boolean isIB = _sessionTags != null;
@@ -319,6 +319,18 @@ class RatchetTagSet implements TagSetHandle {
         return _nextKey;
     }
 
+    /**
+     *  Next Forward KeyPair if applicable (we're running low).
+     *  Null if remaining is sufficient.
+     *  Once non-null, will be constant for the remaining life of the tagset.
+     *
+     *  @return keys or null
+     *  @since 0.9.46
+     */
+    public KeyPair getNextKeys() {
+        return _nextKeys;
+    }
+
     /**
      *  tags still available
      *  inbound only
@@ -502,11 +514,21 @@ class RatchetTagSet implements TagSetHandle {
      */
     public boolean getAcked() { return _acked; }
 
-    /** the Key ID */
+    /**
+     * The TagSet ID, starting at 0.
+     * After that = 1 + my key id + his key id
+     */
     public int getID() {
         return _id;
     }
 
+    /**
+     * A unique ID for debugging only
+     */
+    public int getDebugID() {
+        return _tagSetID;
+    }
+
     @Override
     public synchronized String toString() {
         StringBuilder buf = new StringBuilder(256);