diff --git a/ethereum/rlp/src/jmh/java/tech/pegasys/pantheon/ethereum/rlp/RLPBench.java b/ethereum/rlp/src/jmh/java/tech/pegasys/pantheon/ethereum/rlp/RLPBench.java index 48389caa0f..82b1c3b92b 100644 --- a/ethereum/rlp/src/jmh/java/tech/pegasys/pantheon/ethereum/rlp/RLPBench.java +++ b/ethereum/rlp/src/jmh/java/tech/pegasys/pantheon/ethereum/rlp/RLPBench.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon.ethereum.rlp; +import tech.pegasys.pantheon.ethereum.rlp.util.RLPTestUtil; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.util.ArrayList; @@ -62,16 +63,16 @@ public class RLPBench { @Setup(Level.Trial) public void prepare() { toEncode = generate(depth, width, size); - toDecode = RLP.encode(toEncode); + toDecode = RLPTestUtil.encode(toEncode); } @Benchmark public BytesValue getBenchmarkEncoding() { - return RLP.encode(toEncode); + return RLPTestUtil.encode(toEncode); } @Benchmark public Object getBenchmarkDecoding() { - return RLP.decode(toDecode); + return RLPTestUtil.decode(toDecode); } } diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java index 993d6b8480..af0015458d 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java @@ -22,8 +22,6 @@ import tech.pegasys.pantheon.ethereum.rlp.RLPDecodingHelpers.Kind; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.MutableBytesValue; -import java.util.ArrayList; -import java.util.List; import java.util.function.Consumer; import io.vertx.core.buffer.Buffer; @@ -81,65 +79,6 @@ public abstract class RLP { return new BytesValueRLPInput(BytesValue.wrapBuffer(buffer, offset), false, false); } - /** - * Fully decodes a RLP encoded value. - * - *

This method is mostly intended for testing as it is often more convenient and - * efficient to use a {@link RLPInput} (through {@link #input(BytesValue)}) instead. - * - * @param value The RLP encoded value to decode. - * @return The output of decoding {@code value}. It will be either directly a {@link BytesValue}, - * or a list whose elements are either {@link BytesValue}, or similarly composed sub-lists. - * @throws RLPException if {@code value} is not a properly formed RLP encoding. - */ - public static Object decode(final BytesValue value) { - return decode(input(value)); - } - - private static Object decode(final RLPInput in) { - if (!in.nextIsList()) { - return in.readBytesValue(); - } - - final int size = in.enterList(); - final List l = new ArrayList<>(size); - for (int i = 0; i < size; i++) l.add(decode(in)); - in.leaveList(); - return l; - } - - /** - * Fully RLP encode an object consisting of recursive lists of {@link BytesValue}. - * - *

This method is mostly intended for testing as it is often more convenient and - * efficient to use a {@link RLPOutput} (through {@link #encode(Consumer)} for instance) instead. - * - * @param obj An object that must be either directly a {@link BytesValue}, or a list whose - * elements are either {@link BytesValue}, or similarly composed sub-lists. - * @return The RLP encoding corresponding to {@code obj}. - * @throws IllegalArgumentException if {@code obj} is not a valid input (not entirely composed - * from lists and {@link BytesValue}). - */ - public static BytesValue encode(final Object obj) { - final BytesValueRLPOutput out = new BytesValueRLPOutput(); - encode(obj, out); - return out.encoded(); - } - - private static void encode(final Object obj, final RLPOutput out) { - if (obj instanceof BytesValue) { - out.writeBytesValue((BytesValue) obj); - } else if (obj instanceof List) { - final List l = (List) obj; - out.startList(); - for (final Object o : l) encode(o, out); - out.endList(); - } else { - throw new IllegalArgumentException( - format("Invalid input type %s for RLP encoding", obj.getClass())); - } - } - /** * Creates a {@link RLPOutput}, pass it to the provided consumer for writing, and then return the * RLP encoded result of that writing. diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java index 9c6c723537..d016caaf27 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java @@ -66,8 +66,19 @@ class RLPDecodingHelpers { } /** Read from the provided offset a size of the provided length, assuming this is enough bytes. */ - static int extractSizeFromLong( + static int extractSizeFromLongItem( final LongUnaryOperator getter, final long offset, final int sizeLength) { + String oversizedErrorMessage = + "RLP item at offset " + + offset + + " with size value consuming " + + sizeLength + + " bytes exceeds max supported size of " + + Integer.MAX_VALUE; + if (sizeLength > 4) { + throw new RLPException(oversizedErrorMessage); + } + long res = 0; int shift = 0; for (int i = 0; i < sizeLength; i++) { @@ -77,9 +88,7 @@ class RLPDecodingHelpers { try { return Math.toIntExact(res); } catch (final ArithmeticException e) { - final String msg = - "unable to extract size from long at offset " + offset + ", sizeLen=" + sizeLength; - throw new RLPException(msg, e); + throw new RLPException(oversizedErrorMessage, e); } } @@ -139,7 +148,7 @@ class RLPDecodingHelpers { throw new MalformedRLPInputException("Malformed RLP item: size of payload has leading zeros"); } - final int res = RLPDecodingHelpers.extractSizeFromLong(byteGetter, item + 1, sizeLength); + final int res = RLPDecodingHelpers.extractSizeFromLongItem(byteGetter, item + 1, sizeLength); // We should not have had the size written separately if it was less than 56 bytes long. if (res < 56) { @@ -166,7 +175,15 @@ class RLPDecodingHelpers { /** @return the size of the byte string holding the rlp-encoded value and metadata */ int getEncodedSize() { - return Math.toIntExact(elementEnd() - elementStart + 1); + long encodedSize = elementEnd() - elementStart + 1; + try { + return Math.toIntExact(encodedSize); + } catch (ArithmeticException e) { + String errorMessage = + String.format( + "RLP item exceeds max supported size of %d: %d", Integer.MAX_VALUE, encodedSize); + throw new RLPException(errorMessage, e); + } } /** diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java index a08ac0701d..a6fff6dc17 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java @@ -12,13 +12,17 @@ */ package tech.pegasys.pantheon.ethereum.rlp; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.bytes.BytesValues; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.Test; public class BytesValueRLPInputTest { @@ -36,104 +40,104 @@ public class BytesValueRLPInputTest { @Test public void empty() { final RLPInput in = RLP.input(BytesValue.EMPTY); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void singleByte() { final RLPInput in = RLP.input(h("0x01")); - assertFalse(in.isDone()); - assertEquals((byte) 1, in.readByte()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readByte()).isEqualTo((byte) 1); + assertThat(in.isDone()).isTrue(); } @Test public void singleByteLowerBoundary() { final RLPInput in = RLP.input(h("0x00")); - assertFalse(in.isDone()); - assertEquals((byte) 0, in.readByte()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readByte()).isEqualTo((byte) 0); + assertThat(in.isDone()).isTrue(); } @Test public void singleByteUpperBoundary() { final RLPInput in = RLP.input(h("0x7f")); - assertFalse(in.isDone()); - assertEquals((byte) 0x7f, in.readByte()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readByte()).isEqualTo((byte) 0x7f); + assertThat(in.isDone()).isTrue(); } @Test public void singleShortElement() { final RLPInput in = RLP.input(h("0x81FF")); - assertFalse(in.isDone()); - assertEquals((byte) 0xFF, in.readByte()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readByte()).isEqualTo((byte) 0xFF); + assertThat(in.isDone()).isTrue(); } @Test public void singleBarelyShortElement() { final RLPInput in = RLP.input(h("0xb7" + times("2b", 55))); - assertFalse(in.isDone()); - assertEquals(h(times("2b", 55)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("2b", 55))); + assertThat(in.isDone()).isTrue(); } @Test public void singleBarelyLongElement() { final RLPInput in = RLP.input(h("0xb838" + times("2b", 56))); - assertFalse(in.isDone()); - assertEquals(h(times("2b", 56)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("2b", 56))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElement() { final RLPInput in = RLP.input(h("0xb908c1" + times("3c", 2241))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 2241)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 2241))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElementBoundaryCase_1() { final RLPInput in = RLP.input(h("0xb8ff" + times("3c", 255))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 255)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 255))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElementBoundaryCase_2() { final RLPInput in = RLP.input(h("0xb90100" + times("3c", 256))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 256)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 256))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElementBoundaryCase_3() { final RLPInput in = RLP.input(h("0xb9ffff" + times("3c", 65535))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 65535)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 65535))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElementBoundaryCase_4() { final RLPInput in = RLP.input(h("0xba010000" + times("3c", 65536))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 65536)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 65536))); + assertThat(in.isDone()).isTrue(); } @Test public void singleLongElementBoundaryCase_5() { final RLPInput in = RLP.input(h("0xbaffffff" + times("3c", 16777215))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 16777215)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 16777215))); + assertThat(in.isDone()).isTrue(); } @Test @@ -141,9 +145,9 @@ public class BytesValueRLPInputTest { // A RLPx Frame can have a maximum length of 0xffffff, so boundary above this // will be not be real world scenarios. final RLPInput in = RLP.input(h("0xbb01000000" + times("3c", 16777216))); - assertFalse(in.isDone()); - assertEquals(h(times("3c", 16777216)), in.readBytesValue()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(h(times("3c", 16777216))); + assertThat(in.isDone()).isTrue(); } @Test @@ -168,9 +172,9 @@ public class BytesValueRLPInputTest { private void assertLongScalar(final long expected, final BytesValue toTest) { final RLPInput in = RLP.input(toTest); - assertFalse(in.isDone()); - assertEquals(expected, in.readLongScalar()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readLongScalar()).isEqualTo(expected); + assertThat(in.isDone()).isTrue(); } @Test @@ -186,128 +190,136 @@ public class BytesValueRLPInputTest { private void assertIntScalar(final int expected, final BytesValue toTest) { final RLPInput in = RLP.input(toTest); - assertFalse(in.isDone()); - assertEquals(expected, in.readIntScalar()); - assertTrue(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readIntScalar()).isEqualTo(expected); + assertThat(in.isDone()).isTrue(); } @Test public void emptyList() { final RLPInput in = RLP.input(h("0xc0")); - assertFalse(in.isDone()); - assertEquals(0, in.enterList()); - assertFalse(in.isDone()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(0); + assertThat(in.isDone()).isFalse(); in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); + } + + @Test + public void emptyByteString() { + final RLPInput in = RLP.input(h("0x80")); + assertThat(in.isDone()).isFalse(); + assertThat(in.readBytesValue()).isEqualTo(BytesValue.EMPTY); + assertThat(in.isDone()).isTrue(); } @Test public void simpleShortList() { final RLPInput in = RLP.input(h("0xc22c3b")); - assertFalse(in.isDone()); - assertEquals(2, in.enterList()); - assertEquals((byte) 0x2c, in.readByte()); - assertEquals((byte) 0x3b, in.readByte()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(2); + assertThat(in.readByte()).isEqualTo((byte) 0x2c); + assertThat(in.readByte()).isEqualTo((byte) 0x3b); in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleIntBeforeShortList() { final RLPInput in = RLP.input(h("0x02c22c3b")); - assertFalse(in.isDone()); - assertEquals(2, in.readIntScalar()); - assertEquals(2, in.enterList()); - assertEquals((byte) 0x2c, in.readByte()); - assertEquals((byte) 0x3b, in.readByte()); + assertThat(in.isDone()).isFalse(); + assertThat(in.readIntScalar()).isEqualTo(2); + assertThat(in.enterList()).isEqualTo(2); + assertThat(in.readByte()).isEqualTo((byte) 0x2c); + assertThat(in.readByte()).isEqualTo((byte) 0x3b); in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleShortListUpperBoundary() { final RLPInput in = RLP.input(h("0xf7" + times("3c", 55))); - assertFalse(in.isDone()); - assertEquals(55, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(55); for (int i = 0; i < 55; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListLowerBoundary() { final RLPInput in = RLP.input(h("0xf838" + times("3c", 56))); - assertFalse(in.isDone()); - assertEquals(56, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(56); for (int i = 0; i < 56; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListBoundaryCase_1() { final RLPInput in = RLP.input(h("0xf8ff" + times("3c", 255))); - assertFalse(in.isDone()); - assertEquals(255, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(255); for (int i = 0; i < 255; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListBoundaryCase_2() { final RLPInput in = RLP.input(h("0xf90100" + times("3c", 256))); - assertFalse(in.isDone()); - assertEquals(256, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(256); for (int i = 0; i < 256; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListBoundaryCase_3() { final RLPInput in = RLP.input(h("0xf9ffff" + times("3c", 65535))); - assertFalse(in.isDone()); - assertEquals(65535, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(65535); for (int i = 0; i < 65535; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListBoundaryCase_4() { final RLPInput in = RLP.input(h("0xfa010000" + times("3c", 65536))); - assertFalse(in.isDone()); - assertEquals(65536, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(65536); for (int i = 0; i < 65536; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleLongListBoundaryCase_5() { final RLPInput in = RLP.input(h("0xfaffffff" + times("3c", 16777215))); - assertFalse(in.isDone()); - assertEquals(16777215, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(16777215); for (int i = 0; i < 16777215; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test @@ -315,39 +327,39 @@ public class BytesValueRLPInputTest { // A RLPx Frame can have a maximum length of 0xffffff, so boundary above this // will be not be real world scenarios. final RLPInput in = RLP.input(h("0xfb01000000" + times("3c", 16777216))); - assertFalse(in.isDone()); - assertEquals(16777216, in.enterList()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(16777216); for (int i = 0; i < 16777216; i++) { - assertEquals((byte) 0x3c, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3c); } in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleListwithBytesValue() { final RLPInput in = RLP.input(h("0xc28180")); - assertFalse(in.isDone()); - assertEquals(1, in.enterList()); - assertEquals(h("0x80"), in.readBytesValue()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(1); + assertThat(in.readBytesValue()).isEqualTo(h("0x80")); in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test public void simpleNestedList() { final RLPInput in = RLP.input(h("0xc52cc203123b")); - assertFalse(in.isDone()); - assertEquals(3, in.enterList()); - assertEquals((byte) 0x2c, in.readByte()); - assertEquals(2, in.enterList()); - assertEquals((byte) 0x03, in.readByte()); - assertEquals((byte) 0x12, in.readByte()); + assertThat(in.isDone()).isFalse(); + assertThat(in.enterList()).isEqualTo(3); + assertThat(in.readByte()).isEqualTo((byte) 0x2c); + assertThat(in.enterList()).isEqualTo(2); + assertThat(in.readByte()).isEqualTo((byte) 0x03); + assertThat(in.readByte()).isEqualTo((byte) 0x12); in.leaveList(); - assertEquals((byte) 0x3b, in.readByte()); + assertThat(in.readByte()).isEqualTo((byte) 0x3b); in.leaveList(); - assertTrue(in.isDone()); + assertThat(in.isDone()).isTrue(); } @Test @@ -355,19 +367,19 @@ public class BytesValueRLPInputTest { // Test null value final BytesValue nullValue = h("0x80"); final RLPInput nv = RLP.input(nullValue); - assertEquals(nv.raw(), nv.readAsRlp().raw()); + assertThat(nv.readAsRlp().raw()).isEqualTo(nv.raw()); nv.reset(); - assertTrue(nv.nextIsNull()); - assertTrue(nv.readAsRlp().nextIsNull()); + assertThat(nv.nextIsNull()).isTrue(); + assertThat(nv.readAsRlp().nextIsNull()).isTrue(); // Test empty list final BytesValue emptyList = h("0xc0"); final RLPInput el = RLP.input(emptyList); - assertEquals(emptyList, el.readAsRlp().raw()); + assertThat(el.readAsRlp().raw()).isEqualTo(emptyList); el.reset(); - assertEquals(0, el.readAsRlp().enterList()); + assertThat(el.readAsRlp().enterList()).isEqualTo(0); el.reset(); - assertEquals(0, el.enterList()); + assertThat(el.enterList()).isEqualTo(0); final BytesValue nestedList = RLP.encode( @@ -388,16 +400,16 @@ public class BytesValueRLPInputTest { final RLPInput nl = RLP.input(nestedList); final RLPInput compare = nl.readAsRlp(); - assertEquals(nl.raw(), compare.raw()); + assertThat(compare.raw()).isEqualTo(nl.raw()); nl.reset(); nl.enterList(); nl.skipNext(); // 0x01 // Read the next byte that's inside the list, extract it as raw RLP and assert it's its own // representation. - assertEquals(h("0x02"), nl.readAsRlp().raw()); + assertThat(nl.readAsRlp().raw()).isEqualTo(h("0x02")); // Extract the inner list. - assertEquals(h("0xc51112c22122"), nl.readAsRlp().raw()); + assertThat(nl.readAsRlp().raw()).isEqualTo(h("0xc51112c22122")); // Reset nl.reset(); nl.enterList(); @@ -408,29 +420,29 @@ public class BytesValueRLPInputTest { nl.skipNext(); // Assert on the inner list of depth 3. - assertEquals(h("0xc22122"), nl.readAsRlp().raw()); + assertThat(nl.readAsRlp().raw()).isEqualTo(h("0xc22122")); } @Test public void raw() { final BytesValue initial = h("0xc80102c51112c22122"); final RLPInput in = RLP.input(initial); - assertEquals(initial, in.raw()); + assertThat(in.raw()).isEqualTo(initial); } @Test public void reset() { final RLPInput in = RLP.input(h("0xc80102c51112c22122")); for (int i = 0; i < 100; i++) { - assertEquals(3, in.enterList()); - assertEquals(0x01, in.readByte()); - assertEquals(0x02, in.readByte()); - assertEquals(3, in.enterList()); - assertEquals(0x11, in.readByte()); - assertEquals(0x12, in.readByte()); - assertEquals(2, in.enterList()); - assertEquals(0x21, in.readByte()); - assertEquals(0x22, in.readByte()); + assertThat(in.enterList()).isEqualTo(3); + assertThat(in.readByte()).isEqualTo((byte) 0x01); + assertThat(in.readByte()).isEqualTo((byte) 0x02); + assertThat(in.enterList()).isEqualTo(3); + assertThat(in.readByte()).isEqualTo((byte) 0x11); + assertThat(in.readByte()).isEqualTo((byte) 0x12); + assertThat(in.enterList()).isEqualTo(2); + assertThat(in.readByte()).isEqualTo((byte) 0x21); + assertThat(in.readByte()).isEqualTo((byte) 0x22); in.reset(); } } @@ -438,16 +450,16 @@ public class BytesValueRLPInputTest { @Test public void ignoreListTail() { final RLPInput in = RLP.input(h("0xc80102c51112c22122")); - assertEquals(3, in.enterList()); - assertEquals(0x01, in.readByte()); + assertThat(in.enterList()).isEqualTo(3); + assertThat(in.readByte()).isEqualTo((byte) 0x01); in.leaveList(true); } @Test public void leaveListEarly() { final RLPInput in = RLP.input(h("0xc80102c51112c22122")); - assertEquals(3, in.enterList()); - assertEquals(0x01, in.readByte()); + assertThat(in.enterList()).isEqualTo(3); + assertThat(in.readByte()).isEqualTo((byte) 0x01); assertThatThrownBy(() -> in.leaveList(false)) .isInstanceOf(RLPException.class) .hasMessageStartingWith("Not at the end of the current list"); @@ -472,10 +484,10 @@ public class BytesValueRLPInputTest { final BytesValue correctBytes = h( "0xB8380102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"); - assertEquals( - RLP.input(correctBytes).readBytesValue(), - h( - "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738")); + assertThat( + h( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738")) + .isEqualTo(RLP.input(correctBytes).readBytesValue()); // Encode same value, but use 2 bytes to represent the size, and pad size value with leading // zeroes @@ -494,10 +506,10 @@ public class BytesValueRLPInputTest { final BytesValue correctBytes = h( "0xB70102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637"); - assertEquals( - RLP.input(correctBytes).readBytesValue(), - h( - "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")); + assertThat( + h( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")) + .isEqualTo(RLP.input(correctBytes).readBytesValue()); // Encode same value using long format final BytesValue incorrectBytes = @@ -508,4 +520,115 @@ public class BytesValueRLPInputTest { .hasRootCauseInstanceOf(MalformedRLPInputException.class) .hasMessageContaining("written as a long item, but size 55 < 56 bytes"); } + + @Test + public void rlpItemSizeHoldsMaxValue() { + // Size value encode max positive int. So, size is decoded, but + // RLP is malformed because the actual payload is not present + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xBB7FFFFFFF")).readBytesValue()) + .isInstanceOf(CorruptedRLPInputException.class) + .hasMessageContaining("payload should start at offset 5 but input has only 5 bytes"); + } + + @Test + public void rlpItemSizeOverflowsSignedInt() { + // Size value encoded in 4 bytes but exceeds max positive int value + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xBB80000000"))) + .isInstanceOf(RLPException.class) + .hasMessageContaining( + "RLP item at offset 1 with size value consuming 4 bytes exceeds max supported size of 2147483647"); + } + + @Test + public void rlpItemSizeOverflowsInt() { + // Size value is encoded with 5 bytes - overflowing int + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xBC0100000000"))) + .isInstanceOf(RLPException.class) + .hasMessageContaining( + "RLP item at offset 1 with size value consuming 5 bytes exceeds max supported size of 2147483647"); + } + + @Test + public void rlpListSizeHoldsMaxValue() { + // Size value encode max positive int. So, size is decoded, but + // RLP is malformed because the actual payload is not present + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xFB7FFFFFFF")).readBytesValue()) + .isInstanceOf(CorruptedRLPInputException.class) + .hasMessageContaining( + "Input doesn't have enough data for RLP encoding: encoding advertise a payload ending at byte 2147483652 but input has size 5"); + } + + @Test + public void rlpListSizeOverflowsSignedInt() { + // Size value encoded in 4 bytes but exceeds max positive int value + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xFB80000000"))) + .isInstanceOf(RLPException.class) + .hasMessageContaining( + "RLP item at offset 1 with size value consuming 4 bytes exceeds max supported size of 2147483647"); + } + + @Test + public void rlpListSizeOverflowsInt() { + // Size value is encoded with 5 bytes - overflowing int + AssertionsForClassTypes.assertThatThrownBy(() -> RLP.input(h("0xFC0100000000"))) + .isInstanceOf(RLPException.class) + .hasMessageContaining( + "RLP item at offset 1 with size value consuming 5 bytes exceeds max supported size of 2147483647"); + } + + @SuppressWarnings("ReturnValueIgnored") + @Test + public void decodeValueWithLeadingZerosAsScalar() { + String value = "0x8200D0"; + + List> invalidDecoders = + Arrays.asList( + RLPInput::readBigIntegerScalar, + RLPInput::readIntScalar, + RLPInput::readLongScalar, + RLPInput::readUInt256Scalar); + + for (Function decoder : invalidDecoders) { + RLPInput in = RLP.input(h(value)); + AssertionsForClassTypes.assertThatThrownBy(() -> decoder.apply(in)) + .isInstanceOf(MalformedRLPInputException.class) + .hasMessageContaining("Invalid scalar"); + } + } + + @Test + public void decodeValueWithLeadingZerosAsUnsignedInt() { + RLPInput in = RLP.input(h("0x84000000D0")); + assertThat(in.readUnsignedInt()).isEqualTo(208); + } + + @Test + public void decodeValueWithLeadingZerosAsUnsignedShort() { + RLPInput in = RLP.input(h("0x8200D0")); + assertThat(in.readUnsignedShort()).isEqualTo(208); + } + + @Test + public void decodeValueWithLeadingZerosAsSignedInt() { + RLPInput in = RLP.input(h("0x84000000D0")); + assertThat(in.readInt()).isEqualTo(208); + } + + @Test + public void decodeValueWithLeadingZerosAsSignedLong() { + RLPInput in = RLP.input(h("0x8800000000000000D0")); + assertThat(in.readLong()).isEqualTo(208); + } + + @Test + public void decodeValueWithLeadingZerosAsSignedShort() { + RLPInput in = RLP.input(h("0x8200D0")); + assertThat(in.readShort()).isEqualTo((short) 208); + } + + @Test + public void decodeValueWithLeadingZerosAsBytesValue() { + RLPInput in = RLP.input(h("0x8800000000000000D0")); + assertThat(BytesValues.extractLong(in.readBytesValue())).isEqualTo(208); + } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java index 29c04e9000..ce6e9209fe 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java @@ -12,7 +12,7 @@ */ package tech.pegasys.pantheon.ethereum.rlp; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -33,7 +33,18 @@ public class BytesValueRLPOutputTest { @Test public void empty() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); - assertEquals(BytesValue.EMPTY, out.encoded()); + + assertThat(out.encoded()).isEqualTo(BytesValue.EMPTY); + assertThat(out.encoded().toString()).isEqualTo("0x"); + } + + @Test + public void emptyBytesString() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(BytesValue.EMPTY); + + assertThat(out.encoded()).isEqualTo(RLP.NULL); + assertThat(out.encoded().toString()).isEqualTo("0x80"); } @Test @@ -42,21 +53,21 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 1); // Single byte should be encoded as itself - assertEquals(h("0x01"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0x01")); } @Test public void singleByteLowerBoundary() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeByte((byte) 0); - assertEquals(h("0x00"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0x00")); } @Test public void singleByteUpperBoundary() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeByte((byte) 0x7f); - assertEquals(h("0x7f"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0x7f")); } @Test @@ -65,7 +76,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0xFF); // Bigger than single byte: 0x80 + length then value, where length is 1. - assertEquals(h("0x81FF"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0x81FF")); } @Test @@ -74,7 +85,7 @@ public class BytesValueRLPOutputTest { out.writeBytesValue(h(times("2b", 55))); // 55 bytes, so still short: 0x80 + length then value, where length is 55. - assertEquals(h("0xb7" + times("2b", 55)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb7" + times("2b", 55))); } @Test @@ -84,7 +95,7 @@ public class BytesValueRLPOutputTest { // 56 bytes, so long element: 0xb7 + length of value size + value, where the value size is 56. // 56 is 0x38 so its size is 1 byte. - assertEquals(h("0xb838" + times("2b", 56)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb838" + times("2b", 56))); } @Test @@ -95,49 +106,49 @@ public class BytesValueRLPOutputTest { // 2241 bytes, so long element: 0xb7 + length of value size + value, where the value size is // 2241, // 2241 is 0x8c1 so its size is 2 bytes. - assertEquals(h("0xb908c1" + times("3c", 2241)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb908c1" + times("3c", 2241))); } @Test public void singleLongElementBoundaryCase_1() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 255))); - assertEquals(h("0xb8ff" + times("3c", 255)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb8ff" + times("3c", 255))); } @Test public void singleLongElementBoundaryCase_2() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 256))); - assertEquals(h("0xb90100" + times("3c", 256)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb90100" + times("3c", 256))); } @Test public void singleLongElementBoundaryCase_3() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 65535))); - assertEquals(h("0xb9ffff" + times("3c", 65535)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xb9ffff" + times("3c", 65535))); } @Test public void singleLongElementBoundaryCase_4() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 65536))); - assertEquals(h("0xba010000" + times("3c", 65536)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xba010000" + times("3c", 65536))); } @Test public void singleLongElementBoundaryCase_5() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 16777215))); - assertEquals(h("0xbaffffff" + times("3c", 16777215)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xbaffffff" + times("3c", 16777215))); } @Test public void singleLongElementBoundaryCase_6() { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeBytesValue(h(times("3c", 16777216))); - assertEquals(h("0xbb01000000" + times("3c", 16777216)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xbb01000000" + times("3c", 16777216))); } @Test(expected = IllegalStateException.class) @@ -161,7 +172,7 @@ public class BytesValueRLPOutputTest { private void assertLongScalar(final BytesValue expected, final long toTest) { final BytesValueRLPOutput out = new BytesValueRLPOutput(); out.writeLongScalar(toTest); - assertEquals(expected, out.encoded()); + assertThat(out.encoded()).isEqualTo(expected); } @Test @@ -170,7 +181,7 @@ public class BytesValueRLPOutputTest { out.startList(); out.endList(); - assertEquals(h("0xc0"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xc0")); } @Test(expected = IllegalStateException.class) @@ -196,7 +207,7 @@ public class BytesValueRLPOutputTest { // List with payload size = 2 (both element are single bytes) // so 0xc0 + size then payloads - assertEquals(h("0xc22c3b"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xc22c3b")); } @Test @@ -207,7 +218,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xf7" + times("3c", 55)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xf7" + times("3c", 55))); } @Test @@ -218,7 +229,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xf838" + times("3c", 56)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xf838" + times("3c", 56))); } @Test @@ -229,7 +240,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xf8ff" + times("3c", 255)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xf8ff" + times("3c", 255))); } @Test @@ -240,7 +251,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xf90100" + times("3c", 256)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xf90100" + times("3c", 256))); } @Test @@ -251,7 +262,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xf9ffff" + times("3c", 65535)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xf9ffff" + times("3c", 65535))); } @Test @@ -262,7 +273,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xfa010000" + times("3c", 65536)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xfa010000" + times("3c", 65536))); } @Test @@ -273,7 +284,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xfaffffff" + times("3c", 16777215)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xfaffffff" + times("3c", 16777215))); } @Test @@ -284,7 +295,7 @@ public class BytesValueRLPOutputTest { out.writeByte((byte) 0x3c); } out.endList(); - assertEquals(h("0xfb01000000" + times("3c", 16777216)), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xfb01000000" + times("3c", 16777216))); } @Test @@ -302,6 +313,6 @@ public class BytesValueRLPOutputTest { // List payload size = 5 (2 single bytes element + nested list of size 3) // so 0xc0 + size then payloads - assertEquals(h("0xc52cc203123b"), out.encoded()); + assertThat(out.encoded()).isEqualTo(h("0xc52cc203123b")); } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/InvalidRLPRefTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/InvalidRLPRefTest.java index 505bfa7e1c..0b0ef2a9ca 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/InvalidRLPRefTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/InvalidRLPRefTest.java @@ -14,6 +14,7 @@ package tech.pegasys.pantheon.ethereum.rlp; import static org.junit.Assume.assumeTrue; +import tech.pegasys.pantheon.ethereum.rlp.util.RLPTestUtil; import tech.pegasys.pantheon.testutil.JsonTestParameters; import java.util.Collection; @@ -48,6 +49,6 @@ public class InvalidRLPRefTest { /** Test RLP decoding. */ @Test(expected = RLPException.class) public void decode() throws Exception { - RLP.decode(spec.getRLP()); + RLPTestUtil.decode(spec.getRLP()); } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPRefTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPRefTest.java index 65b020c6df..a0f7e43d07 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPRefTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPRefTest.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.ethereum.rlp; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; +import tech.pegasys.pantheon.ethereum.rlp.util.RLPTestUtil; import tech.pegasys.pantheon.testutil.JsonTestParameters; import java.util.Collection; @@ -44,11 +45,11 @@ public class RLPRefTest { @Test public void encode() { - assertEquals(spec.getOut(), RLP.encode(spec.getIn())); + assertEquals(spec.getOut(), RLPTestUtil.encode(spec.getIn())); } @Test public void decode() { - assertEquals(spec.getIn(), RLP.decode(spec.getOut())); + assertEquals(spec.getIn(), RLPTestUtil.decode(spec.getOut())); } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java index 9a9cebd784..fafb36e07e 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java @@ -12,10 +12,14 @@ */ package tech.pegasys.pantheon.ethereum.rlp; -import static junit.framework.TestCase.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import tech.pegasys.pantheon.ethereum.rlp.util.RLPTestUtil; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.Random; + import org.junit.Test; public class RLPTest { @@ -23,48 +27,146 @@ public class RLPTest { @Test public void calculateSize_singleByteValue() { int size = RLP.calculateSize(BytesValue.fromHexString("0x01")); - assertEquals(1, size); + assertThat(size).isEqualTo(1); + } + + @Test + public void calculateSize_minSingleByteValue() { + int size = RLP.calculateSize(BytesValue.fromHexString("0x00")); + assertThat(size).isEqualTo(1); + } + + @Test + public void calculateSize_maxSingleByteValue() { + int size = RLP.calculateSize(BytesValue.fromHexString("0x7F")); + assertThat(size).isEqualTo(1); } @Test public void calculateSize_smallByteString() { // Prefix indicates a payload of size 5, with a 1 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0x85")); - assertEquals(6, size); + assertThat(size).isEqualTo(6); + } + + @Test + public void calculateSize_nullByteString() { + // Prefix indicates a payload of size 0, with a 1 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0x80")); + assertThat(size).isEqualTo(1); + } + + @Test + public void calculateSize_minNonNullSmallByteString() { + // Prefix indicates a payload of size 1, with a 1 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0x81")); + assertThat(size).isEqualTo(2); + } + + @Test + public void calculateSize_maxSmallByteString() { + // Prefix indicates a payload of size 55, with a 1 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xB7")); + assertThat(size).isEqualTo(56); } @Test public void calculateSize_longByteString() { // Prefix indicates a payload of 56 bytes, with a 2 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0xB838")); - assertEquals(58, size); + assertThat(size).isEqualTo(58); } @Test public void calculateSize_longByteStringWithMultiByteSize() { // Prefix indicates a payload of 258 bytes, with a 3 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0xB90102")); - assertEquals(261, size); + assertThat(size).isEqualTo(261); } @Test public void calculateSize_shortList() { // Prefix indicates a payload of 5 bytes, with a 1 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0xC5")); - assertEquals(6, size); + assertThat(size).isEqualTo(6); + } + + @Test + public void calculateSize_emptyList() { + int size = RLP.calculateSize(BytesValue.fromHexString("0xC0")); + assertThat(size).isEqualTo(1); + } + + @Test + public void calculateSize_minNonEmptyList() { + int size = RLP.calculateSize(BytesValue.fromHexString("0xC1")); + assertThat(size).isEqualTo(2); + } + + @Test + public void calculateSize_maxShortList() { + int size = RLP.calculateSize(BytesValue.fromHexString("0xF7")); + assertThat(size).isEqualTo(56); } @Test public void calculateSize_longList() { // Prefix indicates a payload of 56 bytes, with a 2 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0xF838")); - assertEquals(58, size); + assertThat(size).isEqualTo(58); } @Test public void calculateSize_longListWithMultiByteSize() { // Prefix indicates a payload of 258 bytes, with a 3 byte prefix int size = RLP.calculateSize(BytesValue.fromHexString("0xF90102")); - assertEquals(261, size); + assertThat(size).isEqualTo(261); + } + + @Test + public void calculateSize_fuzz() { + final Random random = new Random(1); + for (int i = 0; i < 1000; ++i) { + BytesValueRLPOutput out = RLPTestUtil.randomRLPValue(random.nextInt()); + assertThat(RLP.calculateSize(out.encoded())).isEqualTo(out.encodedSize()); + } + } + + @Test + public void calculateSize_extremelyDeepNestedList() { + final int MAX_DEPTH = 20000; + + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + int depth = 0; + + for (int i = 0; i < MAX_DEPTH; ++i) { + out.startList(); + depth += 1; + } + while (depth > 0) { + out.endList(); + --depth; + } + assertThat(RLP.calculateSize(out.encoded())).isEqualTo(out.encodedSize()); + } + + @Test + public void calculateSize_maxRLPStringLength() { + // Value represents a single item with an encoded payload size of MAX_VALUE - 5 and + // 5 bytes of metadata (payload is not actually present) + assertThat(RLP.calculateSize(h("0xBB7FFFFFFA"))).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void calculateSize_overflowMaxRLPStringLength() { + // Value represents a single item with an encoded payload size of MAX_VALUE - 4 and + // 5 bytes of metadata (payload is not actually present) + assertThatThrownBy(() -> RLP.calculateSize(h("0xBB7FFFFFFB"))) + .isInstanceOf(RLPException.class) + .hasMessageContaining("RLP item exceeds max supported size of 2147483647: 2147483648"); + } + + private static BytesValue h(final String hex) { + return BytesValue.fromHexString(hex); } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/util/RLPTestUtil.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/util/RLPTestUtil.java new file mode 100644 index 0000000000..87355aea5f --- /dev/null +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/util/RLPTestUtil.java @@ -0,0 +1,162 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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. + */ +package tech.pegasys.pantheon.ethereum.rlp.util; + +import static java.lang.String.format; + +import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import tech.pegasys.pantheon.ethereum.rlp.RLP; +import tech.pegasys.pantheon.ethereum.rlp.RLPException; +import tech.pegasys.pantheon.ethereum.rlp.RLPInput; +import tech.pegasys.pantheon.ethereum.rlp.RLPOutput; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +public class RLPTestUtil { + + /** + * Recursively decodes an RLP encoded value. Byte strings are assumed to be non-scalar (leading + * zeros are allowed). + * + * @param value The RLP encoded value to decode. + * @return The output of decoding {@code value}. It will be either directly a {@link BytesValue}, + * or a list whose elements are either {@link BytesValue}, or similarly composed sub-lists. + * @throws RLPException if {@code value} is not a properly formed RLP encoding. + */ + public static Object decode(final BytesValue value) { + return decode(RLP.input(value)); + } + + private static Object decode(final RLPInput in) { + if (!in.nextIsList()) { + return in.readBytesValue(); + } + + final int size = in.enterList(); + final List l = new ArrayList<>(size); + for (int i = 0; i < size; i++) l.add(decode(in)); + in.leaveList(); + return l; + } + + /** + * Recursively RLP encode an object consisting of recursive lists of {@link BytesValue}. + * BytesValues are assumed to be non-scalar (leading zeros are not trimmed). + * + * @param obj An object that must be either directly a {@link BytesValue}, or a list whose + * elements are either {@link BytesValue}, or similarly composed sub-lists. + * @return The RLP encoding corresponding to {@code obj}. + * @throws IllegalArgumentException if {@code obj} is not a valid input (not entirely composed + * from lists and {@link BytesValue}). + */ + public static BytesValue encode(final Object obj) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + encode(obj, out); + return out.encoded(); + } + + private static void encode(final Object obj, final RLPOutput out) { + if (obj instanceof BytesValue) { + out.writeBytesValue((BytesValue) obj); + } else if (obj instanceof List) { + final List l = (List) obj; + out.startList(); + for (final Object o : l) encode(o, out); + out.endList(); + } else { + throw new IllegalArgumentException( + format("Invalid input type %s for RLP encoding", obj.getClass())); + } + } + + /** + * Generate a random rlp-encoded value. + * + * @param randomSeed Seed to use for random generation. + * @return a random rlp-encoded value + */ + public static BytesValueRLPOutput randomRLPValue(final int randomSeed) { + final Random random = new Random(randomSeed); + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + final AtomicInteger listDepth = new AtomicInteger(0); + int iterations = 0; + do { + if (iterations > 1000) { + out.endList(); + listDepth.decrementAndGet(); + continue; + } + iterations += 1; + + writeRandomRLPData(out, random, listDepth); + } while (listDepth.get() > 0); + + return out; + } + + private static void writeRandomRLPData( + final RLPOutput out, final Random random, final AtomicInteger listDepth) { + switch (random.nextInt(12)) { + case 0: + // Write empty byte string + out.writeBytesValue(BytesValue.EMPTY); + break; + case 1: + // Small single byte + out.writeByte((byte) random.nextInt(128)); + break; + case 2: + // Large single byte + byte value = (byte) (random.nextInt(128) + 128); + out.writeByte(value); + break; + case 3: + // Small byte string + int smallBytesSize = random.nextInt(54) + 2; + out.writeBytesValue(randomBytesValue(random, smallBytesSize)); + break; + case 4: + // Large byte string + int largeBytesSize = random.nextInt(500) + 56; + out.writeBytesValue(randomBytesValue(random, largeBytesSize)); + break; + case 5: + // Close list + if (listDepth.get() == 0) { + // If we're outside of a list try again + writeRandomRLPData(out, random, listDepth); + return; + } + out.endList(); + listDepth.decrementAndGet(); + break; + default: + // Start list + out.startList(); + listDepth.incrementAndGet(); + break; + } + } + + private static BytesValue randomBytesValue(final Random random, final int size) { + final byte[] bytes = new byte[size]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) random.nextInt(256); + } + return BytesValue.wrap(bytes); + } +}