Repository: hyperledger/quilt
Pull request #446 (2020-04-09 22:36:19)

Fix Array Allocation
* Fixes a bug in the ASN.1 OER Codecs framework that incorrectly allocates an array.
* Update SendMoney example.
* Improve unit test coverage.

Signed-off-by: David Fuelling <sappenin@gmail.com>

lgtm-com[bot]: This pull request **introduces 1 alert** when merging 092e97876dfb41751b798124f53d65b42be8defa into d3d4aa7e9fa70eb773d658861d1899dd356be74a - [view on LGTM.com](https://lgtm.com/projects/g/hyperledger/quilt/rev/pr-210c15ab6e3ac24896d98e800abeafde16e260f1)

**new alerts:**

* 1 for Potential input resource leak
lgtm-com[bot]: This pull request **introduces 1 alert** when merging fb1aec77d18db2f21a5c49a294889b94b5da6fee into d3d4aa7e9fa70eb773d658861d1899dd356be74a - [view on LGTM.com](https://lgtm.com/projects/g/hyperledger/quilt/rev/pr-291e1d65020cbdc2ff24c506564d09e94223bf72)

**new alerts:**

* 1 for Potential input resource leak
lgtm-com[bot]: This pull request **introduces 1 alert** when merging 9be6b8a0a23d44926ae58b73e19dac75632f5c0f into d3d4aa7e9fa70eb773d658861d1899dd356be74a - [view on LGTM.com](https://lgtm.com/projects/g/hyperledger/quilt/rev/pr-d68412738c1ffda9d03383c0cf2d57c3c23100e9)

**new alerts:**

* 1 for Potential input resource leak

Patch:
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnCharStringBasedObjectCodec.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnCharStringBasedObjectCodec.java
index c0955ac..9135e4f 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnCharStringBasedObjectCodec.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnCharStringBasedObjectCodec.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.codecs;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -32,18 +32,16 @@ import java.util.function.Predicate;
  * ASN.1 object codec that uses a character string as an intermediary encoding.
  *
  * <p>This base class should be used for codecs where the ASN.1 object is a subclass of an ASN.1
- * string type like IA5String or UTF8String (although the specific subclasses
- * {@link AsnIA5StringBasedObjectCodec} and {@link AsnUtf8StringBasedObjectCodec} should be used in
- * the case of those two types).
+ * string type like IA5String or UTF8String (although the specific subclasses {@link AsnIA5StringBasedObjectCodec} and
+ * {@link AsnUtf8StringBasedObjectCodec} should be used in the case of those two types).
  *
  * <p>Codecs that extend this object can call {@link #getCharString()} and
- * {@link #setCharString(String)} in their implementations of {@link #decode()} and
- * {@link #encode(Object)} respectively.
+ * {@link #setCharString(String)} in their implementations of {@link #decode()} and {@link #encode(Object)}
+ * respectively.
  *
  * <p>The serializers for this object must call {@link #getCharString()} and
- * {@link #setCharString(String)} when reading from a stream. The values passed and returned must
- * not contain any length prefixes or tags.
- *
+ * {@link #setCharString(String)} when reading from a stream. The values passed and returned must not contain any length
+ * prefixes or tags.
  */
 public abstract class AsnCharStringBasedObjectCodec<T> extends AsnPrimitiveCodec<T> {
 
@@ -52,26 +50,38 @@ public abstract class AsnCharStringBasedObjectCodec<T> extends AsnPrimitiveCodec
   private String charString;
 
   /**
-   * Default constructor.
+   * Required args Constructor.
    *
-   * @param sizeConstraint The size constraint for this ASN.1 object.
-   * @param characterSet   The character set to use when encoding and decoding this string from
-   *                       the byte stream.
+   * @param fixedSizeConstraint A single integer that represents the min/max value of this codec.
+   * @param characterSet        The character set to use when encoding and decoding this string from the byte stream.
    */
-  public AsnCharStringBasedObjectCodec(AsnSizeConstraint sizeConstraint, Charset characterSet) {
-    super(sizeConstraint);
-    this.characterSet = characterSet;
-  }
-
-
   public AsnCharStringBasedObjectCodec(int fixedSizeConstraint, Charset characterSet) {
     this(new AsnSizeConstraint(fixedSizeConstraint), characterSet);
   }
 
+  /**
+   * Required args Constructor.
+   *
+   * @param minSize      The minimum size of the string this codec can handle.
+   * @param maxSize      The maximum size of the string this codec can handle.
+   * @param characterSet The character set to use when encoding and decoding this string from the byte stream.
+   */
   public AsnCharStringBasedObjectCodec(int minSize, int maxSize, Charset characterSet) {
     this(new AsnSizeConstraint(minSize, maxSize), characterSet);
   }
 
+  /**
+   * Default constructor.
+   *
+   * @param sizeConstraint The size constraint for this ASN.1 object.
+   * @param characterSet   The character set to use when encoding and decoding this string from the byte stream.
+   */
+  public AsnCharStringBasedObjectCodec(final AsnSizeConstraint sizeConstraint, final Charset characterSet) {
+    super(sizeConstraint);
+    this.characterSet = Objects.requireNonNull(characterSet);
+  }
+
+
   public Charset getCharacterSet() {
     return characterSet;
   }
@@ -95,10 +105,11 @@ public abstract class AsnCharStringBasedObjectCodec<T> extends AsnPrimitiveCodec
    * to set String that should be written by the serializer.
    *
    * <p>The provided String will be validated both against the codec size constraint and the
-   * {@link Predicate} set in {@link #setValidator(Predicate)} (if any). If the String
-   * fails validation a {@link CodecException} will be thrown.
+   * {@link Predicate} set in {@link #setValidator(Predicate)} (if any). If the String fails validation a {@link
+   * CodecException} will be thrown.
    *
    * @param charString the {@link String} representing the value of this object
+   *
    * @throws CodecException if the provided charString fails validation
    */
   public final void setCharString(String charString) {
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodec.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodec.java
index 4770590..bb4a359 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodec.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodec.java
@@ -97,22 +97,14 @@ public abstract class AsnOctetStringBasedObjectCodec<T> extends AsnPrimitiveCode
       return;
     }
 
-    if (getSizeConstraint().isFixedSize()) {
-      if (bytes.length != getSizeConstraint().getMax()) {
-        throw new CodecException(format("Invalid octet string length. Expected %s, got %s",
-            getSizeConstraint().getMax(), bytes.length));
-      }
-    } else {
-      if (bytes.length < getSizeConstraint().getMin()) {
-        throw new CodecException(format("Invalid octet string length. Expected > %s, got %s",
-            getSizeConstraint().getMin(), bytes.length));
-      }
-      if (bytes.length > getSizeConstraint().getMax()) {
-        throw new CodecException(format("Invalid octet string length. Expected < %s, got %s",
-            getSizeConstraint().getMax(), bytes.length));
-      }
+    if (bytes.length < getSizeConstraint().getMin()) {
+      throw new CodecException(format("Invalid octet string length. Expected > %s, got %s",
+          getSizeConstraint().getMin(), bytes.length));
+    }
+    if (bytes.length > getSizeConstraint().getMax()) {
+      throw new CodecException(format("Invalid octet string length. Expected < %s, got %s",
+          getSizeConstraint().getMax(), bytes.length));
     }
-
   }
 
   @Override
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrimitiveCodec.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrimitiveCodec.java
index 8ba5dbd..dfa23fe 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrimitiveCodec.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrimitiveCodec.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.codecs;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -21,6 +21,8 @@ package org.interledger.encoding.asn.codecs;
  */
 
 
+import java.util.Objects;
+
 /**
  * A base for codecs for primitive ASN.1 types.
  */
@@ -28,15 +30,15 @@ public abstract class AsnPrimitiveCodec<T> extends AsnObjectCodecBase<T> {
 
   private final AsnSizeConstraint sizeConstraint;
 
-  public AsnPrimitiveCodec(AsnSizeConstraint sizeConstraint) {
-    this.sizeConstraint = sizeConstraint;
+  public AsnPrimitiveCodec(final AsnSizeConstraint sizeConstraint) {
+    this.sizeConstraint = Objects.requireNonNull(sizeConstraint);
   }
 
-  public AsnPrimitiveCodec(int fixedSizeConstraint) {
+  public AsnPrimitiveCodec(final int fixedSizeConstraint) {
     this.sizeConstraint = new AsnSizeConstraint(fixedSizeConstraint);
   }
 
-  public AsnPrimitiveCodec(int minSize, int maxSize) {
+  public AsnPrimitiveCodec(final int minSize, final int maxSize) {
     this.sizeConstraint = new AsnSizeConstraint(minSize, maxSize);
   }
 
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrintableStringBasedObjectCodec.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrintableStringBasedObjectCodec.java
index e40c0f5..7115729 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrintableStringBasedObjectCodec.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnPrintableStringBasedObjectCodec.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.codecs;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -27,8 +27,7 @@ import java.util.regex.Pattern;
 /**
  * An ASN.1 codec for ASN.1 objects that extend PrintableString
  */
-public abstract class AsnPrintableStringBasedObjectCodec<T>
-    extends AsnCharStringBasedObjectCodec<T> {
+public abstract class AsnPrintableStringBasedObjectCodec<T> extends AsnCharStringBasedObjectCodec<T> {
 
   private static final String REGEX = "[\\p{Alnum}'()+,-.?:/= ]+";
   private static final Predicate<String> MATCHER = Pattern.compile(REGEX).asPredicate();
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodec.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodec.java
index 6f527e8..ad6dad6 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodec.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodec.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.codecs;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,40 +25,97 @@ import java.util.List;
 import java.util.Objects;
 import java.util.function.Supplier;
 
+/**
+ * A CoDec for ASN.1 OER SEQUENCE-OF-SEQUENCE types (i.e., the mechanism used by ASN.1 OER to encode an array of
+ * objects).
+ *
+ * @param <L> The type of {@link List} this codec stores its sub-codecs in.
+ * @param <T> The type of object in the {@link List} of codecs this SEQUENCE-OF codec uses.
+ *
+ * @see "https://www.oss.com/asn1/resources/books-whitepapers-pubs/Overview_of_OER.pdf"
+ */
 public class AsnSequenceOfSequenceCodec<L extends List<T>, T> extends AsnObjectCodecBase<L> {
 
-  private final Supplier<AsnSequenceCodec<T>> supplier;
-  private final Supplier<L> listSupplier;
-  private ArrayList<AsnSequenceCodec<T>> codecs;
+  private static final int CODECS_ARRAY_INITIAL_CAPACITY = 5;
+
+  private final Supplier<L> listConstructor;
+  private final Supplier<AsnSequenceCodec<T>> subCodecSupplier;
+
+  private volatile ArrayList<AsnSequenceCodec<T>> codecs;
 
+  /**
+   * Required-args constructor. Allows a caller to specify a {@link Supplier} of a {@link List} of objects to encode or
+   * decode, using potentially discrete codecs in an array such that for each index of the list, the identically indexed
+   * codec is used.
+   *
+   * @param listConstructor  A {@link Supplier} of type {@link L}, which is a Codec, that can be used to construct a new
+   *                         {@link List}.
+   * @param subCodecSupplier A {@link Supplier} of type {@link AsnSequenceCodec} of type {@link T}.
+   */
   public AsnSequenceOfSequenceCodec(
-      Supplier<L> listSupplier,
-      Supplier<AsnSequenceCodec<T>> sequenceCodecSupplier) {
-    this.supplier = sequenceCodecSupplier;
-    this.listSupplier = listSupplier;
+      final Supplier<L> listConstructor, final Supplier<AsnSequenceCodec<T>> subCodecSupplier
+  ) {
+    this.listConstructor = Objects.requireNonNull(listConstructor);
+    this.subCodecSupplier = Objects.requireNonNull(subCodecSupplier);
+
+    // A SEQUENCE-OF-SEQUENCE must always have the same type of SEQUENCE in its collection, so the codec used for each
+    // SEQUENCE in an Array of SEQUENCES must always be the same type. That said, due to the unfortunate design of this
+    // implementation, there must be a unique instance of a sub-codec for each item in the listSupplier. Despite this
+    // limitation, we can construct a reasonably small array here in order to covers our happy-path (e.g., a typical
+    // use-case for this functionality is the encoding/decoding of STREAM frames, and there generally aren't more than
+    // a few frames in any given STREAM packet). Therefore, this implementation starts with a small array, but allows
+    // the array to grow dynamically as needed.
+    // This design preferable to simply constructing an array with a length equal to the length supplied in the ASN.1
+    // OER packet, because this could lead to a potential DOS attack if an attacker were to maliciously specify an
+    // artificially long length value.
+    // For example, a malicious packet creator could specify a length value of Integer.MAX_VALUE, which a naive decoder
+    // would use to initialize an array, thus accidentially allocating 2GB of JVM heap. This likely would result in an
+    // OutOfMemoryError depending on the default heap size of the JVM. Instead, the chosen design which dynamically
+    // allocates the array based upon actual byte counts avoids this potential attack vector.
+    this.codecs = new ArrayList<>(CODECS_ARRAY_INITIAL_CAPACITY);
   }
 
+  /**
+   * <p>Return the number of sub-codecs currently allocated in this Codec.</p>
+   *
+   * <p>While this size-value can theoretically change at runtime, by the time a serializer is calling `write` on this
+   * codec, no more sub-codecs will be added to this instance, which means we can adjust the contents of {@link #codecs}
+   * in a just-in-time fashion in response to {@link #getCodecAt(int)}. This design, while imperfect, is a legacy of the
+   * original design of our Codec system, which puts state into each sub-codec. This will be addressed and  improved
+   * once the Codec system becomes less stateful.</p>
+   *
+   * @return An integer representing the number of sub-codecs in this instance.
+   *
+   * @see "https://github.com/hyperledger/quilt/issues/164"
+   */
   public int size() {
-    Objects.requireNonNull(codecs);
     return this.codecs.size();
   }
 
-
   /**
-   * Set the size of the sequence. This must be called before {@link #getCodecAt(int)}.
+   * <p>Accessor for the codec at index {@code index}.</p>
+   *
+   * <p>This implementation uses a just-in-time construction mechanism for each sub-codec as an optimization, for two
+   * reasons. First, each sub-codec must have its own unique state, so we can't just use a single instance of the
+   * sub-codec for all indices. Second, the way the Sub-codecs work is the sub-codec is not actually used until it is
+   * accessed via this method. Thus, constructing each instance via the supplier is acceptable.</p>
    *
-   * @param size size of the sequence.
+   * @param index The index of the codec to retrieve. Index corresponds to the index of the item in the SEQUENCE-OF that
+   *              is about to be encoded/decoded.
+   *
+   * @return An {@link AsnSequenceCodec} for encoding/decoding a particular item (encoded in ASN.1 OER) in a SEQUENCE-OF
+   *     collection.
    */
-  public void setSize(int size) {
-    this.codecs = new ArrayList<>(size);
-    for (int i = 0; i < size; i++) {
-      this.codecs.add(i, supplier.get());
-    }
-  }
-
-  public AsnSequenceCodec<T> getCodecAt(int index) {
+  public AsnSequenceCodec<T> getCodecAt(final int index) {
     Objects.requireNonNull(codecs);
-    return this.codecs.get(index);
+    final AsnSequenceCodec<T> subCodec;
+    if (codecs.size() == 0 || codecs.size() <= index) {
+      subCodec = subCodecSupplier.get();
+      codecs.add(index, subCodec);
+    } else {
+      subCodec = codecs.get(index);
+    }
+    return subCodec;
   }
 
   /**
@@ -69,7 +126,7 @@ public class AsnSequenceOfSequenceCodec<L extends List<T>, T> extends AsnObjectC
   @Override
   public L decode() {
     Objects.requireNonNull(codecs);
-    L list = listSupplier.get();
+    L list = listConstructor.get();
     for (AsnSequenceCodec<T> codec : codecs) {
       list.add(codec.decode());
     }
@@ -82,10 +139,11 @@ public class AsnSequenceOfSequenceCodec<L extends List<T>, T> extends AsnObjectC
    * @param values the value to encode
    */
   @Override
-  public void encode(L values) {
-    this.codecs = new ArrayList<>(values.size());
+  public void encode(final L values) {
+    Objects.requireNonNull(values);
+    this.codecs = new ArrayList<>(CODECS_ARRAY_INITIAL_CAPACITY);
     for (T value : values) {
-      AsnSequenceCodec<T> codec = supplier.get();
+      AsnSequenceCodec<T> codec = subCodecSupplier.get();
       codec.encode(value);
       this.codecs.add(codec);
     }
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectCodecRegistry.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectCodecRegistry.java
index 58fbc03..18ddaf9 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectCodecRegistry.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectCodecRegistry.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.framework;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,8 +25,8 @@ import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * A registry of {@link Class} to {@link AsnObjectCodec} mappings that can provide a new instance of
- * an {@link AsnObjectCodec} that can encode/decode a given type.
+ * A registry of {@link Class} to {@link AsnObjectCodec} mappings that can provide a new instance of an {@link
+ * AsnObjectCodec} that can encode/decode a given type.
  */
 public class AsnObjectCodecRegistry {
 
@@ -43,12 +43,13 @@ public class AsnObjectCodecRegistry {
    * Register a new ASN.1 codec that can encode/decode the given type.
    *
    * @param type     An instance of {@link Class}.
-   * @param supplier The {@link AsnObjectCodecSupplier} that will produce new
-   *                 {@link AsnObjectCodec} instances as required.
+   * @param supplier The {@link AsnObjectCodecSupplier} that will produce new {@link AsnObjectCodec} instances as
+   *                 required.
    * @param <T>      The type of object that can be encoded/decoded by the given codec.
+   *
    * @return this {@link AsnObjectCodecRegistry} to allow chaining calls to this method.
    */
-  public <T> AsnObjectCodecRegistry register(Class<T> type, AsnObjectCodecSupplier<T> supplier) {
+  public <T> AsnObjectCodecRegistry register(final Class<T> type, final AsnObjectCodecSupplier<T> supplier) {
     Objects.requireNonNull(type);
     Objects.requireNonNull(supplier);
 
@@ -61,7 +62,7 @@ public class AsnObjectCodecRegistry {
    * Get a new {@link AsnObjectCodec} instance to encode/decode the specified {@code type}.
    *
    * @param type An instance of {@link Class}.
-   * @param <T> the type of object that can be encoded/decoded by the returned codec
+   * @param <T>  the type of object that can be encoded/decoded by the returned codec
    *
    * @return a codec for encoding/decoding objects of type {@code T}
    */
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializationContext.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializationContext.java
index 2a5fae6..170ae34 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializationContext.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializationContext.java
@@ -20,7 +20,6 @@ package org.interledger.encoding.asn.framework;
  * =========================LICENSE_END==================================
  */
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -51,15 +50,13 @@ public class AsnObjectSerializationContext {
    *
    * @param type       An instance of {@link Class}.
    * @param serializer An instance of {@link AsnObjectSerializer}.
-   * @param <T>        The type of {@link AsnObjectCodec} that can be serialized by this
-   *                   serializer.
+   * @param <T>        The type of {@link AsnObjectCodec} that can be serialized by this serializer.
    *
    * @return A {@link AsnObjectSerializationContext} for the supplied {@code type}.
    */
   public <T extends AsnObjectCodec> AsnObjectSerializationContext register(
-      final Class<T> type,
-      final AsnObjectSerializer<? super T> serializer) {
-
+      final Class<T> type, final AsnObjectSerializer<? super T> serializer
+  ) {
     Objects.requireNonNull(type);
     Objects.requireNonNull(serializer);
 
@@ -70,17 +67,14 @@ public class AsnObjectSerializationContext {
   /**
    * Read an deserialize an ASN.1 object from a stream.
    *
-   * @param instance    An instance of {@link AsnObjectCodec} that will be populated with the
-   *                    deserialized data.
-   * @param inputStream An instance of {@link InputStream} that contains bytes in a certain
-   *                    encoding.
+   * @param instance    An instance of {@link AsnObjectCodec} that will be populated with the deserialized data.
+   * @param inputStream An instance of {@link InputStream} that contains bytes in a certain encoding.
    *
    * @return this {@link AsnObjectSerializationContext} for further operations.
    *
    * @throws IOException If anything goes wrong reading from the {@code buffer}.
    */
-  public AsnObjectSerializationContext read(final AsnObjectCodec instance,
-      final InputStream inputStream)
+  public AsnObjectSerializationContext read(final AsnObjectCodec instance, final InputStream inputStream)
       throws IOException {
     Objects.requireNonNull(instance);
     Objects.requireNonNull(inputStream);
@@ -88,43 +82,17 @@ public class AsnObjectSerializationContext {
     return this;
   }
 
-  /**
-   * Read an deserialize an ASN.1 object from a byte array.
-   *
-   * <p>NOTE: This methods wraps IOExceptions in RuntimeExceptions.
-   *
-   * @param instance An instance of {@link AsnObjectCodec} that will be populated with the
-   *                 deserialized data.
-   * @param data     An instance of byte array that contains bytes in a certain encoding.
-   *
-   * @return this {@link AsnObjectSerializationContext} for further operations.
-   */
-  public AsnObjectSerializationContext read(final AsnObjectCodec instance, final byte[] data) {
-    Objects.requireNonNull(instance);
-    Objects.requireNonNull(data);
-
-    try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
-      read(instance, bais);
-    } catch (IOException e) {
-      throw new CodecException("Unable to decode " + instance.getClass().getCanonicalName(), e);
-    }
-
-    return this;
-  }
-
   /**
    * Write the decoded value of an {@link AsnObjectCodec} to the supplied {@link OutputStream}.
    *
-   * @param instance     An instance of {@link AsnObjectCodec} that will encode the value to be
-   *                     serialized.
+   * @param instance     An instance of {@link AsnObjectCodec} that will encode the value to be serialized.
    * @param outputStream An instance of {@link OutputStream} that will be written to.
    *
    * @return this {@link AsnObjectSerializationContext} for further operations.
    *
    * @throws IOException If anything goes wrong while writing to the {@link OutputStream}
    */
-  public AsnObjectSerializationContext write(final AsnObjectCodec instance,
-      final OutputStream outputStream)
+  public AsnObjectSerializationContext write(final AsnObjectCodec instance, final OutputStream outputStream)
       throws IOException {
     Objects.requireNonNull(instance);
     Objects.requireNonNull(outputStream);
@@ -135,11 +103,9 @@ public class AsnObjectSerializationContext {
   }
 
   /**
-   * Write the decoded value of an {@link AsnObjectCodec} to an in-memory stream and return the
-   * bytes.
+   * Write the decoded value of an {@link AsnObjectCodec} to an in-memory stream and return the bytes.
    *
-   * @param instance An instance of {@link AsnObjectCodec} that will encode the value to be
-   *                 serialized.
+   * @param instance An instance of {@link AsnObjectCodec} that will encode the value to be serialized.
    *
    * @return The serialized object.
    */
@@ -172,9 +138,7 @@ public class AsnObjectSerializationContext {
   }
 
   // Visible for testing...
-  protected <T extends AsnObjectCodec> AsnObjectSerializer<T> tryGetSerializerForCodec(
-      final Class<T> type
-  ) {
+  protected <T extends AsnObjectCodec> AsnObjectSerializer<T> tryGetSerializerForCodec(final Class<T> type) {
     Objects.requireNonNull(type);
 
     AsnObjectSerializer<T> serializer;
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializer.java
index 92622a2..0c4b93c 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/AsnObjectSerializer.java
@@ -30,8 +30,7 @@ import java.io.OutputStream;
 public interface AsnObjectSerializer<T extends AsnObjectCodec> {
 
   /**
-   * Read an ASN.1 object from the buffer according to the rules defined in the {@link
-   * AsnObjectSerializationContext}.
+   * Read an ASN.1 object from the buffer according to the rules defined in the {@link AsnObjectSerializationContext}.
    *
    * @param context     An instance of {@link AsnObjectSerializationContext}.
    * @param instance    An instance of {@link AsnObjectCodec} to read the data into.
@@ -39,12 +38,10 @@ public interface AsnObjectSerializer<T extends AsnObjectCodec> {
    *
    * @throws IOException if anything goes wrong reading from the {@link InputStream}.
    */
-  void read(AsnObjectSerializationContext context, T instance, InputStream inputStream)
-      throws IOException;
+  void read(AsnObjectSerializationContext context, T instance, InputStream inputStream) throws IOException;
 
   /**
-   * Write an object to the {@code outputStream} according to the rules defined in the {@code
-   * context}.
+   * Write an object to the {@code outputStream} according to the rules defined in the {@code context}.
    *
    * @param context      An instance of {@link AsnObjectSerializationContext}.
    * @param instance     An instance of {@link AsnObjectCodec} that is being serialized.
@@ -52,7 +49,6 @@ public interface AsnObjectSerializer<T extends AsnObjectCodec> {
    *
    * @throws IOException If anything goes wrong writing to the {@link OutputStream}
    */
-  void write(AsnObjectSerializationContext context, T instance, OutputStream outputStream)
-      throws IOException;
+  void write(AsnObjectSerializationContext context, T instance, OutputStream outputStream) throws IOException;
 
 }
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/CodecContext.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/CodecContext.java
index 6ee0940..0ebbcbf 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/CodecContext.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/framework/CodecContext.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.framework;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -40,16 +40,17 @@ public class CodecContext {
   }
 
   /**
-   * Register a mapping between an object and an {@link AsnObjectCodec} supplier that can be used to
-   * encode/decode the object into it's ASN.1 representation.
+   * Register a mapping between an object and an {@link AsnObjectCodec} supplier that can be used to encode/decode the
+   * object into it's ASN.1 representation.
    *
    * @param type     The type of object that will be encoded/decoded and serialized/deserialized
    * @param supplier A {@link Supplier} that creates new ASN.1 codecs for use by the serializer
    * @param <T>      The type of object that will be encoded/decoded and serialized/deserialized
    *
    * @return this object so that calls to register can be chained together.
-   * @throws CodecException if there is no serializer available for the given {@link AsnObjectCodec}
-   *                        that is provided by the {@code supplier}.
+   *
+   * @throws CodecException if there is no serializer available for the given {@link AsnObjectCodec} that is provided by
+   *                        the {@code supplier}.
    */
   public <T> CodecContext register(Class<T> type, AsnObjectCodecSupplier<T> supplier) {
     Objects.requireNonNull(type);
@@ -65,23 +66,23 @@ public class CodecContext {
   }
 
   /**
-   * Register a mapping between an object and an {@link AsnObjectCodec} supplier that can be used to
-   * encode/decode the object into it's ASN.1 representation and also register the serializer that
-   * is to be used for serializing these objects.
+   * Register a mapping between an object and an {@link AsnObjectCodec} supplier that can be used to encode/decode the
+   * object into it's ASN.1 representation and also register the serializer that is to be used for serializing these
+   * objects.
    *
-   * @param type       The type of the objects that must be encoded/decoded and serialized by the
-   *                   given ASN.1 encoding and serializers.
-   * @param supplier   A supplier of new ASN.1 codecs that can be used to encode/decode objects
-   *                   of the given type.
+   * @param type       The type of the objects that must be encoded/decoded and serialized by the given ASN.1 encoding
+   *                   and serializers.
+   * @param supplier   A supplier of new ASN.1 codecs that can be used to encode/decode objects of the given type.
    * @param serializer A serializer that can serialize and deserialize the given ASN.1 object
    * @param <T>        The type of object being registered.
    * @param <U>        The type of ASN.1 codec being registered.
+   *
    * @return this for chaining.
    */
   @SuppressWarnings("unchecked")
   public <T, U extends AsnObjectCodec<T>> CodecContext register(Class<T> type,
-                                                                AsnObjectCodecSupplier<T> supplier,
-                                                                AsnObjectSerializer<U> serializer) {
+      AsnObjectCodecSupplier<T> supplier,
+      AsnObjectSerializer<U> serializer) {
     Objects.requireNonNull(type);
     Objects.requireNonNull(supplier);
     Objects.requireNonNull(serializer);
@@ -96,13 +97,15 @@ public class CodecContext {
   }
 
   /**
-   * Deserialize an object of the given type from the given stream and use the appropriate ASN.1
-   * codec to decode the deserialized ASN.1 object into an object of type {@code T}.
+   * Deserialize an object of the given type from the given stream and use the appropriate ASN.1 codec to decode the
+   * deserialized ASN.1 object into an object of type {@code T}.
    *
    * @param type        The type of the object to read from the stream.
    * @param inputStream The stream from which to read the object.
    * @param <T>         The type of the object.
+   *
    * @return the object read from the stream.
+   *
    * @throws IOException if there are errors reading from the stream.
    */
   public <T> T read(Class<T> type, InputStream inputStream) throws IOException {
@@ -117,11 +120,11 @@ public class CodecContext {
    * @param instance     The object to write.
    * @param outputStream The stream to write to.
    * @param <T>          The type of the object.
+   *
    * @throws IOException if there are errors writing to the stream.
    */
   public <T> void write(T instance, OutputStream outputStream) throws IOException {
-    AsnObjectCodec<T> asnObjectCodec = mappings.getAsnObjectForType((Class<T>) instance
-        .getClass());
+    AsnObjectCodec<T> asnObjectCodec = mappings.getAsnObjectForType((Class<T>) instance.getClass());
     asnObjectCodec.encode(instance);
     serializers.write(asnObjectCodec, outputStream);
   }
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnCharStringOerSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnCharStringOerSerializer.java
index 86d76e8..f680f80 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnCharStringOerSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnCharStringOerSerializer.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,54 +20,83 @@ package org.interledger.encoding.asn.serializers.oer;
  * =========================LICENSE_END==================================
  */
 
+import static java.lang.String.format;
+
 import org.interledger.encoding.asn.codecs.AsnCharStringBasedObjectCodec;
 import org.interledger.encoding.asn.codecs.AsnSizeConstraint;
 import org.interledger.encoding.asn.framework.AsnObjectSerializationContext;
 import org.interledger.encoding.asn.framework.AsnObjectSerializer;
 
-import java.io.ByteArrayOutputStream;
+import com.google.common.base.Utf8;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharStreams;
+
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.nio.charset.Charset;
 import java.util.Objects;
 
 /**
- * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER
- * object that is represented by a character string.
+ * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER object that is represented by a
+ * character string.
  */
-public class AsnCharStringOerSerializer implements
-    AsnObjectSerializer<AsnCharStringBasedObjectCodec> {
+public class AsnCharStringOerSerializer implements AsnObjectSerializer<AsnCharStringBasedObjectCodec> {
 
   @Override
-  public void read(final AsnObjectSerializationContext context,
-                   final AsnCharStringBasedObjectCodec instance,
-                   final InputStream inputStream) throws IOException {
+  public void read(
+      final AsnObjectSerializationContext context,
+      final AsnCharStringBasedObjectCodec instance,
+      final InputStream inputStream
+  ) throws IOException {
 
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
     Objects.requireNonNull(inputStream);
 
-    int length;
+    // WARNING: This length can be maliciously specified by the packet creator, so be careful not to use it for unsafe
+    // operations, such as creating a new array of initial size `length`. This usage is safe because it merely caps the
+    // InputStream to the specified packet-length, whereas the InputStream is authoritative for when it actually ends,
+    // and this limit may be well smaller than `length`.
+    int lengthToRead;
     final AsnSizeConstraint sizeConstraint = instance.getSizeConstraint();
     if (sizeConstraint.isFixedSize()) {
-      length = sizeConstraint.getMax();
+      lengthToRead = sizeConstraint.getMax();
+    } else {
+      // Read the lengthToRead of the encoded OctetString...
+      lengthToRead = OerLengthSerializer.readLength(inputStream);
+    }
+
+    final String result;
+    /* beware the 0-lengthToRead string */
+    if (lengthToRead == 0) {
+      result = "";
     } else {
-      // Read the length of the encoded OctetString...
-      length = OerLengthSerializer.readLength(inputStream);
+      // Use a limited input stream so we don't read too many bytes.
+      final InputStream limitedInputStream = ByteStreams.limit(inputStream, lengthToRead);
+      // WARNING: Don't close the InputStreamReader so that the underlying inputStream is not closed.
+      result = CharStreams.toString(new InputStreamReader(limitedInputStream, instance.getCharacterSet().name()));
+
+      // For UTF-8 characters, result.length() will report the viewable length (e.g., 3) but for certain encoded
+      // characters, the actual byte-length will be larger (e.g., the String 元元元 is 3 viewable bytes, but 9 encoded
+      // UTF-8 bytes). Thus, when we write the length-prefix, the code will write 9, so when we read, we need to
+      // validate that 9 bytes were read, and not 3 (in this example).
+      if (Utf8.encodedLength(result) != lengthToRead) {
+        throw new IOException(
+            format("Unable to properly decode %s bytes (could only read %s bytes)", lengthToRead, result.length())
+        );
+      }
     }
-    
-    /* beware the 0-length string */
-    final String result = (length == 0 ? "" :
-        this.toString(inputStream, length, instance.getCharacterSet()));
 
     instance.setCharString(result);
   }
 
   @Override
-  public void write(final AsnObjectSerializationContext context,
-                    final AsnCharStringBasedObjectCodec instance,
-                    final OutputStream outputStream) throws IOException {
+  public void write(
+      final AsnObjectSerializationContext context,
+      final AsnCharStringBasedObjectCodec instance,
+      final OutputStream outputStream
+  ) throws IOException {
 
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
@@ -85,31 +114,4 @@ public class AsnCharStringOerSerializer implements
     outputStream.write(data);
   }
 
-  /**
-   * Convert an {@link InputStream} into a {@link String}. Reference the SO below for an interesting
-   * performance comparison of various InputStream to String methodologies.
-   *
-   * @param inputStream An instance of {@link InputStream}.
-   * @return A {@link String}
-   * @throws IOException If the {@code inputStream} is unable to be read properly.
-   * @see "http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string"
-   */
-  private String toString(final InputStream inputStream, final int lengthToRead, Charset charset)
-      throws IOException {
-    Objects.requireNonNull(inputStream);
-    ByteArrayOutputStream result = new ByteArrayOutputStream();
-
-    // Read lengthToRead bytes from the inputStream into the buffer...
-    byte[] buffer = new byte[lengthToRead];
-    int read = inputStream.read(buffer);
-
-    if (read != lengthToRead) {
-      throw new IOException(
-          "error reading " + lengthToRead + " bytes from stream, only read " + read);
-    }
-
-    result.write(buffer, 0, lengthToRead);
-    return result.toString(charset.name());
-  }
-
 }
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOctetStringOerSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOctetStringOerSerializer.java
index 98a9688..bf9a2da 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOctetStringOerSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOctetStringOerSerializer.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -26,7 +26,8 @@ import org.interledger.encoding.asn.codecs.AsnOctetStringBasedObjectCodec;
 import org.interledger.encoding.asn.codecs.AsnSizeConstraint;
 import org.interledger.encoding.asn.framework.AsnObjectSerializationContext;
 import org.interledger.encoding.asn.framework.AsnObjectSerializer;
-import org.interledger.encoding.asn.framework.CodecException;
+
+import com.google.common.io.ByteStreams;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -34,50 +35,56 @@ import java.io.OutputStream;
 import java.util.Objects;
 
 /**
- * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER
- * object that is represented by an octet string.
+ * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER object that is represented by an
+ * octet string.
  */
-public class AsnOctetStringOerSerializer
-    implements AsnObjectSerializer<AsnOctetStringBasedObjectCodec> {
+public class AsnOctetStringOerSerializer implements AsnObjectSerializer<AsnOctetStringBasedObjectCodec> {
 
   @Override
-  public void read(final AsnObjectSerializationContext context,
-                   final AsnOctetStringBasedObjectCodec instance,
-                   final InputStream inputStream) throws IOException {
+  public void read(
+      final AsnObjectSerializationContext context,
+      final AsnOctetStringBasedObjectCodec instance,
+      final InputStream inputStream
+  ) throws IOException {
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
     Objects.requireNonNull(inputStream);
 
-    int length;
+    // WARNING: This length can be maliciously specified by the packet creator, so be careful not to use it for unsafe
+    // operations, such as creating a new array of initial size `length`. This usage is safe because it merely caps the
+    // InputStream to the specified packet-length, whereas the InputStream is authoritative for when it actually ends,
+    // and this limit may be well smaller than `length`.
+    int lengthToRead;
     final AsnSizeConstraint sizeConstraint = instance.getSizeConstraint();
     if (sizeConstraint.isFixedSize()) {
-      length = sizeConstraint.getMax();
+      lengthToRead = sizeConstraint.getMax();
     } else {
-      // Read the length of the encoded OctetString...
-      length = OerLengthSerializer.readLength(inputStream);
+      // Read the lengthToRead of the encoded OctetString...will never be > 127
+      lengthToRead = OerLengthSerializer.readLength(inputStream);
     }
 
-    final byte[] returnable = new byte[length];
-
-    if (length == 0) {
-      instance.setBytes(returnable);
+    final byte[] bytes;
+    if (lengthToRead == 0) {
+      bytes = new byte[0];
     } else {
-
-      int bytesRead = inputStream.read(returnable);
-      if (bytesRead < length) {
-        throw new CodecException(
-            format("Unexpected end of stream. Expected %s bytes but only read %s.",
-                length, bytesRead));
+      // Use a limited input stream so we don't read too many bytes.
+      final InputStream limitedInputStream = ByteStreams.limit(inputStream, lengthToRead);
+      bytes = ByteStreams.toByteArray(limitedInputStream);
+      if (bytes.length != lengthToRead) {
+        throw new IOException(
+            format("Unable to properly decode %s bytes (could only read %s bytes)", lengthToRead, bytes.length)
+        );
       }
-      instance.setBytes(returnable);
     }
-
+    instance.setBytes(bytes);
   }
 
   @Override
-  public void write(final AsnObjectSerializationContext context,
-                    final AsnOctetStringBasedObjectCodec instance,
-                    final OutputStream outputStream) throws IOException {
+  public void write(
+      final AsnObjectSerializationContext context,
+      final AsnOctetStringBasedObjectCodec instance,
+      final OutputStream outputStream
+  ) throws IOException {
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
     Objects.requireNonNull(outputStream);
@@ -91,9 +98,7 @@ public class AsnOctetStringOerSerializer
     }
 
     // Write the OctetString bytes to the buffer.
-    if (bytes.length > 0) {
-      outputStream.write(bytes);
-    }
+    outputStream.write(bytes);
   }
 
 }
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOpenTypeOerSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOpenTypeOerSerializer.java
index 87f47cc..b2ed64e 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOpenTypeOerSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnOpenTypeOerSerializer.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,51 +20,43 @@ package org.interledger.encoding.asn.serializers.oer;
  * =========================LICENSE_END==================================
  */
 
-import static java.lang.String.format;
-
 import org.interledger.encoding.asn.codecs.AsnOpenTypeCodec;
 import org.interledger.encoding.asn.framework.AsnObjectSerializationContext;
 import org.interledger.encoding.asn.framework.AsnObjectSerializer;
 
+import com.google.common.io.ByteStreams;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Objects;
 
 /**
- * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER
- * object that is represented by an octet string.
+ * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER object that is represented by an
+ * octet string.
  */
-public class AsnOpenTypeOerSerializer
-    implements AsnObjectSerializer<AsnOpenTypeCodec> {
+public class AsnOpenTypeOerSerializer implements AsnObjectSerializer<AsnOpenTypeCodec> {
 
   @Override
-  public void read(final AsnObjectSerializationContext context,
-                   final AsnOpenTypeCodec instance,
-                   final InputStream inputStream) throws IOException {
+  public void read(
+      final AsnObjectSerializationContext context, final AsnOpenTypeCodec instance, final InputStream inputStream
+  ) throws IOException {
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
     Objects.requireNonNull(inputStream);
 
-    //Get length
+    // WARNING: This length can be maliciously specified by the packet creator, so be careful not to use it for unsafe
+    // operations, such as creating a new array of initial size `length`. This usage is safe because it merely caps the
+    // InputStream to the specified packet-length, although the InputStream is authoritative for when it actually ends,
+    // and this limit may be well smaller than `length` (length will never be > 127)
     int length = OerLengthSerializer.readLength(inputStream);
-
-    //Read in correct number of bytes
-    byte[] buffer = new byte[length];
-    int bytesRead = inputStream.read(buffer);
-
-    if (bytesRead != length) {
-      throw new IOException(format("Unexpected end of stream. Expected %s bytes but got %s.",
-          length, bytesRead));
-    }
-    context.read(instance.getInnerCodec(), buffer);
-
+    context.read(instance.getInnerCodec(), ByteStreams.limit(inputStream, length));
   }
 
   @Override
-  public void write(final AsnObjectSerializationContext context,
-                    final AsnOpenTypeCodec instance,
-                    final OutputStream outputStream) throws IOException {
+  public void write(
+      final AsnObjectSerializationContext context, final AsnOpenTypeCodec instance, final OutputStream outputStream
+  ) throws IOException {
     Objects.requireNonNull(context);
     Objects.requireNonNull(instance);
     Objects.requireNonNull(outputStream);
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnSequenceOfSequenceOerSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnSequenceOfSequenceOerSerializer.java
index c768a07..fc39703 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnSequenceOfSequenceOerSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/AsnSequenceOfSequenceOerSerializer.java
@@ -32,16 +32,17 @@ import java.io.OutputStream;
 import java.math.BigInteger;
 
 /**
- * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER object that is represented by an
- * SEQUENCE.
+ * An extension of {@link AsnObjectSerializer} for reading and writing an ASN.1 OER object that is represented by a
+ * SEQUENCE OF SEQUENCE (which in ASN.1 OER nomenclature is an array of objects).
  */
-public class AsnSequenceOfSequenceOerSerializer
-    implements AsnObjectSerializer<AsnSequenceOfSequenceCodec> {
+public class AsnSequenceOfSequenceOerSerializer implements AsnObjectSerializer<AsnSequenceOfSequenceCodec> {
 
   @Override
-  public void read(AsnObjectSerializationContext context, AsnSequenceOfSequenceCodec instance,
-      InputStream inputStream)
-      throws IOException {
+  public void read(
+      final AsnObjectSerializationContext context,
+      final AsnSequenceOfSequenceCodec instance,
+      final InputStream inputStream
+  ) throws IOException {
 
     AsnUintCodec quantityCodec = new AsnUintCodec();
     context.read(quantityCodec, inputStream);
@@ -51,18 +52,21 @@ public class AsnSequenceOfSequenceOerSerializer
       throw new CodecException("SEQUENCE_OF quantities > Integer.MAX_VALUE ar not supported");
     }
 
-    int quantity = quantityBigInt.intValue();
-    instance.setSize(quantity);
-
-    for (int i = 0; i < quantity; i++) {
+    // This is the number of sequences that were indicated in the ASN.1 OER encoding. While this is specified by an
+    // outside caller and _could_ be malicious, the code will not read past the actual number of bytes in the
+    // InputStream, which due to ILPv4 packet limits should never be greater than 32kb.
+    int indicatedNumberOfSequences = quantityBigInt.intValue();
+    for (int i = 0; i < indicatedNumberOfSequences; i++) {
       context.read(instance.getCodecAt(i), inputStream);
     }
   }
 
   @Override
-  public void write(AsnObjectSerializationContext context,
-      AsnSequenceOfSequenceCodec instance,
-      OutputStream outputStream) throws IOException {
+  public void write(
+      final AsnObjectSerializationContext context,
+      final AsnSequenceOfSequenceCodec instance,
+      final OutputStream outputStream
+  ) throws IOException {
 
     AsnUintCodec quantityCodec = new AsnUintCodec();
     quantityCodec.encode(BigInteger.valueOf(instance.size()));
diff --git a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializer.java b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializer.java
index 6eddb1f..cf96b0f 100644
--- a/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializer.java
+++ b/codecs-parent/codecs-framework/src/main/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializer.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,28 +22,46 @@ package org.interledger.encoding.asn.serializers.oer;
 
 import org.interledger.encoding.asn.framework.CodecException;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Objects;
 
 /**
- * A serializer with some static utility functions for reading and writing length prefixes.
+ * <p>A serializer with some static utility functions for reading and writing length prefixes.</p>
+ *
+ * <p>The length prefix that is part of the encoding of certain ASN.1 types specifies the length of an encoding in
+ * terms of number of octets. A length prefix consists of one or more octets, as follows:</p>
+ *
+ * <p>1) If the length is less than 128, it is encoded in the lowest-order 7 bit positions of the first and only octet,
+ * and the highest-order bit of the octet is set to zero.</p>
+ *
+ * <p>2) If the length is greater than 127, it is encoded in one or more subsequent octets, in big-endian order. The
+ * number of those octets is indicated in the lowest-order 7 bit positions of the first octet, and the highest-order bit
+ * of that octet is set to 1.</p>
+ *
+ * <p>For the second case, the length values are encoded as an unsigned binary integer using four octets, which allows
+ * for a length of 4294967295. While OER lengths can technically support a length that can fit into 127 bytes, this
+ * implementation caps length values at 4-byte numbers.</p>
  */
-public class OerLengthSerializer  {
+public class OerLengthSerializer {
 
   /**
-   * Read a length prefix from the stream.
+   * Read a length prefix from the stream as a positive integer. Negative integer values are mapped to 0. Note that this
+   * value will never exceed {@link Integer#MAX_VALUE}.
+   *
+   * @param inputStream the stream to read from.
+   *
+   * @return The length encoded in the length prefix.
    *
-   * @param inputStream the stream to read from
-   * @return The length encoded in the length prefix
    * @throws IOException if there is an error reading from the stream.
    */
   public static int readLength(final InputStream inputStream) throws IOException {
 
     Objects.requireNonNull(inputStream);
 
-
     // The number of encoded octets that the encoded payload will be stored in.
     final int length;
 
@@ -51,53 +69,57 @@ public class OerLengthSerializer  {
     if (initialLengthPrefixOctet >= 0 && initialLengthPrefixOctet < 128) {
       length = initialLengthPrefixOctet;
     } else {
-      // Truncate the MSB and use the rest as a number...
-      final int lengthOfLength = initialLengthPrefixOctet & 0x7f;
+      // Truncate the MSB and use the rest as a number...length-of-length may not exceed 127 since this can only be 1
+      // signed byte. This represents the number of bytes to consume in order to determine the actual length.
+      final int lengthOfLength = initialLengthPrefixOctet & 0x7F;
 
       // Convert the bytes into an integer...
       byte[] ba = new byte[lengthOfLength];
       int read = inputStream.read(ba, 0, lengthOfLength);
 
       if (read != lengthOfLength) {
-        throw new IOException(
-            "error reading " + lengthOfLength + " bytes from stream, only read " + read);
+        throw new IOException("Unable to read " + lengthOfLength + " bytes from stream, only read " + read);
       }
+      // Ensure that this value is always unsigned...
       length = toInt(ba);
     }
 
-    return length;
+    // Only allow positive lengths allowed. If negative, return 0.
+    return length > 0 ? length : 0;
   }
 
   /**
-   * Helper method to convert a byte array of varying length (assuming not larger than 4 bytes) into
-   * an int. This is necessary because most traditional library assume a 4-byte array when
-   * converting to an Integer.
+   * Helper method to convert a byte array of varying length (assuming not larger than 4 bytes) into a positive integer.
+   * This is necessary because most traditional libraries assume a 4-byte array when converting to an Integer, but in
+   * ASN.1 OER we can encounter integers encoded into 1,2,3, or 4 bytes.
    *
    * @param bytes An array of up to 4 bytes representing an integer
+   *
    * @return the int representation of the given bytes
    */
-  private static int toInt(final byte[] bytes) {
-
+  @VisibleForTesting
+  static int toInt(final byte[] bytes) {
+    Objects.requireNonNull(bytes);
     switch (bytes.length) {
       case 0:
         return 0;
       case 1: {
-        return (bytes[0]) & 0x000000ff;
+        return (bytes[0]) & 0x000000FF;
       }
       case 2: {
-        return (bytes[0] << 8) & 0x0000ff00
-            | (bytes[1]) & 0x000000ff;
+        return (bytes[0] << 8) & 0x0000FF00
+            | (bytes[1]) & 0x000000FF;
       }
       case 3: {
-        return (bytes[0] << 16) & 0x00ff0000
-            | (bytes[1] << 8) & 0x0000ff00
-            | (bytes[2]) & 0x000000ff;
+        return (bytes[0] << 16) & 0x00FF0000
+            | (bytes[1] << 8) & 0x0000FF00
+            | (bytes[2]) & 0x000000FF;
       }
       case 4: {
-        return (bytes[0] << 24) & 0xff000000
+        return (bytes[0] << 24) & 0xFF000000
             | (bytes[1] << 16) & 0x00ff0000
-            | (bytes[2] << 8) & 0x0000ff00
-            | (bytes[3]) & 0x000000ff;
+            | (bytes[2] << 8) & 0x0000FF00
+            | (bytes[3]) & 0x000000FF;
       }
       default: {
         throw new CodecException("This method only supports arrays up to length 4!");
@@ -108,52 +130,55 @@ public class OerLengthSerializer  {
   /**
    * Write an OER length prefix to the stream.
    *
-   * @param length The length to encode and write to the stream.
+   * @param length       The length to encode and write to the stream.
    * @param outputStream the stream to write to
+   *
    * @throws IOException if there is an error writing to the stream
    */
-  public static void writeLength(final int length, final OutputStream outputStream)
-      throws IOException {
-
+  public static void writeLength(final int length, final OutputStream outputStream) throws IOException {
     Objects.requireNonNull(outputStream);
 
-    if (length >= 0 && length < 128) {
-
-      // Write a single byte that contains the length (it will start with a 0, and not exceed
-      // 127 in Base10.
-      outputStream.write(length);
+    if (length <= 0) {
+      outputStream.write(0);
     } else {
-      // Write the number of octets required to encode the length, and move the cursor to the
-      // correct position.
-      if (length <= 127) {
-        // Write the first byte
-        outputStream.write(length);
-        // return 1;
-      } else if (length <= 255) {
-        // Write the first byte
-        outputStream.write(128 + 1);
-        outputStream.write(length);
-        // return 2;
-      } else if (length <= 65535) {
-        outputStream.write(128 + 2);
-        // Write the first byte, then the second byte.
-        outputStream.write((length >> 8));
-        outputStream.write(length);
-        // return 3;
-      } else if (length <= 16777215) {
-        outputStream.write(128 + 3);
-        // Write three bytes
-        outputStream.write((length >> 16));
-        outputStream.write((length >> 8));
+      if (length >= 0 && length < 128) {
+
+        // Write a single byte that contains the length (it will start with a 0, and not exceed
+        // 127 in Base10.
         outputStream.write(length);
-        // return 4;
       } else {
-        outputStream.write(128 + 4);
-        // Write four bytes,
-        outputStream.write((length >> 24));
-        outputStream.write((length >> 16));
-        outputStream.write((length >> 8));
-        outputStream.write(length);
+        // Write the number of octets required to encode the length, and move the cursor to the
+        // correct position.
+        if (length <= 127) {
+          // Write the first byte
+          outputStream.write(length);
+          // return 1;
+        } else if (length <= 255) {
+          // Write the first byte
+          outputStream.write(128 + 1);
+          outputStream.write(length);
+          // return 2;
+        } else if (length <= 65535) {
+          outputStream.write(128 + 2);
+          // Write the first byte, then the second byte.
+          outputStream.write((length >> 8));
+          outputStream.write(length);
+          // return 3;
+        } else if (length <= 16777215) {
+          outputStream.write(128 + 3);
+          // Write three bytes
+          outputStream.write((length >> 16));
+          outputStream.write((length >> 8));
+          outputStream.write(length);
+          // return 4;
+        } else {
+          outputStream.write(128 + 4);
+          // Write four bytes,
+          outputStream.write((length >> 24));
+          outputStream.write((length >> 16));
+          outputStream.write((length >> 8));
+          outputStream.write(length);
+        }
       }
     }
   }
diff --git a/codecs-parent/codecs-ildcp/src/main/java/org/interledger/codecs/ildcp/IldcpUtils.java b/codecs-parent/codecs-ildcp/src/main/java/org/interledger/codecs/ildcp/IldcpUtils.java
index c67cdc6..98f96bf 100644
--- a/codecs-parent/codecs-ildcp/src/main/java/org/interledger/codecs/ildcp/IldcpUtils.java
+++ b/codecs-parent/codecs-ildcp/src/main/java/org/interledger/codecs/ildcp/IldcpUtils.java
@@ -9,9 +9,9 @@ package org.interledger.codecs.ildcp;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,8 +20,6 @@ package org.interledger.codecs.ildcp;
  * =========================LICENSE_END==================================
  */
 
-import org.interledger.codecs.ildcp.IldcpCodecContextFactory;
-import org.interledger.codecs.ildcp.IldcpCodecException;
 import org.interledger.core.InterledgerFulfillPacket;
 import org.interledger.core.InterledgerPreparePacket;
 import org.interledger.encoding.asn.framework.CodecException;
diff --git a/codecs-parent/codecs-stream/src/main/java/org/interledger/codecs/stream/frame/AsnStreamFramesCodec.java b/codecs-parent/codecs-stream/src/main/java/org/interledger/codecs/stream/frame/AsnStreamFramesCodec.java
index 0ba33ae..a548984 100644
--- a/codecs-parent/codecs-stream/src/main/java/org/interledger/codecs/stream/frame/AsnStreamFramesCodec.java
+++ b/codecs-parent/codecs-stream/src/main/java/org/interledger/codecs/stream/frame/AsnStreamFramesCodec.java
@@ -27,10 +27,9 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * Codec for a List of Frames (i.e., a SEQUENCE-OF Frame).
+ * Codec for an Array of Frames (i.e., a SEQUENCE OF SEQUENCE of type StreamFrame).
  */
-public class AsnStreamFramesCodec
-    extends AsnSequenceOfSequenceCodec<List<StreamFrame>, StreamFrame> {
+public class AsnStreamFramesCodec extends AsnSequenceOfSequenceCodec<List<StreamFrame>, StreamFrame> {
 
   public AsnStreamFramesCodec() {
     super(ArrayList::new, AsnStreamFrameCodec::new);
diff --git a/examples-parent/send-money/pom.xml b/examples-parent/send-money/pom.xml
index e2541e5..4a8097d 100644
--- a/examples-parent/send-money/pom.xml
+++ b/examples-parent/send-money/pom.xml
@@ -61,7 +61,11 @@
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-jdk14</artifactId>
-      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.zalando</groupId>
+      <artifactId>problem-violations</artifactId>
+      <version>0.25.2</version>
     </dependency>
   </dependencies>
 
diff --git a/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java b/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
index 5f3e267..9ec08d5 100644
--- a/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
+++ b/examples-parent/send-money/src/main/java/org/interledger/examples/SendMoneyExample.java
@@ -8,23 +8,29 @@ import org.interledger.core.SharedSecret;
 import org.interledger.link.Link;
 import org.interledger.link.http.IlpOverHttpLink;
 import org.interledger.link.http.auth.SimpleBearerTokenSupplier;
+import org.interledger.quilt.jackson.InterledgerModule;
+import org.interledger.quilt.jackson.conditions.Encoding;
 import org.interledger.spsp.PaymentPointer;
 import org.interledger.spsp.StreamConnectionDetails;
 import org.interledger.spsp.client.SimpleSpspClient;
 import org.interledger.spsp.client.SpspClient;
-import org.interledger.spsp.client.rust.InterledgerRustNodeClient;
 import org.interledger.stream.Denominations;
 import org.interledger.stream.SendMoneyRequest;
 import org.interledger.stream.SendMoneyResult;
 import org.interledger.stream.sender.FixedSenderAmountPaymentTracker;
 import org.interledger.stream.sender.SimpleStreamSender;
 
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.google.common.primitives.UnsignedLong;
 import okhttp3.ConnectionPool;
 import okhttp3.ConnectionSpec;
 import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
+import org.zalando.problem.ProblemModule;
+import org.zalando.problem.violations.ConstraintViolationProblemModule;
 
 import java.time.Duration;
 import java.util.Arrays;
@@ -37,27 +43,29 @@ import java.util.concurrent.TimeUnit;
 public class SendMoneyExample {
 
   // NOTE - replace this with the username for your sender account
-  private static final String SENDER_ACCOUNT_USERNAME = "user_9wgfsfte";
+  private static final String SENDER_ACCOUNT_USERNAME = "demo_user";
   // NOTE - replace this with the passkey for your sender account
-  private static final String SENDER_PASS_KEY = "w6uwvg42ogktl";
+  private static final String SENDER_PASS_KEY = "MjA0NGU2MDQtMDVlOC00NzFlLTgyMzYtYTEzNTkzMDQ4ODBk";
   // NOTE - replace this with the payment pointer for your receiver account
-  private static final String RECEIVER_PAYMENT_POINTER = "$rs3.xpring.dev/accounts/user_e8zprn59/spsp";
+  private static final String RECEIVER_PAYMENT_POINTER = "$xpring.money/demo_receiver";
 
-  private static final String TESTNET_URI = "https://rs3.xpring.dev";
+  private static final String TESTNET_URI = "https://jc1.xpring.dev/accounts/" + SENDER_ACCOUNT_USERNAME + "/ilp";
 
+  /**
+   * This value will go away once #445 is fixed.
+   *
+   * @see "https://github.com/hyperledger/quilt/issues/445"
+   */
+  @Deprecated
   private static final InterledgerAddress SENDER_ADDRESS =
-      InterledgerAddress.of("test.xpring-dev.rs3").with(SENDER_ACCOUNT_USERNAME);
+      InterledgerAddress.of("test.xpring-dev.jc1.spsp-test").with(SENDER_ACCOUNT_USERNAME);
 
   public static void main(String[] args) throws ExecutionException, InterruptedException {
     SpspClient spspClient = new SimpleSpspClient();
 
-    // Create rust client
-    InterledgerRustNodeClient rustClient =
-        new InterledgerRustNodeClient(newHttpClient(), SENDER_ACCOUNT_USERNAME + ":" + SENDER_PASS_KEY, TESTNET_URI);
-
     // Fetch shared secret and destination address using SPSP client
-    StreamConnectionDetails connectionDetails =
-        spspClient.getStreamConnectionDetails(PaymentPointer.of(RECEIVER_PAYMENT_POINTER));
+    StreamConnectionDetails connectionDetails = spspClient
+        .getStreamConnectionDetails(PaymentPointer.of(RECEIVER_PAYMENT_POINTER));
 
     // Use ILP over HTTP for our underlying link
     Link link = newIlpOverHttpLink();
@@ -65,33 +73,33 @@ public class SendMoneyExample {
     // Create SimpleStreamSender for sending STREAM payments
     SimpleStreamSender simpleStreamSender = new SimpleStreamSender(link);
 
-    System.out.println("Starting balance for sender: " + rustClient.getBalance(SENDER_ACCOUNT_USERNAME));
+    // This is 1 drop when scale=9
+    final long ONE_DROP_IN_SCALE_9 = 1000;
 
     // Send payment using STREAM
     SendMoneyResult result = simpleStreamSender.sendMoney(
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
-            .amount(UnsignedLong.valueOf(100000))
-            .denomination(Denominations.XRP)
+            .amount(UnsignedLong.valueOf(ONE_DROP_IN_SCALE_9))
+            .denomination(Denominations.XRP_MILLI_DROPS)
             .destinationAddress(connectionDetails.destinationAddress())
-            .timeout(Duration.ofMillis(30000))
-            .paymentTracker(new FixedSenderAmountPaymentTracker(UnsignedLong.valueOf(100000)))
+            .timeout(Duration.ofMillis(15000))
+            .paymentTracker(new FixedSenderAmountPaymentTracker(UnsignedLong.valueOf(ONE_DROP_IN_SCALE_9)))
             .sharedSecret(SharedSecret.of(connectionDetails.sharedSecret().value()))
             .build()
     ).get();
 
     System.out.println("Send money result: " + result);
-    System.out.println("Ending balance for sender: " + rustClient.getBalance(SENDER_ACCOUNT_USERNAME));
   }
 
   private static Link newIlpOverHttpLink() {
     return new IlpOverHttpLink(
         () -> SENDER_ADDRESS,
-        HttpUrl.parse(TESTNET_URI + "/ilp"),
+        HttpUrl.parse(TESTNET_URI),
         newHttpClient(),
-        new ObjectMapper(),
+        createObjectMapperForProblemsJson(),
         InterledgerCodecContextFactory.oer(),
-        new SimpleBearerTokenSupplier(SENDER_ACCOUNT_USERNAME + ":" + SENDER_PASS_KEY)
+        new SimpleBearerTokenSupplier(SENDER_PASS_KEY)
     );
   }
 
@@ -107,4 +115,22 @@ public class SendMoneyExample {
     return builder.connectionPool(connectionPool).build();
   }
 
+  /**
+   * Construct an {@link ObjectMapper} that can be used to serialize and deserialize ProblemsJSON where JSON numbers
+   * emit as non-String values. Because Problems+Json requires HTTP status codes to be serialized as numbers (and not
+   * Strings) per RFC-7807, this ObjectMapper should not be used for payloads that involve Problems.
+   *
+   * @return An {@link ObjectMapper}.
+   *
+   * @see "https://tools.ietf.org/html/rfc7807"
+   */
+  private static ObjectMapper createObjectMapperForProblemsJson() {
+    return new ObjectMapper()
+        .registerModule(new Jdk8Module())
+        .registerModule(new InterledgerModule(Encoding.BASE64))
+        .registerModule(new ProblemModule())
+        .registerModule(new ConstraintViolationProblemModule())
+        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+        .configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, false);
+  }
 }
diff --git a/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java b/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
index 2996f8c..698af65 100644
--- a/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
+++ b/stream-parent/stream-client/src/main/java/org/interledger/stream/sender/SimpleStreamSender.java
@@ -827,16 +827,18 @@ public class SimpleStreamSender implements StreamSender {
       paymentTracker.rollback(prepareAmounts, true);
       congestionController.reject(amountToSend, rejectPacket);
 
-      logger.debug(
-          "Prepare with amount {} was rejected with code: {} ({} left to send). originalPreparePacket={} "
-              + "originalStreamPacket={} rejectPacket={}",
-          amountToSend,
-          rejectPacket.getCode().getCode(),
-          paymentTracker.getOriginalAmountLeft(),
-          originalPreparePacket,
-          originalStreamPacket,
-          rejectPacket
-      );
+      if (logger.isDebugEnabled()) {
+        logger.debug(
+            "Prepare with amount {} was rejected with code: {} ({} left to send). originalPreparePacket={} "
+                + "originalStreamPacket={} rejectPacket={}",
+            amountToSend,
+            rejectPacket.getCode().getCode(),
+            paymentTracker.getOriginalAmountLeft(),
+            originalPreparePacket,
+            originalStreamPacket,
+            rejectPacket
+        );
+      }
 
       this.checkForAndTriggerUnrecoverableError(rejectPacket);
 
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/Denominations.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/Denominations.java
index 417926d..523b53d 100644
--- a/stream-parent/stream-core/src/main/java/org/interledger/stream/Denominations.java
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/Denominations.java
@@ -1,24 +1,43 @@
 package org.interledger.stream;
 
+/**
+ * Utility instances of various commonly used denominations.
+ */
 public final class Denominations {
-  private Denominations() {
-  }
 
-  public static final Denomination XRP = Denomination.builder()
+  public static final Denomination XRP_DROPS = Denomination.builder()
       .assetCode("XRP")
       .assetScale((short) 6)
       .build();
 
+  /**
+   * @deprecated This value will be removed in a future version because it should actually have a scale of 0. Prefer
+   *     {@link #XRP_DROPS} instead.
+   */
+  @Deprecated
+  public static final Denomination XRP = XRP_DROPS;
+
+  public static final Denomination XRP_MILLI_DROPS = Denomination.builder()
+      .assetCode("XRP")
+      .assetScale((short) 9)
+      .build();
+
   public static final Denomination USD = Denomination.builder()
       .assetCode("USD")
       .assetScale((short) 0)
       .build();
 
-  public static final Denomination US_CENTS = Denomination.builder()
+  public static final Denomination USD_CENTS = Denomination.builder()
       .assetCode("USD")
       .assetScale((short) 2)
       .build();
 
+  /**
+   * @deprecated This value will be removed in a future version because it is misnamed. Prefer {@link #USD_CENTS}
+   *     instead.
+   */
+  public static final Denomination US_CENTS = USD_CENTS;
+
   public static final Denomination EUR = Denomination.builder()
       .assetCode("EUR")
       .assetScale((short) 0)
@@ -28,4 +47,7 @@ public final class Denominations {
       .assetCode("EUR")
       .assetScale((short) 2)
       .build();
+
+  private Denominations() {
+  }
 }


Test patch:
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodecTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodecTest.java
index 364e4d5..26dbb01 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodecTest.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnOctetStringBasedObjectCodecTest.java
@@ -66,7 +66,7 @@ public class AsnOctetStringBasedObjectCodecTest {
     try {
       codec.setBytes(bytes);
     } catch (CodecException e) {
-      assertThat(e.getMessage()).isEqualTo("Invalid octet string length. Expected 1, got 2");
+      assertThat(e.getMessage()).isEqualTo("Invalid octet string length. Expected < 1, got 2");
       throw e;
     }
   }
@@ -91,7 +91,7 @@ public class AsnOctetStringBasedObjectCodecTest {
     try {
       codec.setBytes(bytes);
     } catch (CodecException e) {
-      assertThat(e.getMessage()).isEqualTo("Invalid octet string length. Expected 1, got 0");
+      assertThat(e.getMessage()).isEqualTo("Invalid octet string length. Expected > 1, got 0");
       throw e;
     }
   }
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodecTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodecTest.java
new file mode 100644
index 0000000..ff76dc5
--- /dev/null
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/codecs/AsnSequenceOfSequenceCodecTest.java
@@ -0,0 +1,142 @@
+package org.interledger.encoding.asn.codecs;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+/**
+ * Unit test for {@link AsnSequenceOfSequenceCodec}.
+ */
+public class AsnSequenceOfSequenceCodecTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  private ArrayList<Object> newList;
+  private AsnSequenceOfSequenceCodec codec;
+
+  @Before
+  public void setup() {
+    this.newList = new ArrayList<>();
+    // Used to uniquely identify sub-codecs.
+    final AtomicInteger subCodecIdentifier = new AtomicInteger(0);
+
+    final Supplier<ArrayList> listSupplier = () -> new ArrayList<>(5);
+    final Supplier<AsnSequenceCodec<Integer>> subCodecSupplier
+        = () -> new TestSubCodec(subCodecIdentifier.getAndIncrement());
+
+    codec = new AsnSequenceOfSequenceCodec(listSupplier, subCodecSupplier);
+  }
+
+  /**
+   * Validates that the codecs List supplier passed in during construction is always used to create a new list.
+   */
+  @Test
+  public void testConstructedCodecsSizeAlwaysNonZero() {
+    // Always use the same list, instead of the supplier creating a new list on every call.
+    final Supplier<ArrayList> listSupplier = () -> new ArrayList<>(0);
+
+    final Supplier<AsnSequenceCodec<Integer>> subCodecSupplier = () -> new TestSubCodec(123);
+    codec = new AsnSequenceOfSequenceCodec(listSupplier, subCodecSupplier);
+
+    for (int i = 0; i < 20; i++) {
+      // If the supplier isn't translated correctly, then it will be re-used, and the size of codecs will grow, which is
+      // an error in the implementation. This test validates this doesn't happen.
+      codec.getCodecAt(i);
+
+      List list = codec.decode();
+      assertThat(list.size()).isEqualTo(i + 1);
+    }
+  }
+
+  @Test
+  public void getCodecAtNegativeIndex() {
+    expectedException.expect(IndexOutOfBoundsException.class);
+    codec.getCodecAt(-1);
+  }
+
+  /**
+   * Asserts that a small codec list will grow dynamically, and that an ordered access succeeds properly.
+   */
+  @Test
+  public void getCodecWithListThatIsEmpty() {
+    // Assemble a list with 5 values.
+    final ArrayList initialValues = new ArrayList();
+    for (int i = 0; i < 5; i++) {
+      initialValues.add(i, i);
+    }
+    // This merely sets the internal `codecs` object to have a length of 5.
+    codec.encode(initialValues); // Assign empty list.
+
+    // Actual test
+    final int size = 20;
+    for (int i = 0; i < size; i++) {
+      final TestSubCodec subCodec = (TestSubCodec) codec.getCodecAt(i);
+      assertThat(subCodec).isNotNull();
+      assertThat(subCodec.getIdentifier()).isEqualTo(i);
+    }
+    assertThat(codec.size()).isEqualTo(size);
+
+    // Do this cycle again to assert that a re-loop through the codec keeps all of its indices correct.
+    for (int i = 0; i < size; i++) {
+      final TestSubCodec subCodec = (TestSubCodec) codec.getCodecAt(i);
+      assertThat(subCodec).isNotNull();
+      assertThat(subCodec.getIdentifier()).isEqualTo(i);
+    }
+    assertThat(codec.size()).isEqualTo(size);
+  }
+
+  /**
+   * We want to ensure that getCodec returns the proper value on successive calls with the same index, even when the
+   * index being asked for is potentially far away from the nearest valid index. This test validates that the algorithm
+   * behind getCodec doesn't assume sub-codecs are added in-order, but may be requested & added in any order.
+   */
+  @Test
+  public void getCodecAtWithRandomAccess() {
+    expectedException.expect(IndexOutOfBoundsException.class);
+    codec.getCodecAt(100);
+  }
+
+  @Test
+  public void testEncodeWithNullCodecsList() {
+    expectedException.expect(NullPointerException.class);
+    codec.encode(null);
+  }
+
+  /**
+   * A class used for testing.
+   */
+  private static class TestSubCodec extends AsnSequenceCodec<Integer> {
+
+    // Used to uniquely identify the instance of sub-codec that is constructed so that this test harness can validate
+    // index lookups properly.
+    private int identifier;
+
+    TestSubCodec(int identifier) {
+      this.identifier = identifier;
+    }
+
+    @Override
+    public Integer decode() {
+      // NOTE: Unused, defined only to satisfy Java.
+      return identifier;
+    }
+
+    @Override
+    public void encode(final Integer value) {
+      // NOTE: Unused, defined only to satisfy Java.
+      this.identifier = value;
+    }
+
+    int getIdentifier() {
+      return identifier;
+    }
+  }
+}
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/IA5StringOerSerializerTestBadLength.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/IA5StringOerSerializerTestBadLength.java
index 09eca27..2279951 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/IA5StringOerSerializerTestBadLength.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/IA5StringOerSerializerTestBadLength.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,7 +25,9 @@ import org.interledger.encoding.asn.codecs.AsnSizeConstraint;
 import org.interledger.encoding.asn.framework.CodecContext;
 import org.interledger.encoding.asn.framework.CodecContextFactory;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -33,13 +35,18 @@ import java.io.IOException;
 import java.util.Arrays;
 
 /**
- * Test to ensure that the {@link AsnCharStringOerSerializer} properly fails when it cannot read
- * the number of bytes indicated.
+ * Test to ensure that the {@link AsnCharStringOerSerializer} properly fails when it cannot read the number of bytes
+ * indicated.
  */
 public class IA5StringOerSerializerTestBadLength {
 
-  @Test(expected = IOException.class)
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
   public void test_BadLengthIndicator() throws IOException {
+    expectedException.expect(IOException.class);
+    expectedException.expectMessage("Unable to properly decode 34 bytes (could only read 9 bytes)");
 
     CodecContext context = CodecContextFactory.oer()
         .register(String.class, () -> new AsnIA5StringCodec(AsnSizeConstraint.UNCONSTRAINED));
@@ -47,14 +54,12 @@ public class IA5StringOerSerializerTestBadLength {
     final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 
     context.write("A test string of reasonable length", outputStream);
-
     final byte[] correctData = outputStream.toByteArray();
 
     /*
      * we now remove a portion of the data, i.e. the length indicator should claim that there are 34
      * bytes, but we have truncated to 10 bytes
      */
-
     final byte[] truncated = Arrays.copyOfRange(correctData, 0, 10);
 
     /* create an input stream over the truncated data */
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTest.java
similarity index 59%
rename from codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTest.java
rename to codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTest.java
index 27aee33..11587b2 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTest.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTest.java
@@ -22,27 +22,34 @@ package org.interledger.encoding.asn.serializers.oer;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import org.interledger.encoding.asn.framework.CodecException;
+
 import com.google.common.io.BaseEncoding;
-import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 
 /**
- * Parameterized unit tests for encoding an instance of {@link AsnUint8OerSerializer}.
+ * Parameterized unit tests for {@link OerLengthSerializer}.
  */
 @RunWith(Parameterized.class)
-public class OerLengthPrefixCodecTest {
+public class OerLengthSerializerTest {
 
   private final int expectedPayloadLength;
   private final byte[] asn1OerBytes;
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   /**
    * Construct an instance of this parameterized test with the supplied inputs.
    *
@@ -50,7 +57,7 @@ public class OerLengthPrefixCodecTest {
    *                              length-prefix.
    * @param asn1OerBytes          The expected value, in binary, of the supplied {@code expectedPayloadLength}.
    */
-  public OerLengthPrefixCodecTest(final int expectedPayloadLength, final byte[] asn1OerBytes) {
+  public OerLengthSerializerTest(final int expectedPayloadLength, final byte[] asn1OerBytes) {
     this.expectedPayloadLength = expectedPayloadLength;
     this.asn1OerBytes = asn1OerBytes;
   }
@@ -80,25 +87,15 @@ public class OerLengthPrefixCodecTest {
         {256, BaseEncoding.base16().decode("820100")},
         // 8 (Last number that can be encoded in 2 value octets).
         {65535, BaseEncoding.base16().decode("82FFFF")},
-
         // 9 (First number that can be encoded in 3 value octets).
         {65536, BaseEncoding.base16().decode("83010000")},
-
         // 10 (Last number that can be encoded in 3 value octets).
         {16777215, BaseEncoding.base16().decode("83FFFFFF")},
-
         // 11 (First number that can be encoded in 4 value octets).
         {16777216, BaseEncoding.base16().decode("8401000000")},
-
-        // 11 (First number that can be encoded in 4 value octets).
-        {Integer.MAX_VALUE, BaseEncoding.base16().decode("847FFFFFFF")},});
-  }
-
-  /**
-   * Test setup.
-   */
-  @Before
-  public void setUp() throws Exception {
+        // 12 (Largest signed Integer value).
+        {Integer.MAX_VALUE, BaseEncoding.base16().decode("847FFFFFFF")},
+    });
   }
 
   @Test
@@ -136,4 +133,62 @@ public class OerLengthPrefixCodecTest {
     assertThat(outputStream.toByteArray()).isEqualTo(outputStream2.toByteArray());
   }
 
+  @Test
+  public void writeNegativeLength() throws IOException {
+    // Allow the AsnObjectCodec to write to 'outputStream'
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    OerLengthSerializer.writeLength(Integer.MIN_VALUE, outputStream);
+    assertThat(BaseEncoding.base16().decode("00")).isEqualTo(outputStream.toByteArray());
+
+    // Allow the AsnObjectCodec to write to 'outputStream'
+    outputStream = new ByteArrayOutputStream();
+    OerLengthSerializer.writeLength(-1, outputStream);
+    assertThat(BaseEncoding.base16().decode("00")).isEqualTo(outputStream.toByteArray());
+  }
+
+  @Test
+  public void readLengthWithCorrectPrefixButNegativeNumber() throws IOException {
+    // Allow the AsnObjectCodec to write to 'outputStream'
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(3); // 00000010 --> The following bytes after this will be ignored.
+    for (int i = 0; i < 3; i++) {
+      outputStream.write(-1); // FF
+    }
+
+    ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+    assertThat(OerLengthSerializer.readLength(inputStream)).isEqualTo(3);
+  }
+
+  @Test
+  public void readMultiByteLengthWithNegativeValue() throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(-127); // 10000001 (should be translated to 1) --> Length-of-Length
+    outputStream.write(0x80); // 10000000 (should be translated to -128) --> Length
+
+    ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+    assertThat(OerLengthSerializer.readLength(inputStream)).isEqualTo(128);
+  }
+
+  @Test
+  public void readMultiByteLengthWith1Byte() throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(-127); // 10000001 (should be translated to 1) --> Length-of-Length
+    outputStream.write(2); // 00000001 (should be translated to 2) --> Length
+
+    ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+    assertThat(OerLengthSerializer.readLength(inputStream)).isEqualTo(2);
+  }
+
+  @Test
+  public void readMultiByteLengthWithMaxBytes() throws IOException {
+    expectedException.expect(CodecException.class);
+    expectedException.expectMessage("This method only supports arrays up to length 4!");
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    outputStream.write(-1); // (0xFF) or 11111111 (should be translated to 127) --> Length-of-Length
+    for (int i = 0; i < 127; i++) {
+      outputStream.write(-1); // FF
+    }
+    ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+    OerLengthSerializer.readLength(inputStream);
+  }
 }
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTestBadLength.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTestBadLength.java
similarity index 83%
rename from codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTestBadLength.java
rename to codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTestBadLength.java
index 88b7f73..19561a1 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthPrefixCodecTestBadLength.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerTestBadLength.java
@@ -9,9 +9,9 @@ package org.interledger.encoding.asn.serializers.oer;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,19 +20,25 @@ package org.interledger.encoding.asn.serializers.oer;
  * =========================LICENSE_END==================================
  */
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 
 /**
- * Tests that the {@link OerLengthSerializer} correctly fails if the required length indicator
- * cannot be fully read.
+ * Tests that the {@link OerLengthSerializer} correctly fails if the required length indicator cannot be fully read.
  */
-public class OerLengthPrefixCodecTestBadLength {
+public class OerLengthSerializerTestBadLength {
 
-  @Test(expected = IOException.class)
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
   public void test_BadLengthIndicator() throws IOException {
+    expectedException.expect(IOException.class);
+
     /*
      * we create an incorrect length indicator that says that the next two bytes encode the length
      * of the actual data. however, we'll only supply one byte of information. the codec should
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerToIntTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerToIntTest.java
new file mode 100644
index 0000000..0791e94
--- /dev/null
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/OerLengthSerializerToIntTest.java
@@ -0,0 +1,99 @@
+package org.interledger.encoding.asn.serializers.oer;
+
+/*-
+ * ========================LICENSE_START=================================
+ * Interledger Codec Framework
+ * %%
+ * Copyright (C) 2017 - 2018 Hyperledger and its contributors
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.io.BaseEncoding;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Parameterized unit tests for {@link OerLengthSerializer}.
+ */
+@RunWith(Parameterized.class)
+public class OerLengthSerializerToIntTest {
+
+  private final int expectedPayloadLength;
+  private final byte[] numberBytes;
+
+  /**
+   * Construct an instance of this parameterized test with the supplied inputs.
+   *
+   * @param expectedPayloadLength An integer representing an integer payload.
+   * @param numberBytes           The expected value, in binary, of the supplied {@code expectedPayloadLength}.
+   */
+  public OerLengthSerializerToIntTest(final int expectedPayloadLength, final byte[] numberBytes) {
+    this.expectedPayloadLength = expectedPayloadLength;
+    this.numberBytes = numberBytes;
+  }
+
+  /**
+   * The data for this test...
+   */
+  @Parameters
+  public static Collection<Object[]> data() {
+    return Arrays.asList(new Object[][] {
+        // [input_value][num_octets_written][byte_values]
+        // 0
+        {0, BaseEncoding.base16().decode("00")},
+        // 1
+        {1, BaseEncoding.base16().decode("01")},
+        // 2
+        {2, BaseEncoding.base16().decode("02")},
+        // 3 (the last number that can be encoded in 1 overall octet)
+        {127, BaseEncoding.base16().decode("7F")},
+        // 4 (the first number that can be encoded in 1 value octet)
+        {128, BaseEncoding.base16().decode("80")},
+        // 5
+        {129, BaseEncoding.base16().decode("81")},
+        // 6 (the last number that can be encoded in 1 value octet)
+        {255, BaseEncoding.base16().decode("FF")},
+        // 7 (the first number that can be encoded in 2 value octets)
+        {256, BaseEncoding.base16().decode("0100")},
+        // 8 (Last number that can be encoded in 2 value octets).
+        {65535, BaseEncoding.base16().decode("FFFF")},
+        // 9 (First number that can be encoded in 3 value octets).
+        {65536, BaseEncoding.base16().decode("010000")},
+        // 10 (Last number that can be encoded in 3 value octets).
+        {16777215, BaseEncoding.base16().decode("FFFFFF")},
+        // 11 (First number that can be encoded in 4 value octets).
+        {16777216, BaseEncoding.base16().decode("01000000")},
+        // 12 (Largest signed Integer value).
+        {Integer.MAX_VALUE, BaseEncoding.base16().decode("7FFFFFFF")},
+        // 13 (Smallest signed Integer value).
+        {Integer.MIN_VALUE, BaseEncoding.base16().decode("80000000")},
+        // 14 (Largest negative signed Integer value).
+        {-1, BaseEncoding.base16().decode("FFFFFFFF")},
+    });
+  }
+
+  @Test
+  public void testToInt() {
+    assertThat(OerLengthSerializer.toInt(numberBytes)).isEqualTo(expectedPayloadLength);
+  }
+
+}
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceIntOerSerializerTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceIntOerSerializerTest.java
index 92f6e51..e132763 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceIntOerSerializerTest.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceIntOerSerializerTest.java
@@ -30,25 +30,33 @@ import org.interledger.encoding.asn.framework.CodecContextFactory;
 
 import com.google.common.io.BaseEncoding;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 
 /**
- * Parameterized unit tests for encoding an instance of {@link Integer}.
+ * * Parameterized unit tests for encoding a "Sequence Of" (i.e., an Array) of an Sequence (i.e., an object) containing
+ * a {@link Integer} via {@link AsnSequenceOfSequenceOerSerializer}.
  */
 @RunWith(Parameterized.class)
 public class SequenceOfSequenceIntOerSerializerTest {
 
   private final int[][] inputValue;
   private final byte[] asn1OerBytes;
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   private CodecContext codecContext;
 
   /**
@@ -57,8 +65,7 @@ public class SequenceOfSequenceIntOerSerializerTest {
    * @param inputValue   A {@code int} representing the unsigned 8bit integer to write in OER encoding.
    * @param asn1OerBytes The expected value, in binary, of the supplied {@code intValue}.
    */
-  public SequenceOfSequenceIntOerSerializerTest(final int[][] inputValue,
-      final byte[] asn1OerBytes) {
+  public SequenceOfSequenceIntOerSerializerTest(final int[][] inputValue, final byte[] asn1OerBytes) {
     this.inputValue = inputValue;
     this.asn1OerBytes = asn1OerBytes;
   }
@@ -99,7 +106,7 @@ public class SequenceOfSequenceIntOerSerializerTest {
    * Test setup.
    */
   @Before
-  public void setUp() throws Exception {
+  public void setUp() {
     // Register the codec to be tested...
     codecContext = CodecContextFactory.oer();
     codecContext.register(TestSequence.class, TestSequenceCodec::new);
@@ -143,10 +150,8 @@ public class SequenceOfSequenceIntOerSerializerTest {
     assertThat(byteArrayOutputStream.toByteArray()).isEqualTo(asn1OerBytes);
 
     // Read...
-    final ByteArrayInputStream byteArrayInputStream =
-        new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
-    final TestSequenceOfSequence decodedValue = codecContext
-        .read(TestSequenceOfSequence.class, byteArrayInputStream);
+    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+    final TestSequenceOfSequence decodedValue = codecContext.read(TestSequenceOfSequence.class, byteArrayInputStream);
 
     // Write...
     final ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream();
@@ -154,6 +159,70 @@ public class SequenceOfSequenceIntOerSerializerTest {
     assertThat(byteArrayOutputStream2.toByteArray()).isEqualTo(asn1OerBytes);
   }
 
+  /**
+   * This implementation guards against DOS attacks by not allocating arrays based upon values in ASN.1 OER packets. For
+   * example, one vector would be to assemble a Stream packet that with a quantity field set to {@link Short#MAX_VALUE},
+   * but only supply a single sequence. Naive implementations might accidentally allocate arrays off of this value,
+   * which could cause the program to exhaust the total avialble memory in the JVM, which would be a DOS vector. While
+   * this implementation doesn't allocate arrays in that manner, this test validates that bytes are not read past the
+   * total available bytes in the InputStream.
+   */
+  @Test
+  public void readWithLengthOneTooBig() throws IOException {
+    expectedException.expect(IOException.class);
+    expectedException.expectMessage("Unable to properly decode 2 bytes (could only read 0 bytes)");
+
+    // Write 1 SEQUENCE...
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    TestSequenceOfSequence sequences = new TestSequenceOfSequence();
+    for (int i = 0; i < 1; i++) {
+      sequences.add(i, new TestSequence(1, 2, 3));
+    }
+    codecContext.write(sequences, byteArrayOutputStream);
+
+    // Twiddle the byte at index=1 to falsely indicate that the total SEQUENCES is 2.
+    // byte[1] = 0x02
+    byte[] copiedByteArray = byteArrayOutputStream.toByteArray();
+    copiedByteArray[1] = (byte) 0x02;
+
+    // Read...
+    assertThat(copiedByteArray.length).isEqualTo(8);
+    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(copiedByteArray);
+    codecContext.read(TestSequenceOfSequence.class, byteArrayInputStream); // throws exception
+  }
+
+  /**
+   * This implementation guards against DOS attacks by not allocating arrays based upon values in ASN.1 OER packets. For
+   * example, one vector would be to assemble a Stream packet that with a quantity field set to {@link Short#MAX_VALUE},
+   * but only supply a single sequence. Naive implementations might accidentally allocate arrays off of this value,
+   * which could cause the program to exhaust the total avialble memory in the JVM, which would be a DOS vector. While
+   * this implementation doesn't allocate arrays in that manner, this test validates that bytes are not read past the
+   * total available bytes in the InputStream.
+   */
+  @Test
+  public void readWithLengthWayTooBig() throws IOException {
+    expectedException.expect(IOException.class);
+    expectedException.expectMessage("Unable to properly decode 2 bytes (could only read 0 bytes)");
+
+    // Write 1 SEQUENCE...
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    TestSequenceOfSequence sequences = new TestSequenceOfSequence();
+    for (int i = 0; i < 1; i++) {
+      sequences.add(i, new TestSequence(1, 2, 3));
+    }
+    codecContext.write(sequences, byteArrayOutputStream);
+
+    // Twiddle the byte at index=1 to falsely indicate that the total SEQUENCES is 255
+    // byte[1] = 0xFF
+    byte[] copiedByteArray = byteArrayOutputStream.toByteArray();
+    copiedByteArray[1] = (byte) 0xFF;
+
+    // Read...
+    assertThat(copiedByteArray.length).isEqualTo(8);
+    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(copiedByteArray);
+    codecContext.read(TestSequenceOfSequence.class, byteArrayInputStream); // throws exception
+  }
+
   /**
    * A sequence of 3 UInt8 values.
    */
diff --git a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceShortOerSerializerTest.java b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceShortOerSerializerTest.java
index e72608d..fc1b4d2 100644
--- a/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceShortOerSerializerTest.java
+++ b/codecs-parent/codecs-framework/src/test/java/org/interledger/encoding/asn/serializers/oer/SequenceOfSequenceShortOerSerializerTest.java
@@ -42,7 +42,8 @@ import java.util.Arrays;
 import java.util.Collection;
 
 /**
- * Parameterized unit tests for encoding an instance of {@link Short}.
+ * Parameterized unit tests for encoding a "Sequence Of" (i.e., an Array) of an Sequence (i.e., an object) containing a
+ * {@link Short}.
  */
 @RunWith(Parameterized.class)
 public class SequenceOfSequenceShortOerSerializerTest {
@@ -57,9 +58,7 @@ public class SequenceOfSequenceShortOerSerializerTest {
    * @param inputValue   A {@code short} representing the unsigned 8bit shorteger to write in OER encoding.
    * @param asn1OerBytes The expected value, in binary, of the supplied {@code shortValue}.
    */
-  public SequenceOfSequenceShortOerSerializerTest(
-      final short[][] inputValue, final byte[] asn1OerBytes
-  ) {
+  public SequenceOfSequenceShortOerSerializerTest(final short[][] inputValue, final byte[] asn1OerBytes) {
     this.inputValue = inputValue;
     this.asn1OerBytes = asn1OerBytes;
   }
diff --git a/codecs-parent/codecs-ilp/src/test/java/org/interledger/codecs/ilp/InterledgerFulfillPacketOerSerializerTests.java b/codecs-parent/codecs-ilp/src/test/java/org/interledger/codecs/ilp/InterledgerFulfillPacketOerSerializerTests.java
index 087f77a..074153d 100644
--- a/codecs-parent/codecs-ilp/src/test/java/org/interledger/codecs/ilp/InterledgerFulfillPacketOerSerializerTests.java
+++ b/codecs-parent/codecs-ilp/src/test/java/org/interledger/codecs/ilp/InterledgerFulfillPacketOerSerializerTests.java
@@ -97,8 +97,7 @@ public class InterledgerFulfillPacketOerSerializerTests {
     final CodecContext context = InterledgerCodecContextFactory.oer();
     final ByteArrayInputStream asn1OerPaymentBytes = constructAsn1OerPacketBytes();
 
-    final InterledgerFulfillPacket packet = context.read(InterledgerFulfillPacket.class,
-        asn1OerPaymentBytes);
+    final InterledgerFulfillPacket packet = context.read(InterledgerFulfillPacket.class, asn1OerPaymentBytes);
     assertThat(packet).isEqualTo(this.packet);
   }
 
@@ -111,8 +110,7 @@ public class InterledgerFulfillPacketOerSerializerTests {
     final CodecContext context = InterledgerCodecContextFactory.oer();
     final ByteArrayInputStream asn1OerPaymentBytes = constructAsn1OerPacketBytes();
 
-    final InterledgerPacket decodedPacket = context.read(InterledgerPacket.class,
-        asn1OerPaymentBytes);
+    final InterledgerPacket decodedPacket = context.read(InterledgerPacket.class, asn1OerPaymentBytes);
     assertThat(decodedPacket.getClass().getName()).isEqualTo(packet.getClass().getName());
     assertThat(decodedPacket).isEqualTo(packet);
   }
diff --git a/spsp-parent/spsp-core/src/test/java/org/interledger/spsp/PaymentPointerTest.java b/spsp-parent/spsp-core/src/test/java/org/interledger/spsp/PaymentPointerTest.java
index 3a95ff3..fbf0b7f 100644
--- a/spsp-parent/spsp-core/src/test/java/org/interledger/spsp/PaymentPointerTest.java
+++ b/spsp-parent/spsp-core/src/test/java/org/interledger/spsp/PaymentPointerTest.java
@@ -57,6 +57,13 @@ public class PaymentPointerTest {
         .isEqualTo(ImmutablePaymentPointer.builder().host("localhost").path("/").build());
   }
 
+  @Test
+  public void parsePaymentPointerWithEmailAddress() {
+    PaymentPointer paymentPointer = PaymentPointer.of("$rafiki.money/p/test@example.com");
+    assertThat(paymentPointer.host()).isEqualTo("rafiki.money");
+    assertThat(paymentPointer.path()).isEqualTo("/p/test@example.com");
+  }
+
   @Test
   public void exceptionIfDoesntStartWithDollarSign() {
     thrown.expect(IllegalArgumentException.class);
diff --git a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedReceiverAmountPaymentTrackerTest.java b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedReceiverAmountPaymentTrackerTest.java
index a469347..e323ebe 100644
--- a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedReceiverAmountPaymentTrackerTest.java
+++ b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedReceiverAmountPaymentTrackerTest.java
@@ -24,12 +24,12 @@ public class FixedReceiverAmountPaymentTrackerTest {
     assertThat(tracker.getOriginalAmountLeft()).isEqualTo(UnsignedLong.valueOf(10));
     assertThat(tracker.moreToSend()).isTrue();
 
-    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.ZERO, Denominations.XRP, Denominations.XRP);
+    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.ZERO, Denominations.XRP_DROPS, Denominations.XRP_DROPS);
 
     assertThat(amounts.getAmountToSend()).isEqualTo(UnsignedLong.ZERO);
     assertThat(amounts.getMinimumAmountToAccept()).isEqualTo(UnsignedLong.ZERO);
 
-    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP, Denominations.XRP);
+    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP_DROPS, Denominations.XRP_DROPS);
 
     assertThat(amounts.getAmountToSend()).isEqualTo(UnsignedLong.valueOf(6));
     assertThat(amounts.getMinimumAmountToAccept()).isEqualTo(UnsignedLong.valueOf(3));
@@ -79,7 +79,7 @@ public class FixedReceiverAmountPaymentTrackerTest {
     assertThat(tracker.getOriginalAmountLeft()).isEqualTo(UnsignedLong.ZERO);
     assertThat(tracker.moreToSend()).isFalse();
 
-    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP, Denominations.XRP);
+    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP_DROPS, Denominations.XRP_DROPS);
 
     assertThat(amounts.getAmountToSend()).isEqualTo(UnsignedLong.ZERO);
     assertThat(amounts.getMinimumAmountToAccept()).isEqualTo(UnsignedLong.ZERO);
diff --git a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedSenderAmountPaymentTrackerTest.java b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedSenderAmountPaymentTrackerTest.java
index b4c215e..6ae24c9 100644
--- a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedSenderAmountPaymentTrackerTest.java
+++ b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/FixedSenderAmountPaymentTrackerTest.java
@@ -24,12 +24,12 @@ public class FixedSenderAmountPaymentTrackerTest {
     assertThat(tracker.getOriginalAmountLeft()).isEqualTo(UnsignedLong.valueOf(12));
     assertThat(tracker.moreToSend()).isTrue();
 
-    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.ZERO, Denominations.XRP);
+    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.ZERO, Denominations.XRP_DROPS);
 
     assertThat(amounts.getAmountToSend()).isEqualTo(UnsignedLong.ZERO);
     assertThat(amounts.getMinimumAmountToAccept()).isEqualTo(UnsignedLong.ZERO);
 
-    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP);
+    amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(6), Denominations.XRP_DROPS);
 
     assertThat(amounts.getAmountToSend()).isEqualTo(UnsignedLong.valueOf(6));
     assertThat(amounts.getMinimumAmountToAccept()).isEqualTo(UnsignedLong.valueOf(3));
@@ -81,7 +81,7 @@ public class FixedSenderAmountPaymentTrackerTest {
     FixedSenderAmountPaymentTracker tracker = new FixedSenderAmountPaymentTracker(UnsignedLong.valueOf(12L),
         new HalfsiesExchangeRateCalculator());
 
-    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(12), Denominations.XRP);
+    PrepareAmounts amounts = tracker.getSendPacketAmounts(UnsignedLong.valueOf(12), Denominations.XRP_DROPS);
     tracker.auth(amounts);
     tracker.commit(amounts, UnsignedLong.valueOf(12));
     assertThat(tracker.getOriginalAmountLeft()).isEqualTo(UnsignedLong.ZERO);
diff --git a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SendMoneyAggregatorTest.java b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SendMoneyAggregatorTest.java
index 097f2a3..36f090a 100644
--- a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SendMoneyAggregatorTest.java
+++ b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SendMoneyAggregatorTest.java
@@ -23,7 +23,6 @@ import static org.interledger.core.InterledgerErrorCode.T04_INSUFFICIENT_LIQUIDI
 import static org.interledger.core.InterledgerErrorCode.T05_RATE_LIMITED;
 import static org.interledger.core.InterledgerErrorCode.T99_APPLICATION_ERROR;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -133,7 +132,7 @@ public class SendMoneyAggregatorTest {
         .destinationAddress(destinationAddress)
         .amount(originalAmountToSend)
         .timeout(Optional.of(Duration.ofSeconds(60)))
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .paymentTracker(paymentTracker)
         .build();
     this.sendMoneyAggregator = new SendMoneyAggregator(
@@ -243,7 +242,7 @@ public class SendMoneyAggregatorTest {
         .amount(originalAmountToSend)
         .destinationAddress(destinationAddress)
         .timeout(Optional.of(Duration.ofSeconds(60)))
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .paymentTracker(new FixedSenderAmountPaymentTracker(originalAmountToSend, new NoOpExchangeRateCalculator()))
         .build();
     ExecutorService executor = mock(ExecutorService.class);
@@ -383,7 +382,7 @@ public class SendMoneyAggregatorTest {
         .destinationAddress(destinationAddress)
         .amount(originalAmountToSend)
         .timeout(Optional.of(Duration.ofSeconds(60)))
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .paymentTracker(tracker)
         .build();
 
@@ -596,7 +595,7 @@ public class SendMoneyAggregatorTest {
         .destinationAddress(destinationAddress)
         .amount(originalAmountToSend)
         .timeout(Optional.of(Duration.ofSeconds(60)))
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .paymentTracker(
             new FixedSenderAmountPaymentTracker(UnsignedLong.valueOf(10l), new NoOpExchangeRateCalculator())
         )
@@ -670,7 +669,7 @@ public class SendMoneyAggregatorTest {
         .destinationAddress(destinationAddress)
         .amount(originalAmountToSend)
         .timeout(Optional.of(Duration.ofSeconds(60)))
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .paymentTracker(
             new FixedSenderAmountPaymentTracker(UnsignedLong.valueOf(10l), new NoOpExchangeRateCalculator()))
         .build();
diff --git a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
index afc5edb..896feb6 100644
--- a/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
+++ b/stream-parent/stream-client/src/test/java/org/interledger/stream/sender/SimpleStreamSenderIT.java
@@ -165,7 +165,7 @@ public class SimpleStreamSenderIT {
     SendMoneyRequest request = SendMoneyRequest.builder()
         .sourceAddress(SENDER_ADDRESS)
         .amount(paymentAmount)
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .destinationAddress(connectionDetails.destinationAddress())
         .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
         .sharedSecret(connectionDetails.sharedSecret())
@@ -197,7 +197,7 @@ public class SimpleStreamSenderIT {
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .destinationAddress(connectionDetails.destinationAddress())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
             .sharedSecret(connectionDetails.sharedSecret())
@@ -234,7 +234,7 @@ public class SimpleStreamSenderIT {
           SendMoneyRequest.builder()
               .sourceAddress(SENDER_ADDRESS)
               .amount(paymentAmount)
-              .denomination(Denominations.XRP)
+              .denomination(Denominations.XRP_DROPS)
               .destinationAddress(connectionDetails.destinationAddress())
               .sharedSecret(connectionDetails.sharedSecret())
               .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
@@ -293,7 +293,7 @@ public class SimpleStreamSenderIT {
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .destinationAddress(connectionDetails.destinationAddress())
             .sharedSecret(connectionDetails.sharedSecret())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
@@ -336,7 +336,7 @@ public class SimpleStreamSenderIT {
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .destinationAddress(connectionDetails.destinationAddress())
             .sharedSecret(connectionDetails.sharedSecret())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
@@ -428,7 +428,7 @@ public class SimpleStreamSenderIT {
                 SendMoneyRequest.builder()
                     .sourceAddress(SENDER_ADDRESS)
                     .amount(paymentAmount)
-                    .denomination(Denominations.XRP)
+                    .denomination(Denominations.XRP_DROPS)
                     .destinationAddress(connectionDetails.destinationAddress())
                     .sharedSecret(connectionDetails.sharedSecret())
                     .paymentTracker(
@@ -471,7 +471,7 @@ public class SimpleStreamSenderIT {
     SendMoneyRequest request = SendMoneyRequest.builder()
         .sourceAddress(SENDER_ADDRESS)
         .amount(paymentAmount)
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .destinationAddress(connectionDetails.destinationAddress())
         .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, noExchangeRateExceptionCalculator))
         .sharedSecret(connectionDetails.sharedSecret())
@@ -503,7 +503,7 @@ public class SimpleStreamSenderIT {
     SendMoneyRequest request = SendMoneyRequest.builder()
         .sourceAddress(SENDER_ADDRESS)
         .amount(paymentAmount)
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .destinationAddress(connectionDetails.destinationAddress())
         .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
         .sharedSecret(connectionDetails.sharedSecret())
@@ -533,7 +533,7 @@ public class SimpleStreamSenderIT {
     SendMoneyRequest request = SendMoneyRequest.builder()
         .sourceAddress(SENDER_ADDRESS)
         .amount(paymentAmount)
-        .denomination(Denominations.XRP)
+        .denomination(Denominations.XRP_DROPS)
         .destinationAddress(connectionDetails.destinationAddress())
         .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new GreedyExchangeRateCalculator()))
         .sharedSecret(connectionDetails.sharedSecret())
@@ -575,7 +575,7 @@ public class SimpleStreamSenderIT {
             .sourceAddress(SENDER_ADDRESS)
             .destinationAddress(connectionDetails.destinationAddress())
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .sharedSecret(connectionDetails.sharedSecret())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
             .timeout(Duration.ofMillis(100L))
@@ -600,7 +600,7 @@ public class SimpleStreamSenderIT {
         SendMoneyRequest.builder()
             .sourceAddress(SENDER_ADDRESS)
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .destinationAddress(InterledgerAddress.of("test.foo.bar.patrick"))
             .sharedSecret(connectionDetails.sharedSecret())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
@@ -651,7 +651,7 @@ public class SimpleStreamSenderIT {
             .sourceAddress(SENDER_ADDRESS)
             .destinationAddress(connectionDetails.destinationAddress())
             .amount(paymentAmount)
-            .denomination(Denominations.XRP)
+            .denomination(Denominations.XRP_DROPS)
             .sharedSecret(connectionDetails.sharedSecret())
             .paymentTracker(new FixedSenderAmountPaymentTracker(paymentAmount, new NoOpExchangeRateCalculator()))
             .build()
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/DenominationsTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/DenominationsTest.java
index 30d1d56..7165bc9 100644
--- a/stream-parent/stream-core/src/test/java/org/interledger/stream/DenominationsTest.java
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/DenominationsTest.java
@@ -4,22 +4,42 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 import org.junit.Test;
 
+/**
+ * Unit tests for {@link Denominations}.
+ */
 public class DenominationsTest {
 
   @Test
   public void values() {
+
     assertThat(Denominations.XRP)
         .extracting("assetCode", "assetScale")
         .containsExactly("XRP", (short) 6);
+
+    assertThat(Denominations.XRP_DROPS)
+        .extracting("assetCode", "assetScale")
+        .containsExactly("XRP", (short) 6);
+
+    assertThat(Denominations.XRP_MILLI_DROPS)
+        .extracting("assetCode", "assetScale")
+        .containsExactly("XRP", (short) 9);
+
     assertThat(Denominations.US_CENTS)
         .extracting("assetCode", "assetScale")
         .containsExactly("USD", (short) 2);
+
+    assertThat(Denominations.USD_CENTS)
+        .extracting("assetCode", "assetScale")
+        .containsExactly("USD", (short) 2);
+
     assertThat(Denominations.USD)
         .extracting("assetCode", "assetScale")
         .containsExactly("USD", (short) 0);
+
     assertThat(Denominations.EUR_CENTS)
         .extracting("assetCode", "assetScale")
         .containsExactly("EUR", (short) 2);
+
     assertThat(Denominations.EUR)
         .extracting("assetCode", "assetScale")
         .containsExactly("EUR", (short) 0);
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/calculators/ExchangeRateCalculatorTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/calculators/ExchangeRateCalculatorTest.java
index e9dffd9..d1c7957 100644
--- a/stream-parent/stream-core/src/test/java/org/interledger/stream/calculators/ExchangeRateCalculatorTest.java
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/calculators/ExchangeRateCalculatorTest.java
@@ -31,7 +31,7 @@ public class ExchangeRateCalculatorTest {
         return null;
       }
     };
-    assertThat(calculator.scaled(UnsignedLong.ONE, Denominations.US_CENTS)).isEqualTo(BigDecimal.valueOf(.01));
+    assertThat(calculator.scaled(UnsignedLong.ONE, Denominations.USD_CENTS)).isEqualTo(BigDecimal.valueOf(.01));
   }
 
   @Test
@@ -51,7 +51,7 @@ public class ExchangeRateCalculatorTest {
         return UnsignedLong.ZERO;
       }
     };
-    calculator.calculateMinAmountToAccept(UnsignedLong.ZERO, Denominations.US_CENTS);
+    calculator.calculateMinAmountToAccept(UnsignedLong.ZERO, Denominations.USD_CENTS);
     assertThat(called.get()).isTrue();
   }
 }