diff --git a/router/java/src/net/i2p/data/i2np/GarlicClove.java b/router/java/src/net/i2p/data/i2np/GarlicClove.java
index a0e395a8394068c3ea2af38190af1c4c865705fa..218f23579359dba135b4a384dbbf24bfba2ba8dc 100644
--- a/router/java/src/net/i2p/data/i2np/GarlicClove.java
+++ b/router/java/src/net/i2p/data/i2np/GarlicClove.java
@@ -30,7 +30,6 @@ import net.i2p.util.Log;
  */
 public class GarlicClove extends DataStructureImpl {
 
-    //private final Log _log;
     private static final long serialVersionUID = 1L;
     private transient final I2PAppContext _context;
     private DeliveryInstructions _instructions;
@@ -41,7 +40,6 @@ public class GarlicClove extends DataStructureImpl {
     
     public GarlicClove(I2PAppContext context) {
         _context = context;
-        //_log = context.logManager().getLog(GarlicClove.class);
         _cloveId = -1;
     }
     
@@ -66,14 +64,12 @@ public class GarlicClove extends DataStructureImpl {
     }
 
     /**
-     *
+     *  @return length read
      */
     public int readBytes(byte source[], int offset) throws DataFormatException {
         int cur = offset;
         _instructions = DeliveryInstructions.create(source, offset);
         cur += _instructions.getSize();
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("Read instructions: " + _instructions);
         try {
             I2NPMessageHandler handler = new I2NPMessageHandler(_context);
             cur += handler.readMessage(source, cur);
@@ -85,17 +81,31 @@ public class GarlicClove extends DataStructureImpl {
         cur += 4;
         _expiration = DataHelper.fromDate(source, cur);
         cur += DataHelper.DATE_LENGTH;
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("CloveID read: " + _cloveId + " expiration read: " + _expiration);
-        //_certificate = new Certificate();
-        //cur += _certificate.readBytes(source, cur);
         _certificate = Certificate.create(source, cur);
         cur += _certificate.size();
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("Read cert: " + _certificate);
         return cur - offset;
     }
 
+    /**
+     *  Short format for ECIES-Ratchet, saves 22 bytes.
+     *  NTCP2-style header, no ID, no separate expiration, no cert.
+     *
+     *  @since 0.9.44
+     */
+    public void readBytesRatchet(byte source[], int offset, int len) throws DataFormatException {
+        _instructions = DeliveryInstructions.create(source, offset);
+        int isz = _instructions.getSize();
+        try {
+            I2NPMessageHandler handler = new I2NPMessageHandler(_context);
+            _msg = I2NPMessageImpl.fromRawByteArrayNTCP2(_context, source, offset + isz, len - isz, handler);
+            _cloveId = _msg.getUniqueId();
+            _expiration = new Date(_msg.getMessageExpiration());
+            _certificate = Certificate.NULL_CERT;
+        } catch (I2NPMessageException ime) {
+            throw new DataFormatException("Unable to read the message from a garlic clove", ime);
+        }
+    }
+
     /**
      *  @deprecated unused, use byte array method to avoid copying
      *  @throws UnsupportedOperationException always
@@ -111,16 +121,8 @@ public class GarlicClove extends DataStructureImpl {
     @Override
     public byte[] toByteArray() {
         byte rv[] = new byte[estimateSize()];
-        int offset = 0;
-        offset += _instructions.writeBytes(rv, offset);
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("Wrote instructions: " + _instructions);
-        //offset += _msg.toByteArray(rv);
-        try {
-            byte m[] = _msg.toByteArray();
-            System.arraycopy(m, 0, rv, offset, m.length);
-            offset += m.length;
-        } catch (RuntimeException e) { throw new RuntimeException("Unable to write: " + _msg + ": " + e.getMessage()); }
+        int offset = _instructions.writeBytes(rv, 0);
+        offset = _msg.toByteArray(rv, offset);
         DataHelper.toLong(rv, offset, 4, _cloveId);
         offset += 4;
         DataHelper.toDate(rv, offset, _expiration.getTime());
@@ -132,6 +134,28 @@ public class GarlicClove extends DataStructureImpl {
         }
         return rv;
     }
+
+    /**
+     *  Short format for ECIES-Ratchet, saves 22 bytes.
+     *  NTCP2-style header, no ID, no separate expiration, no cert.
+     *
+     *  @return new offset
+     *  @since 0.9.44
+     */
+    public int writeBytesRatchet(byte[] tgt, int offset) {
+        // returns length written
+        offset += _instructions.writeBytes(tgt, offset);
+        // returns new offset
+        offset = _msg.toRawByteArrayNTCP2(tgt, offset);
+        return offset;
+    }
+
+    /**
+     *  @since 0.9.44
+     */
+    public int getSizeRatchet() {
+        return _instructions.getSize() + _msg.getMessageSize() - 7;
+    }
     
     public int estimateSize() {
         return _instructions.getSize()
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 6fef9901f8e98e3a7af3502d6073ff41003acc22..169f87ef5b426838550c40f5fb1a5c1902bcfc19 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/ECIESAEADEngine.java
@@ -19,6 +19,7 @@ import net.i2p.crypto.EncType;
 import net.i2p.crypto.HKDF;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.Base64;
+import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
@@ -26,7 +27,9 @@ import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
+import net.i2p.data.i2np.GarlicClove;
 import static net.i2p.router.crypto.ratchet.RatchetPayload.*;
+import net.i2p.router.message.CloveSet;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleByteCache;
 
@@ -122,7 +125,7 @@ public final class ECIESAEADEngine {
      *
      * @return decrypted data or null on failure
      */
-    public byte[] decrypt(byte data[], PrivateKey targetPrivateKey, RatchetSKM keyManager) throws DataFormatException {
+    public CloveSet decrypt(byte data[], PrivateKey targetPrivateKey, RatchetSKM keyManager) throws DataFormatException {
         if (targetPrivateKey.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
         if (data == null) {
@@ -139,7 +142,7 @@ public final class ECIESAEADEngine {
         System.arraycopy(data, 0, tag, 0, TAGLEN);
         RatchetSessionTag st = new RatchetSessionTag(tag);
         SessionKeyAndNonce key = keyManager.consumeTag(st);
-        byte decrypted[];
+        CloveSet decrypted;
         final boolean shouldDebug = _log.shouldDebug();
         if (key != null) {
             //if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st);
@@ -150,6 +153,7 @@ public final class ECIESAEADEngine {
             if (state != null) {
                 decrypted = decryptExistingSession(tag, data, key, targetPrivateKey);
             } else if (data.length >= MIN_NSR_SIZE) {
+              /**  TODO find the state
                 try {
                     state = state.clone();
                 } catch (CloneNotSupportedException e) {
@@ -158,6 +162,8 @@ public final class ECIESAEADEngine {
                     return null;
                 }
                 decrypted = decryptNewSessionReply(tag, data, state);
+               **/
+                decrypted = null;
             } else {
                 decrypted = null;
                 if (_log.shouldWarn())
@@ -210,7 +216,7 @@ public final class ECIESAEADEngine {
      * @param data 96 bytes minimum
      * @return null if decryption fails
      */
-    private byte[] decryptNewSession(byte data[], PrivateKey targetPrivateKey)
+    private CloveSet decryptNewSession(byte data[], PrivateKey targetPrivateKey)
                                      throws DataFormatException {
         HandshakeState state;
         try {
@@ -271,11 +277,16 @@ public final class ECIESAEADEngine {
         } catch (Exception e) {
             throw new DataFormatException("Msg 1 payload error", e);
         }
-        if (pc.cloveSet == null) {
+        if (pc.cloveSet.isEmpty()) {
             if (_log.shouldWarn())
                 _log.warn("No garlic block in NS payload");
         }
-        return pc.cloveSet;
+        int num = pc.cloveSet.size();
+        // return non-null even if zero cloves
+        GarlicClove[] arr = new GarlicClove[num];
+        // msg id and expiration not checked in GarlicMessageReceiver
+        CloveSet rv = new CloveSet(pc.cloveSet.toArray(arr), Certificate.NULL_CERT, 0, pc.datetime);
+        return rv;
     }
 
     /**
@@ -298,7 +309,7 @@ public final class ECIESAEADEngine {
      * @param state must have already been cloned
      * @return null if decryption fails
      */
-    private byte[] decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState state)
+    private CloveSet decryptNewSessionReply(byte[] tag, byte[] data, HandshakeState state)
                                           throws DataFormatException {
         // part 1 - handshake
         byte[] yy = new byte[KEYLEN];
@@ -361,11 +372,16 @@ public final class ECIESAEADEngine {
         }
         RatchetTagSet tagset_ab = new RatchetTagSet(_hkdf, new SessionKey(ck), new SessionKey(k_ab), 0, 0);
         RatchetTagSet tagset_ba = new RatchetTagSet(_hkdf, null, new SessionKey(ck), new SessionKey(k_ba), 0, 0, 5, 5);
-        if (pc.cloveSet == null) {
+        if (pc.cloveSet.isEmpty()) {
             if (_log.shouldWarn())
                 _log.warn("No garlic block in NSR payload");
         }
-        return pc.cloveSet;
+        int num = pc.cloveSet.size();
+        // return non-null even if zero cloves
+        GarlicClove[] arr = new GarlicClove[num];
+        // msg id and expiration not checked in GarlicMessageReceiver
+        CloveSet rv = new CloveSet(pc.cloveSet.toArray(arr), Certificate.NULL_CERT, 0, pc.datetime);
+        return rv;
     }
 
     /**
@@ -385,7 +401,7 @@ public final class ECIESAEADEngine {
      * @return decrypted data or null on failure
      *
      */
-    private byte[] decryptExistingSession(byte[] tag, byte[] data, SessionKeyAndNonce key, PrivateKey targetPrivateKey)
+    private CloveSet decryptExistingSession(byte[] tag, byte[] data, SessionKeyAndNonce key, PrivateKey targetPrivateKey)
                                           throws DataFormatException {
 // TODO decrypt in place?
         byte decrypted[] = decryptAEADBlock(tag, data, TAGLEN, data.length - TAGLEN, key, key.getNonce());
@@ -409,11 +425,16 @@ public final class ECIESAEADEngine {
         } catch (Exception e) {
             throw new DataFormatException("ES payload error", e);
         }
-        if (pc.cloveSet == null) {
+        if (pc.cloveSet.isEmpty()) {
             if (_log.shouldWarn())
                 _log.warn("No garlic block in ES payload");
         }
-        return pc.cloveSet;
+        int num = pc.cloveSet.size();
+        // return non-null even if zero cloves
+        GarlicClove[] arr = new GarlicClove[num];
+        // msg id and expiration not checked in GarlicMessageReceiver
+        CloveSet rv = new CloveSet(pc.cloveSet.toArray(arr), Certificate.NULL_CERT, 0, pc.datetime);
+        return rv;
     }
 
     /**
@@ -476,12 +497,11 @@ public final class ECIESAEADEngine {
      *
      * @param target public key to which the data should be encrypted. 
      * @param priv local private key to encrypt with, from the leaseset
-     * @param expiration only used for new session messages
      * @return encrypted data or null on failure
      *
      */
-    public byte[] encrypt(byte data[], PublicKey target, PrivateKey priv,
-                          RatchetSKM keyManager, long expiration) {
+    public byte[] encrypt(CloveSet cloves, PublicKey target, PrivateKey priv,
+                          RatchetSKM keyManager) {
         if (target.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
         if (Arrays.equals(target.getData(), NULLPK)) {
@@ -494,7 +514,7 @@ public final class ECIESAEADEngine {
         if (re == null) {
             if (_log.shouldDebug())
                 _log.debug("Encrypting as NS to " + target);
-            return encryptNewSession(data, target, priv, keyManager, expiration);
+            return encryptNewSession(cloves, target, priv, keyManager);
         }
 ////
         byte[] tagsetkey = new byte[32];
@@ -513,9 +533,9 @@ public final class ECIESAEADEngine {
                 return null;
             }
 // register state with skm
-            return encryptNewSessionReply(data, state, re.tag);
+            return encryptNewSessionReply(cloves, state, re.tag);
         }
-        byte rv[] = encryptExistingSession(data, target, re.key, re.tag);
+        byte rv[] = encryptExistingSession(cloves, target, re.key, re.tag);
         return rv;
     }
 
@@ -536,8 +556,8 @@ public final class ECIESAEADEngine {
      *
      * @return encrypted data or null on failure
      */
-    private byte[] encryptNewSession(byte data[], PublicKey target, PrivateKey priv,
-                                     RatchetSKM keyManager, long expiration) {
+    private byte[] encryptNewSession(CloveSet cloves, PublicKey target, PrivateKey priv,
+                                     RatchetSKM keyManager) {
         HandshakeState state;
         try {
             state = new HandshakeState(HandshakeState.PATTERN_ID_IK, HandshakeState.INITIATOR, _edhThread);
@@ -549,22 +569,11 @@ public final class ECIESAEADEngine {
         state.getLocalKeyPair().setPrivateKey(priv.getData(), 0);
         state.start();
 
-        int padlen = 1 + _context.random().nextInt(MAXPAD);
-        byte[] payload = new byte[BHLEN + padlen + BHLEN + 4 + BHLEN + data.length];
-        List<Block> blocks = new ArrayList<Block>(4);
-        Block block = new DateTimeBlock(expiration);
-        blocks.add(block);
-        block = new GarlicBlock(data);
-        blocks.add(block);
-        block = new PaddingBlock(_context, padlen);
-        blocks.add(block);
-        int payloadlen = createPayload(payload, 0, blocks);
-        if (payloadlen != payload.length)
-            throw new IllegalStateException("payload size mismatch");
+        byte[] payload = createPayload(cloves, cloves.getExpiration());
 
-        byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payloadlen + MACLEN];
+        byte[] enc = new byte[KEYLEN + KEYLEN + MACLEN + payload.length + MACLEN];
         try {
-            state.writeMessage(enc, 0, payload, 0, payloadlen);
+            state.writeMessage(enc, 0, payload, 0, payload.length);
         } catch (GeneralSecurityException gse) {
             if (_log.shouldWarn())
                 _log.warn("Encrypt fail NS", gse);
@@ -607,23 +616,14 @@ public final class ECIESAEADEngine {
      * @param state must have already been cloned
      * @return encrypted data or null on failure
      */
-    private byte[] encryptNewSessionReply(byte data[], HandshakeState state, RatchetSessionTag currentTag) {
+    private byte[] encryptNewSessionReply(CloveSet cloves, HandshakeState state, RatchetSessionTag currentTag) {
         byte[] tag = currentTag.getData();
         state.mixHash(tag, 0, TAGLEN);
 
-        int padlen = 1 + _context.random().nextInt(MAXPAD);
-        byte[] payload = new byte[BHLEN + padlen + BHLEN + data.length];
-        List<Block> blocks = new ArrayList<Block>(2);
-        Block block = new GarlicBlock(data);
-        blocks.add(block);
-        block = new PaddingBlock(_context, padlen);
-        blocks.add(block);
-        int payloadlen = createPayload(payload, 0, blocks);
-        if (payloadlen != payload.length)
-            throw new IllegalStateException("payload size mismatch");
+        byte[] payload = createPayload(cloves, 0);
 
         // part 1 - tag and empty payload
-        byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payloadlen + MACLEN];
+        byte[] enc = new byte[TAGLEN + KEYLEN + MACLEN + payload.length + MACLEN];
         System.arraycopy(tag, 0, enc, 0, TAGLEN);
         try {
             state.writeMessage(enc, TAGLEN, ZEROLEN, 0, 0);
@@ -685,19 +685,10 @@ public final class ECIESAEADEngine {
      * @param target unused, this is AEAD encrypt only using the session key and tag
      * @return encrypted data or null on failure
      */
-    private byte[] encryptExistingSession(byte data[], PublicKey target, SessionKeyAndNonce key,
+    private byte[] encryptExistingSession(CloveSet cloves, PublicKey target, SessionKeyAndNonce key,
                                           RatchetSessionTag currentTag) {
         byte rawTag[] = currentTag.getData();
-        int padlen = 1 + _context.random().nextInt(MAXPAD);
-        byte[] payload = new byte[BHLEN + padlen + BHLEN + data.length];
-        List<Block> blocks = new ArrayList<Block>(2);
-        Block block = new GarlicBlock(data);
-        blocks.add(block);
-        block = new PaddingBlock(_context, padlen);
-        blocks.add(block);
-        int payloadlen = createPayload(payload, 0, blocks);
-        if (payloadlen != payload.length)
-            throw new IllegalStateException("payload size mismatch");
+        byte[] payload = createPayload(cloves, 0);
         byte encr[] = encryptAEADBlock(rawTag, payload, key, key.getNonce());
         System.arraycopy(rawTag, 0, encr, 0, TAGLEN);
         return encr;
@@ -741,11 +732,8 @@ public final class ECIESAEADEngine {
     // payload stuff
     /////////////////////////////////////////////////////////
 
-    private void processPayload(byte[] payload, int length, boolean isHandshake) throws Exception {
-    }
-
     private class PLCallback implements RatchetPayload.PayloadCallback {
-        public byte[] cloveSet;
+        public final List<GarlicClove> cloveSet = new ArrayList<GarlicClove>(3);
         public long datetime;
 
         public void gotDateTime(long time) {
@@ -761,13 +749,10 @@ public final class ECIESAEADEngine {
                 _log.debug("Got OPTIONS block length " + options.length);
         }
 
-        public void gotGarlic(byte[] data, int off, int len) {
+        public void gotGarlic(GarlicClove clove) {
             if (_log.shouldDebug())
-                _log.debug("Got GARLIC block length " + len);
-            if (cloveSet != null)
-                throw new IllegalArgumentException("Multiple GARLIC blocks");
-            cloveSet = new byte[len];
-            System.arraycopy(data, off, cloveSet, 0, len);
+                _log.debug("Got GARLIC block");
+            cloveSet.add(clove);
         }
 
         public void gotTermination(int reason, long count) {
@@ -786,6 +771,38 @@ public final class ECIESAEADEngine {
         }
     }
 
+    /**
+     *  @param expiration if greater than zero, add a DateTime block
+     */
+    private byte[] createPayload(CloveSet cloves, long expiration) {
+        int count = cloves.getCloveCount();
+        int numblocks = count + 1;
+        if (expiration > 0)
+            numblocks++;
+        int len = 0;
+        List<Block> blocks = new ArrayList<Block>(numblocks);
+        if (expiration > 0) {
+            Block block = new DateTimeBlock(expiration);
+            blocks.add(block);
+            len += block.getTotalLength();
+        }
+        for (int i = 0; i < count; i++) {
+            GarlicClove clove = cloves.getClove(i);
+            Block block = new GarlicBlock(clove);
+            blocks.add(block);
+            len += block.getTotalLength();
+        }
+        int padlen = 1 + _context.random().nextInt(MAXPAD);
+        Block block = new PaddingBlock(_context, padlen);
+        blocks.add(block);
+        len += block.getTotalLength();
+        byte[] payload = new byte[len];
+        int payloadlen = createPayload(payload, 0, blocks);
+        if (payloadlen != len)
+            throw new IllegalStateException("payload size mismatch");
+        return payload;
+    }
+
     /**
      *  @return the new offset
      */
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
index b15dad7069d744762d04dea69d8c2f7b11253931..a1a1091f010a07ce0d3c65fd37b8715f515ae894 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/MuxedEngine.java
@@ -6,6 +6,7 @@ import net.i2p.crypto.EncType;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.PrivateKey;
 import net.i2p.router.RouterContext;
+import net.i2p.router.message.CloveSet;
 import net.i2p.util.Log;
 
 /**
@@ -28,11 +29,11 @@ public final class MuxedEngine {
      *
      * @return decrypted data or null on failure
      */
-    public byte[] decrypt(byte data[], PrivateKey elgKey, PrivateKey ecKey, MuxedSKM keyManager) throws DataFormatException {
+    public CloveSet decrypt(byte data[], PrivateKey elgKey, PrivateKey ecKey, MuxedSKM keyManager) throws DataFormatException {
         if (elgKey.getType() != EncType.ELGAMAL_2048 ||
             ecKey.getType() != EncType.ECIES_X25519)
             throw new IllegalArgumentException();
-        byte[] rv = null;
+        CloveSet rv = null;
         boolean tryElg = false;
         // See proposal 144
         if (data.length >= 128) {
@@ -41,10 +42,20 @@ public final class MuxedEngine {
                 tryElg = true;
         }
         // Always try ElG first, for now
-        if (tryElg)
-            rv = _context.elGamalAESEngine().decrypt(data, elgKey, keyManager.getElgSKM());
-        if (rv == null)
-            rv = _context.eciesEngine().decrypt(data, ecKey, keyManager.getECSKM());
+        if (tryElg) {
+            byte[] dec = _context.elGamalAESEngine().decrypt(data, elgKey, keyManager.getElgSKM());
+            if (dec != null) {
+                try {
+                    rv = _context.garlicMessageParser().readCloveSet(dec, 0);
+                } catch (DataFormatException dfe) {
+                    if (_log.shouldWarn())
+                        _log.warn("ElG decrypt failed, trying ECIES", dfe);
+                }
+            }
+        }
+        if (rv == null) {
+            rv  = _context.eciesEngine().decrypt(data, ecKey, keyManager.getECSKM());
+        }
         return rv;
     }
 }
diff --git a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
index 8ddb61f1f158e8367e40f86f3f70fc6bb695caaf..f4c3b25c91bdc4f8819955eb5f41d9c3ab751094 100644
--- a/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
+++ b/router/java/src/net/i2p/router/crypto/ratchet/RatchetPayload.java
@@ -8,6 +8,7 @@ import java.util.List;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.data.i2np.GarlicClove;
 import net.i2p.data.i2np.GarlicMessage;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.I2NPMessageException;
@@ -26,13 +27,13 @@ class RatchetPayload {
 
     private static final int BLOCK_DATETIME = 0;
     private static final int BLOCK_SESSIONID = 1;
-    private static final int BLOCK_GARLIC = 3;
     private static final int BLOCK_TERMINATION = 4;
     private static final int BLOCK_OPTIONS = 5;
     private static final int BLOCK_MSGNUM = 6;
     private static final int BLOCK_NEXTKEY = 7;
     private static final int BLOCK_ACKKEY = 8;
     private static final int BLOCK_REPLYDI = 9;
+    private static final int BLOCK_GARLIC = 11;
     private static final int BLOCK_PADDING = 254;
 
     /**
@@ -43,7 +44,7 @@ class RatchetPayload {
     public interface PayloadCallback {
         public void gotDateTime(long time) throws DataFormatException;
 
-        public void gotGarlic(byte[] data, int off, int len) throws DataFormatException;
+        public void gotGarlic(GarlicClove clove);
 
         /**
          *  @param isHandshake true only for message 3 part 2
@@ -111,7 +112,9 @@ class RatchetPayload {
                     break;
 
                 case BLOCK_GARLIC:
-                    cb.gotGarlic(payload, i, len);
+                    GarlicClove clove = new GarlicClove(ctx);
+                    clove.readBytesRatchet(payload, i, len);
+                    cb.gotGarlic(clove);
                     break;
 
                 case BLOCK_TERMINATION:
@@ -200,20 +203,19 @@ class RatchetPayload {
     }
 
     public static class GarlicBlock extends Block {
-        private byte[] d;
+        private final GarlicClove c;
 
-        public GarlicBlock(byte[] data) {
+        public GarlicBlock(GarlicClove clove) {
             super(BLOCK_GARLIC);
-            d = data;
+            c = clove;
         }
 
         public int getDataLength() {
-            return d.length;
+            return c.getSizeRatchet();
         }
 
         public int writeData(byte[] tgt, int off) {
-            System.arraycopy(d, 0, tgt, off, d.length);
-            return off + d.length;
+            return c.writeBytesRatchet(tgt, off);
         }
     }
 
diff --git a/router/java/src/net/i2p/router/message/CloveSet.java b/router/java/src/net/i2p/router/message/CloveSet.java
index 3b9fe3042009dde908bd9db92a477620fdad88cb..19e5c8d6e8d179603a27d22b82c79cc0ba8a5cb7 100644
--- a/router/java/src/net/i2p/router/message/CloveSet.java
+++ b/router/java/src/net/i2p/router/message/CloveSet.java
@@ -14,8 +14,9 @@ import net.i2p.data.i2np.GarlicClove;
 /**
  * Wrap up the data contained in a GarlicMessage after being decrypted
  *
+ * @since public since 0.9.44, was package private
  */
-class CloveSet {
+public class CloveSet {
     private final GarlicClove[] _cloves;
     private final Certificate _cert;
     private final long _msgId;
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
index 33982deab87cdd4f8d6b00146e6cb2007d304815..5321ad7f5d3b4b709e08bdce6cafedd725f72f5d 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
@@ -16,6 +16,7 @@ import java.util.Set;
 
 import net.i2p.crypto.EncType;
 import net.i2p.crypto.SessionKeyManager;
+import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
@@ -261,7 +262,7 @@ public class GarlicMessageBuilder {
             throw new IllegalArgumentException();
         Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
         GarlicMessage msg = new GarlicMessage(ctx);
-        byte cloveSet[] = buildCloveSet(ctx, config);
+        CloveSet cloveSet = buildECIESCloveSet(ctx, config);
         LeaseSetKeys lsk = ctx.keyManager().getKeys(from);
         if (lsk == null) {
             if (log.shouldWarn())
@@ -285,7 +286,7 @@ public class GarlicMessageBuilder {
                 log.warn("No SKM for " + from.toBase32());
             return null;
         }
-        byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm, config.getExpiration());
+        byte encData[] = ctx.eciesEngine().encrypt(cloveSet, target, priv, rskm);
         if (encData == null) {
             if (log.shouldWarn())
                 log.warn("Encrypt fail for " + from.toBase32());
@@ -300,8 +301,8 @@ public class GarlicMessageBuilder {
             return null;
         }
         if (log.shouldDebug())
-            log.debug("CloveSet (" + config.getCloveCount() + " cloves) for message " + msg.getUniqueId() + " is " + cloveSet.length
-                     + " bytes and encrypted message data is " + encData.length + " bytes");
+            log.debug("CloveSet (" + config.getCloveCount() + " cloves) for message " + msg.getUniqueId()
+                     + " encrypted message data is " + encData.length + " bytes");
         return msg;
     }
     
@@ -366,7 +367,7 @@ public class GarlicMessageBuilder {
         return baos.toByteArray();
     }
     
-    private static byte[] buildClove(RouterContext ctx, PayloadGarlicConfig config) throws DataFormatException, IOException {
+    private static byte[] buildClove(RouterContext ctx, PayloadGarlicConfig config) {
         GarlicClove clove = new GarlicClove(ctx);
         clove.setData(config.getPayload());
         return buildCommonClove(clove, config);
@@ -397,17 +398,52 @@ public class GarlicMessageBuilder {
         return buildCommonClove(clove, config);
     }
     
-    private static byte[] buildCommonClove(GarlicClove clove, GarlicConfig config) throws DataFormatException, IOException {
+    private static byte[] buildCommonClove(GarlicClove clove, GarlicConfig config) {
         clove.setCertificate(config.getCertificate());
         clove.setCloveId(config.getId());
         clove.setExpiration(new Date(config.getExpiration()));
         clove.setInstructions(config.getDeliveryInstructions());
         return clove.toByteArray();
-        /*
-        int size = clove.estimateSize();
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(size);
-        clove.writeBytes(baos);
-        return baos.toByteArray();
-         */
+    }
+    
+    /**
+     * Build the unencrypted GarlicMessage specified by the config.
+     * It contains the number of cloves, followed by each clove,
+     * followed by a certificate, ID, and expiration date.
+     *
+     * @throws IllegalArgumentException on error
+     * @since 0.9.44
+     */
+    private static CloveSet buildECIESCloveSet(RouterContext ctx, GarlicConfig config) {
+        GarlicClove[] arr;
+        if (config instanceof PayloadGarlicConfig) {
+            GarlicClove clove = buildECIESClove(ctx, (PayloadGarlicConfig)config);
+            arr = new GarlicClove[1];
+            arr[0] = clove;
+        } else {
+            int cnt = config.getCloveCount();
+            arr = new GarlicClove[cnt];
+            for (int i = 0; i < cnt; i++) {
+                GarlicConfig c = config.getClove(i);
+                if (c instanceof PayloadGarlicConfig) {
+                    arr[i] = buildECIESClove(ctx, (PayloadGarlicConfig)c);
+                } else {
+                    throw new IllegalArgumentException("Subclove IS NOT a payload garlic clove");
+                }
+            }
+        }
+        // GarlicConfig cert, ID, and expiration all ignored here
+        CloveSet rv = new CloveSet(arr, Certificate.NULL_CERT, config.getId(), config.getExpiration());
+        return rv;
+    }
+    
+    private static GarlicClove buildECIESClove(RouterContext ctx, PayloadGarlicConfig config) {
+        GarlicClove clove = new GarlicClove(ctx);
+        clove.setData(config.getPayload());
+        clove.setCertificate(config.getCertificate());
+        clove.setCloveId(config.getId());
+        clove.setExpiration(new Date(config.getExpiration()));
+        clove.setInstructions(config.getDeliveryInstructions());
+        return clove;
     }
 }
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageParser.java b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
index 3608fc5b5a1ebe652e82a44c388ad15416dcc669..f4691641ea1eb2b3608495adac2faa610f47f568 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageParser.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
@@ -70,13 +70,15 @@ public class GarlicMessageParser {
                         _log.warn("No SKM to decrypt ECIES");
                     return null;
                 }
-                decrData = _context.eciesEngine().decrypt(encData, encryptionKey, rskm);
-                if (decrData != null) {
+                CloveSet rv = _context.eciesEngine().decrypt(encData, encryptionKey, rskm);
+                if (rv != null) {
                     if (_log.shouldWarn())
-                        _log.warn("ECIES decrypt success, length: " + decrData.length);
+                        _log.warn("ECIES decrypt success, cloves: " + rv.getCloveCount());
+                    return rv;
                 } else {
                     if (_log.shouldWarn())
                         _log.warn("ECIES decrypt fail");
+                    return null;
                 }
             } else {
                 if (_log.shouldWarn())
@@ -108,10 +110,13 @@ public class GarlicMessageParser {
     }
     
     /**
+     *  ElGamal only
+     *
      *  @param offset where in data to start
      *  @return non-null, throws on all errors
+     *  @since public since 0.9.44
      */
-    private CloveSet readCloveSet(byte data[], int offset) throws DataFormatException {
+    public CloveSet readCloveSet(byte data[], int offset) throws DataFormatException {
         int numCloves = data[offset] & 0xff;
         offset++;
         //if (_log.shouldLog(Log.DEBUG))