diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
index 680766116d791848c1fb0d3f1bf2f7c800edb769..d70d92e3a4f44555830b8b238bc9d8d1d0382c59 100644
--- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
@@ -246,6 +246,8 @@ public class BuildRequestRecord {
      * Not to be used for short ECIES records; use the ChaChaReplyKey instead.
      */
     public SessionKey readReplyKey() {
+        if (_isEC && _data.length == LENGTH_EC_SHORT)
+            throw new IllegalStateException();
         byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
         int off = _isEC ? OFF_REPLY_KEY_EC : OFF_REPLY_KEY;
         System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
@@ -258,6 +260,8 @@ public class BuildRequestRecord {
      * @return 16 bytes
      */
     public byte[] readReplyIV() {
+        if (_isEC && _data.length == LENGTH_EC_SHORT)
+            throw new IllegalStateException();
         byte iv[] = new byte[IV_SIZE];
         int off = _isEC ? OFF_REPLY_IV_EC : OFF_REPLY_IV;
         System.arraycopy(_data, off, iv, 0, IV_SIZE);
diff --git a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
index a3128ede2250fb5bffc0437068045e740f50eb5d..eb44a903df80653d92a0768d0cbd232b62a514ba 100644
--- a/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildResponseRecord.java
@@ -86,7 +86,7 @@ public class BuildResponseRecord {
      * @param options 116 bytes max when serialized
      * @return a 236-byte response record
      * @throws IllegalArgumentException if options too big or on encryption failure
-     * @since 0.9.451
+     * @since 0.9.51
      */
     public static ShortEncryptedBuildRecord createShort(I2PAppContext ctx, int status, SessionKey replyKey,
                                                         byte replyAD[], Properties options) {
diff --git a/router/java/src/net/i2p/data/i2np/InboundTunnelBuildMessage.java b/router/java/src/net/i2p/data/i2np/InboundTunnelBuildMessage.java
index d25443d2f659f4583b49d3e0fba5a4d4522b9d34..6ae986d9e48008d8638d4f552511208348ad254d 100644
--- a/router/java/src/net/i2p/data/i2np/InboundTunnelBuildMessage.java
+++ b/router/java/src/net/i2p/data/i2np/InboundTunnelBuildMessage.java
@@ -18,7 +18,7 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
     public static final int SHORT_RECORD_SIZE = ShortTunnelBuildMessage.SHORT_RECORD_SIZE;
     public static final int MAX_PLAINTEXT_RECORD_SIZE = OutboundTunnelBuildReplyMessage.MAX_PLAINTEXT_RECORD_SIZE;
 
-    private int _plaintextSlot;
+    private int _plaintextSlot = -1;
     private byte[] _plaintextRecord;
 
     /** zero record count, will be set with readMessage() */
@@ -32,10 +32,11 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
 
     /**
      *  @param record must be ShortEncryptedBuildRecord or null
+     *  @throws IllegalArgumentException on bad slot or record length.
      */
     @Override
     public void setRecord(int index, EncryptedBuildRecord record) {
-        if (record != null && record.length() != SHORT_RECORD_SIZE)
+        if (record != null && (record.length() != SHORT_RECORD_SIZE || index == _plaintextSlot))
             throw new IllegalArgumentException();
         super.setRecord(index, record);
     }
@@ -45,7 +46,8 @@ public class InboundTunnelBuildMessage extends TunnelBuildMessage {
      *  @throws IllegalArgumentException on bad slot or data length.
      */
     public void setPlaintextRecord(int slot, byte[] data) {
-        if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE)
+        if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE ||
+            (_records != null && _records[slot] != null))
             throw new IllegalArgumentException();
         _plaintextSlot = slot;
         _plaintextRecord = data;
diff --git a/router/java/src/net/i2p/data/i2np/OutboundTunnelBuildReplyMessage.java b/router/java/src/net/i2p/data/i2np/OutboundTunnelBuildReplyMessage.java
index 59537b718ef7481b9c1fb77389b574371a6e29b0..da00dc495070e15b6798143508bc16d8032e2e92 100644
--- a/router/java/src/net/i2p/data/i2np/OutboundTunnelBuildReplyMessage.java
+++ b/router/java/src/net/i2p/data/i2np/OutboundTunnelBuildReplyMessage.java
@@ -1,6 +1,11 @@
 package net.i2p.data.i2np;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
 import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 
 /**
@@ -18,7 +23,7 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
     public static final int SHORT_RECORD_SIZE = ShortTunnelBuildMessage.SHORT_RECORD_SIZE;
     public static final int MAX_PLAINTEXT_RECORD_SIZE = 172;
 
-    private int _plaintextSlot;
+    private int _plaintextSlot = -1;
     private byte[] _plaintextRecord;
 
     /** zero record count, will be set with readMessage() */
@@ -32,20 +37,58 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
 
     /**
      *  @param record must be ShortEncryptedBuildRecord or null
+     *  @throws IllegalArgumentException on bad slot or record length.
      */
     @Override
     public void setRecord(int index, EncryptedBuildRecord record) {
-        if (record != null && record.length() != SHORT_RECORD_SIZE)
+        if (record != null && (record.length() != SHORT_RECORD_SIZE || index == _plaintextSlot))
             throw new IllegalArgumentException();
         super.setRecord(index, record);
     }
 
+    /**
+     *  Set the slot and data for the plaintext record.
+     *  Empty properties will be used.
+     *
+     *  @param reply 0-255
+     *  @throws IllegalArgumentException on bad slot or data length.
+     *  @since 0.9.51
+     */
+    public void setPlaintextRecord(int slot, int reply) {
+        // 00 00 reply
+        byte[] data = new byte[3];
+        data[2] = (byte) reply;
+        setPlaintextRecord(slot, data);
+    }
+
+    /**
+     *  Set the slot and data for the plaintext record.
+     *
+     *  @param reply 0-255
+     *  @param props may be null
+     *  @throws IllegalArgumentException on bad slot or data length.
+     *  @since 0.9.51
+     */
+    public void setPlaintextRecord(int slot, int reply, Properties props) throws DataFormatException {
+        if (props == null || props.isEmpty()) {
+            setPlaintextRecord(slot, reply);
+            return;
+        }
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            DataHelper.writeProperties(baos, props);
+        } catch (IOException ioe) {}
+        baos.write((byte) reply);
+        setPlaintextRecord(slot, baos.toByteArray());
+    }
+
     /**
      *  Set the slot and data for the plaintext record.
      *  @throws IllegalArgumentException on bad slot or data length.
      */
     public void setPlaintextRecord(int slot, byte[] data) {
-        if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE)
+        if (slot < 0 || slot >= RECORD_COUNT || data.length == 0 || data.length > MAX_PLAINTEXT_RECORD_SIZE ||
+            (_records != null && _records[slot] != null))
             throw new IllegalArgumentException();
         _plaintextSlot = slot;
         _plaintextRecord = data;
@@ -66,6 +109,24 @@ public class OutboundTunnelBuildReplyMessage extends TunnelBuildReplyMessage {
         return _plaintextRecord;
     }
 
+    /**
+     *  Get the data for the plaintext record.
+     *  @since 0.9.51
+     */
+    public int getPlaintextReply() {
+        return _plaintextRecord[_plaintextRecord.length - 1] & 0xff;
+    }
+
+    /**
+     *  Get the data for the plaintext record.
+     *  @since 0.9.51
+     */
+    public Properties getPlaintextOptions() throws DataFormatException {
+        Properties props = new Properties();
+        DataHelper.fromProperties(_plaintextRecord, 0, props);
+        return props;
+    }
+
     @Override
     protected int calculateWrittenLength() { return 4 + _plaintextRecord.length + ((RECORD_COUNT - 1) * SHORT_RECORD_SIZE); }
 
diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
index ff13ce889a178ea8ea7df898a4ab5f17b92ec6e1..e393105d230295172faf26aec49eda5492e84ad0 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
@@ -13,6 +13,7 @@ import net.i2p.data.i2np.DeliveryInstructions;
 import net.i2p.data.i2np.DeliveryStatusMessage;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.OutboundTunnelBuildReplyMessage;
 import net.i2p.data.i2np.TunnelBuildReplyMessage;
 import net.i2p.data.i2np.VariableTunnelBuildReplyMessage;
 import net.i2p.router.ClientMessage;
@@ -131,6 +132,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
 
                 case DeliveryStatusMessage.MESSAGE_TYPE:
                 case GarlicMessage.MESSAGE_TYPE:
+                case OutboundTunnelBuildReplyMessage.MESSAGE_TYPE:
                 case TunnelBuildReplyMessage.MESSAGE_TYPE:
                 case VariableTunnelBuildReplyMessage.MESSAGE_TYPE:
                     // these are safe, handled below
@@ -161,6 +163,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                 case DatabaseSearchReplyMessage.MESSAGE_TYPE:
                 case DeliveryStatusMessage.MESSAGE_TYPE:
                 case GarlicMessage.MESSAGE_TYPE:
+                case OutboundTunnelBuildReplyMessage.MESSAGE_TYPE:
                 case TunnelBuildReplyMessage.MESSAGE_TYPE:
                 case VariableTunnelBuildReplyMessage.MESSAGE_TYPE:
                     // these are safe, handled below
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 da1ecb4b8bb3772f66835dba8014f17340d37204..1c44dc6d00bf19ed2abcbbda6fdf820e433ea940 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -1049,7 +1049,7 @@ class BuildHandler implements Runnable {
             TunnelBuildReplyMessage replyMsg;
             if (state.msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE) {
                 OutboundTunnelBuildReplyMessage otbrm  = new OutboundTunnelBuildReplyMessage(_context, records);
-                otbrm.setPlaintextRecord(ourSlot, null); // TODO
+                otbrm.setPlaintextRecord(ourSlot, response);
                 replyMsg = otbrm;
             } else if (records == TunnelBuildMessage.MAX_RECORD_COUNT) {
                 replyMsg = new TunnelBuildReplyMessage(_context);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageProcessor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageProcessor.java
index 9d0aade215b7e511b40ee12d0f2c95cac8452bee..028de88710d3b0f22afd947f92b05848878819ae 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildMessageProcessor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildMessageProcessor.java
@@ -1,5 +1,6 @@
 package net.i2p.router.tunnel.pool;
 
+import net.i2p.crypto.ChaCha20;
 import net.i2p.crypto.EncType;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
@@ -9,6 +10,8 @@ import net.i2p.data.PrivateKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.BuildRequestRecord;
 import net.i2p.data.i2np.EncryptedBuildRecord;
+import net.i2p.data.i2np.ShortEncryptedBuildRecord;
+import net.i2p.data.i2np.ShortTunnelBuildMessage;
 import net.i2p.data.i2np.TunnelBuildMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.RouterThrottleImpl;
@@ -92,39 +95,39 @@ class BuildMessageProcessor {
     public BuildRequestRecord decrypt(TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) {
         BuildRequestRecord rv = null;
         int ourHop = -1;
-        long beforeActualDecrypt = 0;
-        long afterActualDecrypt = 0;
         byte[] ourHashData = ourHash.getData();
-        long beforeLoop = System.currentTimeMillis();
+        boolean isShort = msg.getType() == ShortTunnelBuildMessage.MESSAGE_TYPE;
         for (int i = 0; i < msg.getRecordCount(); i++) {
             EncryptedBuildRecord rec = msg.getRecord(i);
             boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, BuildRequestRecord.PEER_SIZE);
             if (eq) {
-                beforeActualDecrypt = System.currentTimeMillis();
                 try {
                     rv = new BuildRequestRecord(ctx, privKey, rec);
-                    afterActualDecrypt = System.currentTimeMillis();
 
-                    // i2pd bug
-                    boolean isBad = SessionKey.INVALID_KEY.equals(rv.readReplyKey());
-                    if (isBad) {
-                        if (log.shouldLog(Log.WARN))
-                            log.warn(msg.getUniqueId() + ": Bad reply key: " + rv);
-                        ctx.statManager().addRateData("tunnel.buildRequestBadReplyKey", 1);
-                        return null;
-                    }
+                    if (isShort) {
+                        // Bloom filter TBD
+                    } else {
+                        // i2pd bug
+                        boolean isBad = SessionKey.INVALID_KEY.equals(rv.readReplyKey());
+                        if (isBad) {
+                            if (log.shouldLog(Log.WARN))
+                                log.warn(msg.getUniqueId() + ": Bad reply key: " + rv);
+                            ctx.statManager().addRateData("tunnel.buildRequestBadReplyKey", 1);
+                            return null;
+                        }
 
-                    // The spec says to feed the 32-byte AES-256 reply key into the Bloom filter.
-                    // But we were using the first 32 bytes of the encrypted reply.
-                    // Fixed in 0.9.24
-                    boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
-                    int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY;
-                    boolean isDup = _filter.add(rv.getData(), off, 32);
-                    if (isDup) {
-                        if (log.shouldLog(Log.WARN))
-                            log.warn(msg.getUniqueId() + ": Dup record: " + rv);
-                        ctx.statManager().addRateData("tunnel.buildRequestDup", 1);
-                        return null;
+                        // The spec says to feed the 32-byte AES-256 reply key into the Bloom filter.
+                        // But we were using the first 32 bytes of the encrypted reply.
+                        // Fixed in 0.9.24
+                        boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
+                        int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY;
+                        boolean isDup = _filter.add(rv.getData(), off, 32);
+                        if (isDup) {
+                            if (log.shouldLog(Log.WARN))
+                                log.warn(msg.getUniqueId() + ": Dup record: " + rv);
+                            ctx.statManager().addRateData("tunnel.buildRequestDup", 1);
+                            return null;
+                        }
                     }
 
                     if (log.shouldLog(Log.DEBUG))
@@ -150,28 +153,36 @@ class BuildMessageProcessor {
             return null;
         }
         
-        long beforeEncrypt = System.currentTimeMillis();
-        SessionKey replyKey = rv.readReplyKey();
-        byte iv[] = rv.readReplyIV();
-        for (int i = 0; i < msg.getRecordCount(); i++) {
-            if (i != ourHop) {
-                EncryptedBuildRecord data = msg.getRecord(i);
-                //if (log.shouldLog(Log.DEBUG))
-                //    log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
-                // encrypt in-place, corrupts SDS
-                byte[] bytes = data.getData();
-                ctx.aes().encrypt(bytes, 0, bytes, 0, replyKey, iv, 0, EncryptedBuildRecord.LENGTH);
+        if (isShort) {
+            byte[] replyKey = rv.getChaChaReplyKey().getData();
+            byte iv[] = new byte[12];
+            for (int i = 0; i < msg.getRecordCount(); i++) {
+                if (i != ourHop) {
+                    EncryptedBuildRecord data = msg.getRecord(i);
+                    //if (log.shouldLog(Log.DEBUG))
+                    //    log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
+                    // encrypt in-place, corrupts SDS
+                    byte[] bytes = data.getData();
+                    // slot number, little endian
+                    iv[0] = (byte) i;
+                    ChaCha20.encrypt(replyKey, iv, bytes, 0, bytes, 0, ShortEncryptedBuildRecord.LENGTH);
+                }
+            }
+        } else {
+            SessionKey replyKey = rv.readReplyKey();
+            byte iv[] = rv.readReplyIV();
+            for (int i = 0; i < msg.getRecordCount(); i++) {
+                if (i != ourHop) {
+                    EncryptedBuildRecord data = msg.getRecord(i);
+                    //if (log.shouldLog(Log.DEBUG))
+                    //    log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv));
+                    // encrypt in-place, corrupts SDS
+                    byte[] bytes = data.getData();
+                    ctx.aes().encrypt(bytes, 0, bytes, 0, replyKey, iv, 0, EncryptedBuildRecord.LENGTH);
+                }
             }
         }
-        long afterEncrypt = System.currentTimeMillis();
         msg.setRecord(ourHop, null);
-        if (afterEncrypt-beforeLoop > 1000) {
-            if (log.shouldLog(Log.WARN))
-                log.warn("Slow decryption, total=" + (afterEncrypt-beforeLoop) 
-                         + " looping=" + (beforeEncrypt-beforeLoop)
-                         + " decrypt=" + (afterActualDecrypt-beforeActualDecrypt)
-                         + " encrypt=" + (afterEncrypt-beforeEncrypt));
-        }
         return rv;
     }
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
index ba13ad318504c7f7ad5b4ee5f65f400977801487..6371ddcd257e0624306cf066b888b4808ca84efa 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildReplyHandler.java
@@ -3,6 +3,7 @@ package net.i2p.router.tunnel.pool;
 import java.util.List;
 
 import net.i2p.I2PAppContext;
+import net.i2p.crypto.ChaCha20;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
@@ -10,6 +11,7 @@ import net.i2p.data.SessionKey;
 import net.i2p.data.i2np.BuildResponseRecord;
 import net.i2p.data.i2np.EncryptedBuildRecord;
 import net.i2p.data.i2np.OutboundTunnelBuildReplyMessage;
+import net.i2p.data.i2np.ShortEncryptedBuildRecord;
 import net.i2p.data.i2np.TunnelBuildReplyMessage;
 import net.i2p.router.tunnel.TunnelCreatorConfig;
 import net.i2p.util.Log;
@@ -50,6 +52,7 @@ class BuildReplyHandler {
             log.error("Corrupted build reply, expected " + recordOrder.size() + " records, got " + reply.getRecordCount());
             return null;
         }
+        boolean isShort = reply.getType() == OutboundTunnelBuildReplyMessage.MESSAGE_TYPE;
         int rv[] = new int[reply.getRecordCount()];
         for (int i = 0; i < rv.length; i++) {
             int hop = recordOrder.get(i).intValue();
@@ -86,6 +89,10 @@ class BuildReplyHandler {
                 rv[i] = ok;
             }
         }
+        if (isShort) {
+            OutboundTunnelBuildReplyMessage otbrm = (OutboundTunnelBuildReplyMessage) reply;
+            rv[otbrm.getPlaintextSlot()] = otbrm.getPlaintextReply();
+        }
         return rv;
     }
 
@@ -100,6 +107,24 @@ class BuildReplyHandler {
      */
     private int decryptRecord(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) {
         EncryptedBuildRecord rec = reply.getRecord(recordNum);
+        boolean isShort = reply.getType() == OutboundTunnelBuildReplyMessage.MESSAGE_TYPE;
+        if (rec == null) {
+            if (!isShort) {
+                if (log.shouldWarn())
+                    log.warn("Missing record " + recordNum);
+                return -1;
+            }
+            OutboundTunnelBuildReplyMessage otbrm = (OutboundTunnelBuildReplyMessage) reply;
+            if (otbrm.getPlaintextSlot() != recordNum) {
+                if (log.shouldWarn())
+                    log.warn("Plaintext slot mismatch expected " + recordNum + " got " + otbrm.getPlaintextSlot());
+                return -1;
+            }
+            int rv = otbrm.getPlaintextReply();
+            if (log.shouldLog(Log.DEBUG))
+                log.debug(reply.getUniqueId() + ": Received: " + rv + " for plaintext record " + recordNum + "/" + hop);
+            return rv;
+        }
         byte[] data = rec.getData();
         int start = cfg.getLength() - 1;
         if (cfg.isInbound())
@@ -110,18 +135,32 @@ class BuildReplyHandler {
         if (isEC)
             end++;
         // do we need to adjust this for the endpoint?
-        for (int j = start; j >= end; j--) {
-            SessionKey replyKey = cfg.getAESReplyKey(j);
-            byte replyIV[] = cfg.getAESReplyIV(j);
-            if (log.shouldLog(Log.DEBUG)) {
-                log.debug(reply.getUniqueId() + ": Decrypting record " + recordNum + "/" + hop + "/" + j + " with replyKey " 
-                          + replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
-                log.debug(reply.getUniqueId() + ": before decrypt: " + Base64.encode(data));
-                log.debug(reply.getUniqueId() + ": Full reply rec: sz=" + data.length + " data=" + Base64.encode(data, 0, TunnelBuildReplyMessage.RECORD_SIZE));
+        if (isShort) {
+            byte iv[] = new byte[12];
+            for (int j = start; j >= end; j--) {
+                byte[] replyKey = cfg.getChaChaReplyKey(j).getData();
+                if (log.shouldDebug()) {
+                    log.debug(reply.getUniqueId() + ": Decrypting ChaCha record " + recordNum + "/" + hop + "/" + j + " with replyKey " 
+                              + Base64.encode(replyKey) + " : " + cfg);
+                }
+                // slot number, little endian
+                iv[0] = (byte) recordNum;
+                ChaCha20.encrypt(replyKey, iv, data, 0, data, 0, ShortEncryptedBuildRecord.LENGTH);
+            }
+        } else {
+            for (int j = start; j >= end; j--) {
+                SessionKey replyKey = cfg.getAESReplyKey(j);
+                byte replyIV[] = cfg.getAESReplyIV(j);
+                if (log.shouldDebug()) {
+                    log.debug(reply.getUniqueId() + ": Decrypting AES record " + recordNum + "/" + hop + "/" + j + " with replyKey " 
+                              + replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
+                    //log.debug(reply.getUniqueId() + ": before decrypt: " + Base64.encode(data));
+                    //log.debug(reply.getUniqueId() + ": Full reply rec: sz=" + data.length + " data=" + Base64.encode(data));
+                }
+                ctx.aes().decrypt(data, 0, data, 0, replyKey, replyIV, 0, data.length);
+                //if (log.shouldLog(Log.DEBUG))
+                //    log.debug(reply.getUniqueId() + ": after decrypt: " + Base64.encode(data));
             }
-            ctx.aes().decrypt(data, 0, data, 0, replyKey, replyIV, 0, TunnelBuildReplyMessage.RECORD_SIZE);
-            if (log.shouldLog(Log.DEBUG))
-                log.debug(reply.getUniqueId() + ": after decrypt: " + Base64.encode(data));
         }
         // ok, all of the layered encryption is stripped, so lets verify it 
         // (formatted per BuildResponseRecord.create)
diff --git a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
index dde9559c20b348bcd2ffdb098d806cf450a0f432..29177039e07b907c418d4503ed803d8f992325d7 100644
--- a/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
+++ b/router/java/test/junit/net/i2p/router/tunnel/pool/BuildMessageTestStandalone.java
@@ -21,6 +21,8 @@ import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.BuildRequestRecord;
 import net.i2p.data.i2np.BuildResponseRecord;
 import net.i2p.data.i2np.EncryptedBuildRecord;
+import net.i2p.data.i2np.OutboundTunnelBuildReplyMessage;
+import net.i2p.data.i2np.ShortTunnelBuildMessage;
 import net.i2p.data.i2np.TunnelBuildMessage;
 import net.i2p.data.i2np.TunnelBuildReplyMessage;
 import net.i2p.router.Router;
@@ -50,10 +52,11 @@ public class BuildMessageTestStandalone extends TestCase {
         RouterContext ctx = new RouterContext(null);
         x_testBuildMessage(ctx, 1);
         x_testBuildMessage(ctx, 2);
+        x_testBuildMessage(ctx, 3);
     }
 
     /**
-     *  @param testType 1=ElG; 2=ECIES; 3=ECIES short
+     *  @param testType 1=ElG; 2=ECIES; 3=ECIES short outbound; 4=ECIES short inbound
      */
     private void x_testBuildMessage(RouterContext ctx, int testType) {
         Log log = ctx.logManager().getLog(getClass());
@@ -75,7 +78,11 @@ public class BuildMessageTestStandalone extends TestCase {
         _replyTunnel = 42;
         
         // populate and encrypt the message
-        TunnelBuildMessage msg = new TunnelBuildMessage(ctx);
+        TunnelBuildMessage msg;
+        if (testType == 3)
+            msg = new ShortTunnelBuildMessage(ctx, TunnelBuildMessage.MAX_RECORD_COUNT);
+        else
+            msg = new TunnelBuildMessage(ctx);
         for (int i = 0; i < order.size(); i++) {
             int hop = order.get(i).intValue();
             PublicKey key = null;
@@ -102,7 +109,6 @@ public class BuildMessageTestStandalone extends TestCase {
             // If false, no records matched the _peers[i], or the decryption failed
             assertTrue("foo @ " + i, req != null);
             long ourId = req.readReceiveTunnelId();
-            byte replyIV[] = req.readReplyIV();
             long nextId = req.readNextTunnelId();
             Hash nextPeer = req.readNextIdentity();
             boolean isInGW = req.readIsInboundGateway();
@@ -112,23 +118,54 @@ public class BuildMessageTestStandalone extends TestCase {
             int ourSlot = -1;
 
             EncryptedBuildRecord reply;
-            if (testType == 1)
-                reply = BuildResponseRecord.create(ctx, 0, req.readReplyKey(), replyIV, i);
-            else
+            if (testType == 1) {
+                reply = BuildResponseRecord.create(ctx, 0, req.readReplyKey(), req.readReplyIV(), i);
+            } else if (testType == 2) {
                 reply = BuildResponseRecord.create(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
-            for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
-                if (msg.getRecord(j) == null) {
-                    ourSlot = j;
-                    msg.setRecord(j, reply);
-                    break;
+            } else {
+                reply = BuildResponseRecord.createShort(ctx, 0, req.getChaChaReplyKey(), req.getChaChaReplyAD(), EmptyProperties.INSTANCE);
+            }
+            if (testType != 3 || i != cfg.getLength() - 1) {
+                for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
+                    if (msg.getRecord(j) == null) {
+                        ourSlot = j;
+                        msg.setRecord(j, reply);
+                        break;
+                    }
+                }
+            } else {
+                for (int j = 0; j < TunnelBuildMessage.MAX_RECORD_COUNT; j++) {
+                    if (msg.getRecord(j) == null) {
+                        ourSlot = j;
+                        break;
+                    }
                 }
             }
-            
-            log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() 
-                      + " receives on " + ourId 
-                      + " w/ replyIV " + Base64.encode(replyIV) + " sending to " + nextId
-                      + " on " + nextPeer.toBase64()
-                      + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time));
+
+            if (testType == 1) {
+                log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() 
+                          + " receives on " + ourId + " sending to " + nextId
+                          + " replyKey " + Base64.encode(req.readReplyKey().getData())
+                          + " replyIV " + Base64.encode(req.readReplyIV())
+                          + " on " + nextPeer.toBase64()
+                          + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time));
+            } else if (testType == 2) {
+                log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() 
+                          + " receives on " + ourId + " sending to " + nextId
+                          + " replyKey " + Base64.encode(req.readReplyKey().getData())
+                          + " replyIV " + Base64.encode(req.readReplyIV())
+                          + " chachaKey " + Base64.encode(req.getChaChaReplyKey().getData())
+                          + " chachaAD " + Base64.encode(req.getChaChaReplyAD())
+                          + " on " + nextPeer.toBase64()
+                          + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time));
+            } else {
+                log.debug("Read slot " + ourSlot + " containing hop " + i + " @ " + _peers[i].toBase64() 
+                          + " receives on " + ourId + " sending to " + nextId
+                          + " chachaKey " + Base64.encode(req.getChaChaReplyKey().getData())
+                          + " chachaAD " + Base64.encode(req.getChaChaReplyAD())
+                          + " on " + nextPeer.toBase64()
+                          + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time));
+            }
         }
         
         
@@ -137,9 +174,32 @@ public class BuildMessageTestStandalone extends TestCase {
                   "\n================================================================");
         
         // now all of the replies are populated, toss 'em into a reply message and handle it
-        TunnelBuildReplyMessage reply = new TunnelBuildReplyMessage(ctx);
-        for (int i = 0; i < TunnelBuildMessage.MAX_RECORD_COUNT; i++)
-            reply.setRecord(i, msg.getRecord(i));
+        TunnelBuildReplyMessage reply;
+        if (testType == 3) {
+            OutboundTunnelBuildReplyMessage otbrm = new OutboundTunnelBuildReplyMessage(ctx, TunnelBuildMessage.MAX_RECORD_COUNT);
+            int ibep = _peers.length - 1;
+            int ibepSlot = -1;
+            for (int i = 0; i < order.size(); i++) {
+                int slot = order.get(i).intValue();
+                if (slot == ibep) {
+                    ibepSlot = i;
+                    break;
+                }
+            }
+            log.debug("OTBRM plaintext slot is " + ibepSlot);
+            for (int i = 0; i < TunnelBuildMessage.MAX_RECORD_COUNT; i++) {
+                if (i == ibepSlot)
+                    otbrm.setPlaintextRecord(i, 0);
+                else
+                    otbrm.setRecord(i, msg.getRecord(i));
+            }
+            reply = otbrm;
+        } else {
+            reply = new TunnelBuildReplyMessage(ctx);
+            for (int i = 0; i < TunnelBuildMessage.MAX_RECORD_COUNT; i++) {
+                reply.setRecord(i, msg.getRecord(i));
+            }
+        }
 
         int statuses[] = (new BuildReplyHandler(ctx)).decrypt(reply, cfg, order);
         if (statuses == null) throw new RuntimeException("bar");
@@ -154,6 +214,7 @@ public class BuildMessageTestStandalone extends TestCase {
         log.debug("\n================================================================" +
                   "\nAll peers agree? " + allAgree + 
                   "\n================================================================");
+        assertTrue("All peers agree", allAgree);
     }
     
     private static final List<Integer> pickOrder() {
@@ -224,6 +285,7 @@ public class BuildMessageTestStandalone extends TestCase {
         try {
             test.x_testBuildMessage(ctx, 1);
             test.x_testBuildMessage(ctx, 2);
+            test.x_testBuildMessage(ctx, 3);
         } finally {
             ctx.logManager().flush();
         }