Merge pull request #192 from atoulme/better_jsonrpc

Better handle exceptions for JSON-RPC
diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml
index 9daf560..2a90f10 100644
--- a/.github/workflows/assemble.yml
+++ b/.github/workflows/assemble.yml
@@ -41,12 +41,9 @@
           key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
           restore-keys: ${{ runner.os }}-m2
       - name: gradle assemble
-        uses: eskatos/gradle-command-action@v1
+        run: gradle assemble -x test -Psignatory.keyId=38F6C7215DD49C32 -Psigning.gnupg.keyName=38F6C7215DD49C32 -Psigning.gnupg.executable=gpg
         env:
           ENABLE_SIGNING: true
-        with:
-          gradle-version: 6.3
-          arguments: assemble -x test -Psignatory.keyId=38F6C7215DD49C32 -Psigning.gnupg.keyName=38F6C7215DD49C32 -Psigning.gnupg.executable=gpg
       - name: Upload source distrib
         uses: actions/upload-artifact@v2
         with:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index cc4af16..7a1a0f4 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -33,7 +33,4 @@
         with:
           submodules: true
       - name: gradle rat spotlessCheck
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: rat spotlessCheck
\ No newline at end of file
+        run: gradle rat spotlessCheck
\ No newline at end of file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 7a14e51..04a1170 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -45,10 +45,7 @@
           key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
           restore-keys: ${{ runner.os }}-m2
       - name: gradle setup
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: setup
+        run: gradle setup
       - name: gradle docs
         run: bash -c "./gradlew dokka | tee >( grep -i 'No documentation for' | grep -v DisconnectReason | grep -v RPCFlag | grep -v RPCRequestType | grep -v TomlVersion | grep -v PasswordHash | grep -v MessageSender | grep -v 'Identity.Curve' > docs_warning)"
 #      - name: Fail if warnings
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 0061578..83c176e 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -59,10 +59,7 @@
           key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
           restore-keys: ${{ runner.os }}-m2
       - name: gradle integrationTest
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: integrationTest jacocoTestReport
+        run: gradle integrationTest jacocoTestReport
       - name: Upload to Codecov
         uses: codecov/codecov-action@v1
         with:
diff --git a/.github/workflows/license-checks.yml b/.github/workflows/license-checks.yml
index 42349ce..bdb1e79 100644
--- a/.github/workflows/license-checks.yml
+++ b/.github/workflows/license-checks.yml
@@ -31,7 +31,4 @@
         with:
           submodules: true
       - name: gradle checkLicenses
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: checkLicenses
\ No newline at end of file
+        run: gradle checkLicenses
\ No newline at end of file
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 89581c2..824ff55 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -47,7 +47,4 @@
         with:
           java-version: 11
       - name: gradle test
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: test
\ No newline at end of file
+        run: gradle test
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2be5610..668b122 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,10 +45,7 @@
           key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
           restore-keys: ${{ runner.os }}-m2
       - name: gradle test
-        uses: eskatos/gradle-command-action@v1
-        with:
-          gradle-version: 6.3
-          arguments: test jacocoTestReport
+        run: gradle test jacocoTestReport
       - name: Upload to Codecov
         uses: codecov/codecov-action@v1
         with:
diff --git a/RELEASE.md b/RELEASE.md
index daa85e8..32be92a 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -65,7 +65,7 @@
 We're voting on the source distributions available here:
 https://dist.apache.org/repos/dist/dev/incubator/tuweni/${RELEASE VERSION}/
 The release tag is present here:
-https://github.com/apache/incubator-tuweni/releases/tag/v${RELEASE VERSION}
+https://github.com/apache/incubator-tuweni/releases/tag/v${RELEASE VERSION}-rc
 
 Please review and vote as appropriate.
 
diff --git a/bytes/build.gradle b/bytes/build.gradle
index ebf88cb..55515f9 100644
--- a/bytes/build.gradle
+++ b/bytes/build.gradle
@@ -14,6 +14,8 @@
 
 dependencies {
   implementation 'com.google.guava:guava'
+  implementation 'org.connid:framework'
+  implementation 'org.connid:framework-internal'
   compileOnly 'io.vertx:vertx-core'
 
   testImplementation 'io.vertx:vertx-core'
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
index 1f34aa2..596812f 100644
--- a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
@@ -89,6 +89,36 @@
   }
 
   /**
+   * Wrap the provided byte array as a {@link Bytes} value, encrypted in memory.
+   *
+   *
+   * @param value The value to secure.
+   * @return A {@link Bytes} value securing {@code value}.
+   */
+  static Bytes secure(byte[] value) {
+    return secure(value, 0, value.length);
+  }
+
+  /**
+   * Wrap a slice of a byte array as a {@link Bytes} value, encrypted in memory.
+   *
+   *
+   * @param value The value to secure.
+   * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
+   *        words, you will have {@code wrap(value, o, l).get(0) == value[o]}.
+   * @param length The length of the resulting value.
+   * @return A {@link Bytes} value that holds securely the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + length} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
+   */
+  static Bytes secure(byte[] value, int offset, int length) {
+    checkNotNull(value);
+    return new GuardedByteArrayBytes(value, offset, length);
+  }
+
+  /**
    * Wrap a list of other values into a concatenated view.
    *
    * <p>
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
index 688a702..931fe22 100644
--- a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
@@ -67,6 +67,36 @@
   }
 
   /**
+   * Secures the provided byte array, which must be of length 32, as a {@link Bytes32}.
+   *
+   * @param bytes The bytes to secure.
+   * @return A {@link Bytes32} securing {@code value}.
+   * @throws IllegalArgumentException if {@code value.length != 32}.
+   */
+  static Bytes32 secure(byte[] bytes) {
+    checkNotNull(bytes);
+    checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+    return secure(bytes, 0);
+  }
+
+  /**
+   * Secures a slice/sub-part of the provided array as a {@link Bytes32}.
+   *
+   * @param bytes The bytes to secure.
+   * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
+   *        words, you will have {@code wrap(value, i).get(0) == value[i]}.
+   * @return A {@link Bytes32} that holds securely the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 32} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > value.length}.
+   */
+  static Bytes32 secure(byte[] bytes, int offset) {
+    checkNotNull(bytes);
+    return new GuardedByteArrayBytes32(bytes, offset);
+  }
+
+  /**
    * Wrap a the provided value, which must be of size 32, as a {@link Bytes32}.
    *
    * <p>
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java
new file mode 100644
index 0000000..142ca1d
--- /dev/null
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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 org.apache.tuweni.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.vertx.core.buffer.Buffer;
+import org.identityconnectors.common.security.GuardedByteArray;
+
+class GuardedByteArrayBytes extends AbstractBytes {
+
+  protected final GuardedByteArray bytes;
+  protected final int offset;
+  protected final int length;
+
+  GuardedByteArrayBytes(byte[] bytes) {
+    this(bytes, 0, bytes.length);
+  }
+
+  GuardedByteArrayBytes(byte[] bytes, int offset, int length) {
+    checkArgument(length >= 0, "Invalid negative length");
+    if (bytes.length > 0) {
+      checkElementIndex(offset, bytes.length);
+    }
+    checkArgument(
+        offset + length <= bytes.length,
+        "Provided length %s is too big: the value has only %s bytes from offset %s",
+        length,
+        bytes.length - offset,
+        offset);
+
+    this.bytes = new GuardedByteArray(bytes);
+    this.bytes.makeReadOnly();
+    this.offset = offset;
+    this.length = length;
+  }
+
+  @Override
+  public int size() {
+    return length;
+  }
+
+  @Override
+  public byte get(int i) {
+    // Check bounds because while the array access would throw, the error message would be confusing
+    // for the caller.
+    checkElementIndex(i, size());
+    AtomicReference<Byte> b = new AtomicReference<>();
+    bytes.access(bytes -> b.set(bytes[offset + i]));
+    return b.get();
+  }
+
+  @Override
+  public Bytes slice(int i, int length) {
+    if (i == 0 && length == this.length) {
+      return this;
+    }
+    if (length == 0) {
+      return Bytes.EMPTY;
+    }
+
+    checkElementIndex(i, this.length);
+    checkArgument(
+        i + length <= this.length,
+        "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+        length,
+        this.length,
+        this.length - i,
+        i);
+    AtomicReference<byte[]> clearBytes = new AtomicReference<>();
+    bytes.access(data -> {
+      byte[] result = new byte[length];
+      System.arraycopy(data, offset + i, result, 0, length);
+      clearBytes.set(result);
+    });
+
+    return length == Bytes32.SIZE ? new ArrayWrappingBytes32(clearBytes.get())
+        : new ArrayWrappingBytes(clearBytes.get(), 0, length);
+  }
+
+  // MUST be overridden by mutable implementations
+  @Override
+  public Bytes copy() {
+    return new ArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return new MutableArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    digest.update(toArray(), offset, length);
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    if (!(destination instanceof MutableArrayWrappingBytes)) {
+      super.copyTo(destination, destinationOffset);
+      return;
+    }
+
+    int size = size();
+    if (size == 0) {
+      return;
+    }
+
+    checkElementIndex(destinationOffset, destination.size());
+    checkArgument(
+        destination.size() - destinationOffset >= size,
+        "Cannot copy %s bytes, destination has only %s bytes from index %s",
+        size,
+        destination.size() - destinationOffset,
+        destinationOffset);
+
+    MutableArrayWrappingBytes d = (MutableArrayWrappingBytes) destination;
+    System.arraycopy(toArray(), offset, d.bytes, d.offset + destinationOffset, size);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    byteBuffer.put(toArray(), offset, length);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    buffer.appendBytes(toArray(), offset, length);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof GuardedByteArrayBytes)) {
+      return super.equals(obj);
+    }
+    GuardedByteArrayBytes other = (GuardedByteArrayBytes) obj;
+    if (length != other.length) {
+      return false;
+    }
+    for (int i = 0; i < length; ++i) {
+      if (get(offset + i) != other.get(other.offset + i)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = 1;
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      result = 31 * result + get(offset + i);
+    }
+    return result;
+  }
+
+  @Override
+  public byte[] toArray() {
+    AtomicReference<byte[]> clearBytes = new AtomicReference<>();
+    bytes.access(data -> {
+      byte[] result = new byte[length];
+      System.arraycopy(data, offset, result, 0, length);
+      clearBytes.set(result);
+    });
+    return clearBytes.get();
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    return toArray();
+  }
+}
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java
new file mode 100644
index 0000000..2b7a385
--- /dev/null
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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 org.apache.tuweni.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class GuardedByteArrayBytes32 extends GuardedByteArrayBytes implements Bytes32 {
+
+  GuardedByteArrayBytes32(byte[] bytes) {
+    this(checkLength(bytes), 0);
+  }
+
+  GuardedByteArrayBytes32(byte[] bytes, int offset) {
+    super(checkLength(bytes, offset), offset, SIZE);
+  }
+
+  // Ensures a proper error message.
+  private static byte[] checkLength(byte[] bytes) {
+    checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+    return bytes;
+  }
+
+  // Ensures a proper error message.
+  private static byte[] checkLength(byte[] bytes, int offset) {
+    checkArgument(
+        bytes.length - offset >= SIZE,
+        "Expected at least %s bytes from offset %s but got only %s",
+        SIZE,
+        offset,
+        bytes.length - offset);
+    return bytes;
+  }
+
+  @Override
+  public Bytes32 copy() {
+    return new ArrayWrappingBytes32(toArray());
+  }
+
+  @Override
+  public MutableBytes32 mutableCopy() {
+    return new MutableArrayWrappingBytes32(toArray());
+  }
+}
diff --git a/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java b/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java
new file mode 100644
index 0000000..e6cb720
--- /dev/null
+++ b/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java
@@ -0,0 +1,666 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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 org.apache.tuweni.bytes;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class GuardedBytesTest extends CommonBytesTests {
+
+  @Override
+  Bytes h(String hex) {
+    return Bytes.fromHexString(hex);
+  }
+
+  @Override
+  MutableBytes m(int size) {
+    return MutableBytes.create(size);
+  }
+
+  @Override
+  Bytes w(byte[] bytes) {
+    return Bytes.secure(bytes);
+  }
+
+  @Override
+  Bytes of(int... bytes) {
+    return Bytes.of(bytes);
+  }
+
+  @Test
+  void wrapEmpty() {
+    Bytes wrap = Bytes.wrap(new byte[0]);
+    assertEquals(Bytes.EMPTY, wrap);
+  }
+
+  @ParameterizedTest
+  @MethodSource("wrapProvider")
+  void wrap(Object arr) {
+    byte[] bytes = (byte[]) arr;
+    Bytes value = Bytes.wrap(bytes);
+    assertEquals(bytes.length, value.size());
+    assertArrayEquals(value.toArray(), bytes);
+  }
+
+  @SuppressWarnings("UnusedMethod")
+  private static Stream<Arguments> wrapProvider() {
+    return Stream
+        .of(
+            Arguments.of(new Object[] {new byte[10]}),
+            Arguments.of(new Object[] {new byte[] {1}}),
+            Arguments.of(new Object[] {new byte[] {1, 2, 3, 4}}),
+            Arguments.of(new Object[] {new byte[] {-1, 127, -128}}));
+  }
+
+  @Test
+  void wrapNull() {
+    assertThrows(NullPointerException.class, () -> Bytes.wrap((byte[]) null));
+  }
+
+  /**
+   * Checks that modifying a wrapped array modifies the value itself.
+   */
+  @Test
+  void wrapReflectsUpdates() {
+    byte[] bytes = new byte[] {1, 2, 3, 4, 5};
+    Bytes value = Bytes.wrap(bytes);
+
+    assertEquals(bytes.length, value.size());
+    assertArrayEquals(value.toArray(), bytes);
+
+    bytes[1] = 127;
+    bytes[3] = 127;
+
+    assertEquals(bytes.length, value.size());
+    assertArrayEquals(value.toArray(), bytes);
+  }
+
+  @Test
+  void wrapSliceEmpty() {
+    assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[0], 0, 0));
+    assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 0, 0));
+    assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 2, 0));
+  }
+
+  @ParameterizedTest
+  @MethodSource("wrapSliceProvider")
+  void wrapSlice(Object arr, int offset, int length) {
+    assertWrapSlice((byte[]) arr, offset, length);
+  }
+
+  @SuppressWarnings("UnusedMethod")
+  private static Stream<Arguments> wrapSliceProvider() {
+    return Stream
+        .of(
+            Arguments.of(new byte[] {1, 2, 3, 4}, 0, 4),
+            Arguments.of(new byte[] {1, 2, 3, 4}, 0, 2),
+            Arguments.of(new byte[] {1, 2, 3, 4}, 2, 1),
+            Arguments.of(new byte[] {1, 2, 3, 4}, 2, 2));
+  }
+
+  private void assertWrapSlice(byte[] bytes, int offset, int length) {
+    Bytes value = Bytes.wrap(bytes, offset, length);
+    assertEquals(length, value.size());
+    assertArrayEquals(value.toArray(), Arrays.copyOfRange(bytes, offset, offset + length));
+  }
+
+  @Test
+  void wrapSliceNull() {
+    assertThrows(NullPointerException.class, () -> Bytes.wrap(null, 0, 2));
+  }
+
+  @Test
+  void wrapSliceNegativeOffset() {
+    assertThrows(IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, -1, 4));
+  }
+
+  @Test
+  void wrapSliceOutOfBoundOffset() {
+    assertThrows(IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 5, 1));
+  }
+
+  @Test
+  void wrapSliceNegativeLength() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, -2));
+    assertEquals("Invalid negative length", exception.getMessage());
+  }
+
+  @Test
+  void wrapSliceTooBig() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 3));
+    assertEquals("Provided length 3 is too big: the value has only 2 bytes from offset 2", exception.getMessage());
+  }
+
+  /**
+   * Checks that modifying a wrapped array modifies the value itself, but only if within the wrapped slice.
+   */
+  @Test
+  void wrapSliceReflectsUpdates() {
+    byte[] bytes = new byte[] {1, 2, 3, 4, 5};
+    assertWrapSlice(bytes, 2, 2);
+    bytes[2] = 127;
+    bytes[3] = 127;
+    assertWrapSlice(bytes, 2, 2);
+
+    Bytes wrapped = Bytes.wrap(bytes, 2, 2);
+    Bytes copy = wrapped.copy();
+
+    // Modify the bytes outside of the wrapped slice and check this doesn't affect the value (that
+    // it is still equal to the copy from before the updates)
+    bytes[0] = 127;
+    assertEquals(copy, wrapped);
+
+    // Sanity check for copy(): modify within the wrapped slice and check the copy differs now.
+    bytes[2] = 42;
+    assertNotEquals(copy, wrapped);
+  }
+
+  @Test
+  void ofBytes() {
+    assertArrayEquals(Bytes.of().toArray(), new byte[] {});
+    assertArrayEquals(Bytes.of((byte) 1, (byte) 2).toArray(), new byte[] {1, 2});
+    assertArrayEquals(Bytes.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5).toArray(), new byte[] {1, 2, 3, 4, 5});
+    assertArrayEquals(Bytes.of((byte) -1, (byte) 2, (byte) -3).toArray(), new byte[] {-1, 2, -3});
+  }
+
+  @Test
+  void ofInts() {
+    assertArrayEquals(Bytes.of(1, 2).toArray(), new byte[] {1, 2});
+    assertArrayEquals(Bytes.of(1, 2, 3, 4, 5).toArray(), new byte[] {1, 2, 3, 4, 5});
+    assertArrayEquals(Bytes.of(0xff, 0x7f, 0x80).toArray(), new byte[] {-1, 127, -128});
+  }
+
+  @Test
+  void ofIntsTooBig() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, 3, 256));
+    assertEquals("3th value 256 does not fit a byte", exception.getMessage());
+  }
+
+  @Test
+  void ofIntsTooLow() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, -1, 3));
+    assertEquals("2th value -1 does not fit a byte", exception.getMessage());
+  }
+
+  @Test
+  void minimalBytes() {
+    assertEquals(h("0x"), Bytes.minimalBytes(0));
+    assertEquals(h("0x01"), Bytes.minimalBytes(1));
+    assertEquals(h("0x04"), Bytes.minimalBytes(4));
+    assertEquals(h("0x10"), Bytes.minimalBytes(16));
+    assertEquals(h("0xFF"), Bytes.minimalBytes(255));
+    assertEquals(h("0x0100"), Bytes.minimalBytes(256));
+    assertEquals(h("0x0200"), Bytes.minimalBytes(512));
+    assertEquals(h("0x010000"), Bytes.minimalBytes(1L << 16));
+    assertEquals(h("0x01000000"), Bytes.minimalBytes(1L << 24));
+    assertEquals(h("0x0100000000"), Bytes.minimalBytes(1L << 32));
+    assertEquals(h("0x010000000000"), Bytes.minimalBytes(1L << 40));
+    assertEquals(h("0x01000000000000"), Bytes.minimalBytes(1L << 48));
+    assertEquals(h("0x0100000000000000"), Bytes.minimalBytes(1L << 56));
+    assertEquals(h("0xFFFFFFFFFFFFFFFF"), Bytes.minimalBytes(-1L));
+  }
+
+  @Test
+  void ofUnsignedShort() {
+    assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0));
+    assertEquals(h("0x0001"), Bytes.ofUnsignedShort(1));
+    assertEquals(h("0x0100"), Bytes.ofUnsignedShort(256));
+    assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535));
+  }
+
+  @Test
+  void ofUnsignedShortLittleEndian() {
+    assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0, LITTLE_ENDIAN));
+    assertEquals(h("0x0100"), Bytes.ofUnsignedShort(1, LITTLE_ENDIAN));
+    assertEquals(h("0x0001"), Bytes.ofUnsignedShort(256, LITTLE_ENDIAN));
+    assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535, LITTLE_ENDIAN));
+  }
+
+  @Test
+  void ofUnsignedShortNegative() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(-1));
+    assertEquals(
+        "Value -1 cannot be represented as an unsigned short (it is negative or too big)",
+        exception.getMessage());
+  }
+
+  @Test
+  void ofUnsignedShortTooBig() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(65536));
+    assertEquals(
+        "Value 65536 cannot be represented as an unsigned short (it is negative or too big)",
+        exception.getMessage());
+  }
+
+  @Test
+  void asUnsignedBigIntegerConstants() {
+    assertEquals(bi("0"), Bytes.EMPTY.toUnsignedBigInteger());
+    assertEquals(bi("1"), Bytes.of(1).toUnsignedBigInteger());
+  }
+
+  @Test
+  void asSignedBigIntegerConstants() {
+    assertEquals(bi("0"), Bytes.EMPTY.toBigInteger());
+    assertEquals(bi("1"), Bytes.of(1).toBigInteger());
+  }
+
+  @Test
+  void fromHexStringLenient() {
+    assertEquals(Bytes.of(), Bytes.fromHexStringLenient(""));
+    assertEquals(Bytes.of(), Bytes.fromHexStringLenient("0x"));
+    assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0"));
+    assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x0"));
+    assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("00"));
+    assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x00"));
+    assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x1"));
+    assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x01"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A"));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A"));
+  }
+
+  @Test
+  void compareTo() {
+    assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01)));
+    assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01)));
+    assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x01)));
+    assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x00, 0x01)));
+    assertEquals(1, Bytes.of(0x00, 0x00, 0xef).compareTo(Bytes.of(0x00, 0x01)));
+    assertEquals(1, Bytes.of(0x00, 0xef).compareTo(Bytes.of(0x00, 0x00, 0x01)));
+    assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xff)));
+    assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0x01)));
+    assertEquals(1, Bytes.of(0xef, 0xf1).compareTo(Bytes.of(0xef, 0xf0)));
+    assertEquals(1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x00)));
+    assertEquals(0, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf0)));
+    assertEquals(-1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf5)));
+    assertEquals(-1, Bytes.of(0xef).compareTo(Bytes.of(0xff)));
+    assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0xff)));
+    assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0x01, 0xff)));
+    assertEquals(-1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x02)));
+    assertEquals(-1, Bytes.of(0x00, 0x01).compareTo(Bytes.of(0x00, 0x00, 0x05)));
+  }
+
+  @Test
+  void fromHexStringLenientInvalidInput() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo"));
+    assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringLenientLeftPadding() {
+    assertEquals(Bytes.of(), Bytes.fromHexStringLenient("", 0));
+    assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("", 1));
+    assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("", 2));
+    assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("0x", 2));
+    assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0", 3));
+    assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x0", 3));
+    assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("00", 3));
+    assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x00", 3));
+    assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x1", 3));
+    assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x01", 3));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A", 3));
+    assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A", 4));
+    assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a", 5));
+    assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a", 4));
+    assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A", 4));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A", 3));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A", 3));
+  }
+
+  @Test
+  void fromHexStringLenientLeftPaddingInvalidInput() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo", 10));
+    assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringLenientLeftPaddingInvalidSize() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2));
+    assertEquals("Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage());
+  }
+
+  @Test
+  void fromHexString() {
+    assertEquals(Bytes.of(), Bytes.fromHexString("0x"));
+    assertEquals(Bytes.of(0), Bytes.fromHexString("00"));
+    assertEquals(Bytes.of(0), Bytes.fromHexString("0x00"));
+    assertEquals(Bytes.of(1), Bytes.fromHexString("0x01"));
+    assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("01FF2A"));
+    assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A"));
+    assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a"));
+    assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a"));
+  }
+
+  @Test
+  void fromHexStringInvalidInput() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo"));
+    assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringNotLenient() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100"));
+    assertEquals("Invalid odd-length hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringLeftPadding() {
+    assertEquals(Bytes.of(), Bytes.fromHexString("0x", 0));
+    assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x", 2));
+    assertEquals(Bytes.of(0, 0, 0, 0), Bytes.fromHexString("0x", 4));
+    assertEquals(Bytes.of(0, 0), Bytes.fromHexString("00", 2));
+    assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x00", 2));
+    assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexString("0x01", 3));
+    assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("01FF2A", 4));
+    assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A", 3));
+    assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a", 5));
+    assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a", 5));
+  }
+
+  @Test
+  void fromHexStringLeftPaddingInvalidInput() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo", 4));
+    assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringLeftPaddingNotLenient() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100", 4));
+    assertEquals("Invalid odd-length hex binary representation", exception.getMessage());
+  }
+
+  @Test
+  void fromHexStringLeftPaddingInvalidSize() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2));
+    assertEquals("Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage());
+  }
+
+  @Test
+  void fromBase64Roundtrip() {
+    Bytes value = Bytes.fromBase64String("deadbeefISDAbest");
+    assertEquals("deadbeefISDAbest", value.toBase64String());
+  }
+
+  @Test
+  void littleEndianRoundtrip() {
+    int val = Integer.MAX_VALUE - 5;
+    Bytes littleEndianEncoded = Bytes.ofUnsignedInt(val, LITTLE_ENDIAN);
+    assertEquals(4, littleEndianEncoded.size());
+    Bytes bigEndianEncoded = Bytes.ofUnsignedInt(val);
+    assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(3));
+    assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(2));
+    assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(1));
+    assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(0));
+
+    int read = littleEndianEncoded.toInt(LITTLE_ENDIAN);
+    assertEquals(val, read);
+  }
+
+  @Test
+  void littleEndianLongRoundtrip() {
+    long val = 1L << 46;
+    Bytes littleEndianEncoded = Bytes.ofUnsignedLong(val, LITTLE_ENDIAN);
+    assertEquals(8, littleEndianEncoded.size());
+    Bytes bigEndianEncoded = Bytes.ofUnsignedLong(val);
+    assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(7));
+    assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(6));
+    assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(5));
+    assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(4));
+    assertEquals(bigEndianEncoded.get(4), littleEndianEncoded.get(3));
+    assertEquals(bigEndianEncoded.get(5), littleEndianEncoded.get(2));
+    assertEquals(bigEndianEncoded.get(6), littleEndianEncoded.get(1));
+    assertEquals(bigEndianEncoded.get(7), littleEndianEncoded.get(0));
+
+    long read = littleEndianEncoded.toLong(LITTLE_ENDIAN);
+    assertEquals(val, read);
+  }
+
+  @Test
+  void reverseBytes() {
+    Bytes bytes = Bytes.fromHexString("0x000102030405");
+    assertEquals(Bytes.fromHexString("0x050403020100"), bytes.reverse());
+  }
+
+  @Test
+  void reverseBytesEmptyArray() {
+    Bytes bytes = Bytes.fromHexString("0x");
+    assertEquals(Bytes.fromHexString("0x"), bytes.reverse());
+  }
+
+  @Test
+  void mutableBytesIncrement() {
+    MutableBytes one = MutableBytes.of(1);
+    one.increment();
+    assertEquals(Bytes.of(2), one);
+  }
+
+  @Test
+  void mutableBytesIncrementMax() {
+    MutableBytes maxed = MutableBytes.of(1, 0xFF);
+    maxed.increment();
+    assertEquals(Bytes.of(2, 0), maxed);
+  }
+
+  @Test
+  void mutableBytesIncrementOverflow() {
+    MutableBytes maxed = MutableBytes.of(0xFF, 0xFF, 0xFF);
+    maxed.increment();
+    assertEquals(Bytes.of(0, 0, 0), maxed);
+  }
+
+  @Test
+  void mutableBytesDecrement() {
+    MutableBytes one = MutableBytes.of(2);
+    one.decrement();
+    assertEquals(Bytes.of(1), one);
+  }
+
+  @Test
+  void mutableBytesDecrementMax() {
+    MutableBytes maxed = MutableBytes.of(1, 0);
+    maxed.decrement();
+    assertEquals(Bytes.of(0, 0xFF), maxed);
+  }
+
+  @Test
+  void mutableBytesDecrementOverflow() {
+    MutableBytes maxed = MutableBytes.of(0x00, 0x00, 0x00);
+    maxed.decrement();
+    assertEquals(Bytes.of(0xFF, 0xFF, 0xFF), maxed);
+  }
+
+  @Test
+  void concatenation() {
+    MutableBytes value1 = MutableBytes.wrap(Bytes.fromHexString("deadbeef").toArrayUnsafe());
+    Bytes result = Bytes.concatenate(value1, value1);
+    assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+    value1.set(0, (byte) 0);
+    assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+  }
+
+  @Test
+  void wrap() {
+    MutableBytes value1 = MutableBytes.wrap(Bytes.fromHexString("deadbeef").toArrayUnsafe());
+    Bytes result = Bytes.wrap(value1, value1);
+    assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+    value1.set(0, (byte) 0);
+    assertEquals(Bytes.fromHexString("0x00adbeef00adbeef"), result);
+  }
+
+  @Test
+  void random() {
+    Bytes value = Bytes.random(20);
+    assertNotEquals(value, Bytes.random(20));
+    assertEquals(20, value.size());
+  }
+
+  @Test
+  void getInt() {
+    Bytes value = Bytes.fromHexString("0x00000001");
+    assertEquals(1, value.getInt(0));
+    assertEquals(16777216, value.getInt(0, LITTLE_ENDIAN));
+    assertEquals(1, value.toInt());
+    assertEquals(16777216, value.toInt(LITTLE_ENDIAN));
+  }
+
+  @Test
+  void getLong() {
+    Bytes value = Bytes.fromHexString("0x0000000000000001");
+    assertEquals(1, value.getLong(0));
+    assertEquals(72057594037927936L, value.getLong(0, LITTLE_ENDIAN));
+    assertEquals(1, value.toLong());
+    assertEquals(72057594037927936L, value.toLong(LITTLE_ENDIAN));
+  }
+
+  @Test
+  void numberOfLeadingZeros() {
+    Bytes value = Bytes.fromHexString("0x00000001");
+    assertEquals(31, value.numberOfLeadingZeros());
+  }
+
+  @Test
+  void and() {
+    Bytes value = Bytes.fromHexString("0x01000001").and(Bytes.fromHexString("0x01000000"));
+    assertEquals(Bytes.fromHexString("0x01000000"), value);
+  }
+
+  @Test
+  void andResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").and(Bytes.fromHexString("0x01000000"), result);
+    assertEquals(Bytes.fromHexString("0x01000000"), result);
+  }
+
+  @Test
+  void or() {
+    Bytes value = Bytes.fromHexString("0x01000001").or(Bytes.fromHexString("0x01000000"));
+    assertEquals(Bytes.fromHexString("0x01000001"), value);
+  }
+
+  @Test
+  void orResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").or(Bytes.fromHexString("0x01000000"), result);
+    assertEquals(Bytes.fromHexString("0x01000001"), result);
+  }
+
+  @Test
+  void xor() {
+    Bytes value = Bytes.fromHexString("0x01000001").xor(Bytes.fromHexString("0x01000000"));
+    assertEquals(Bytes.fromHexString("0x00000001"), value);
+  }
+
+  @Test
+  void xorResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").xor(Bytes.fromHexString("0x01000000"), result);
+    assertEquals(Bytes.fromHexString("0x00000001"), result);
+  }
+
+  @Test
+  void not() {
+    Bytes value = Bytes.fromHexString("0x01000001").not();
+    assertEquals(Bytes.fromHexString("0xfefffffe"), value);
+  }
+
+  @Test
+  void notResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").not(result);
+    assertEquals(Bytes.fromHexString("0xfefffffe"), result);
+  }
+
+  @Test
+  void shiftRight() {
+    Bytes value = Bytes.fromHexString("0x01000001").shiftRight(2);
+    assertEquals(Bytes.fromHexString("0x00400000"), value);
+  }
+
+  @Test
+  void shiftRightResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").shiftRight(2, result);
+    assertEquals(Bytes.fromHexString("0x00400000"), result);
+  }
+
+  @Test
+  void shiftLeft() {
+    Bytes value = Bytes.fromHexString("0x01000001").shiftLeft(2);
+    assertEquals(Bytes.fromHexString("0x04000004"), value);
+  }
+
+  @Test
+  void shiftLeftResult() {
+    MutableBytes result = MutableBytes.create(4);
+    Bytes.fromHexString("0x01000001").shiftLeft(2, result);
+    assertEquals(Bytes.fromHexString("0x04000004"), result);
+  }
+
+  @Test
+  void commonPrefix() {
+    Bytes value = Bytes.fromHexString("0x01234567");
+    Bytes value2 = Bytes.fromHexString("0x01236789");
+    assertEquals(2, value.commonPrefixLength(value2));
+    assertEquals(Bytes.fromHexString("0x0123"), value.commonPrefix(value2));
+  }
+
+  @Test
+  void testWrapByteBufEmpty() {
+    ByteBuf buffer = Unpooled.buffer(0);
+    assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer));
+  }
+
+  @Test
+  void testWrapByteBufWithIndexEmpty() {
+    ByteBuf buffer = Unpooled.buffer(3);
+    assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer, 3, 0));
+  }
+
+  @Test
+  void testWrapByteBufSizeWithOffset() {
+    ByteBuf buffer = Unpooled.buffer(10);
+    assertEquals(1, Bytes.wrapByteBuf(buffer, 1, 1).size());
+  }
+
+  @Test
+  void testWrapByteBufSize() {
+    ByteBuf buffer = Unpooled.buffer(20);
+    assertEquals(20, Bytes.wrapByteBuf(buffer).size());
+  }
+
+  @Test
+  void testWrapByteBufReadableBytes() {
+    ByteBuf buffer = Unpooled.buffer(20).writeByte(3);
+    assertEquals(1, Bytes.wrapByteBuf(buffer, 0, buffer.readableBytes()).size());
+  }
+}
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java b/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
index 13265da..b25ddc3 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
@@ -16,6 +16,8 @@
 
 import org.apache.tuweni.bytes.Bytes;
 import org.apache.tuweni.bytes.Bytes32;
+import org.apache.tuweni.crypto.sodium.SHA256Hash;
+import org.apache.tuweni.crypto.sodium.Sodium;
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -27,6 +29,8 @@
  * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation for detail.
  */
 public final class Hash {
+  static boolean USE_SODIUM = true;
+
   private Hash() {}
 
   // SHA-2
@@ -79,6 +83,19 @@
    * @return A digest.
    */
   public static byte[] sha2_256(byte[] input) {
+    if (USE_SODIUM && Sodium.isAvailable()) {
+      SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input);
+      try {
+        SHA256Hash.Hash result = SHA256Hash.hash(shaInput);
+        try {
+          return SHA256Hash.hash(shaInput).bytesArray();
+        } finally {
+          result.destroy();
+        }
+      } finally {
+        shaInput.destroy();
+      }
+    }
     try {
       return digestUsingAlgorithm(input, SHA2_256);
     } catch (NoSuchAlgorithmException e) {
@@ -93,6 +110,19 @@
    * @return A digest.
    */
   public static Bytes32 sha2_256(Bytes input) {
+    if (USE_SODIUM && Sodium.isAvailable()) {
+      SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input);
+      try {
+        SHA256Hash.Hash result = SHA256Hash.hash(shaInput);
+        try {
+          return (Bytes32) SHA256Hash.hash(shaInput).bytes();
+        } finally {
+          result.destroy();
+        }
+      } finally {
+        shaInput.destroy();
+      }
+    }
     try {
       return (Bytes32) digestUsingAlgorithm(input, SHA2_256);
     } catch (NoSuchAlgorithmException e) {
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
index 81a3f80..35a245f 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
@@ -404,7 +404,7 @@
   long crypto_hash_sha512_bytes();
 
   // int crypto_hash_sha512(unsigned char * out, const unsigned char * in, unsigned long long inlen);
-  int crypto_hash_sha512(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen);
+  int crypto_hash_sha512(@Out Pointer out, @In Pointer in, @In @u_int64_t long inlen);
 
   // int crypto_hash_sha512_init(crypto_hash_sha512_state * state);
   int crypto_hash_sha512_init(@Out Pointer state);
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java
new file mode 100644
index 0000000..1387fe0
--- /dev/null
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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 org.apache.tuweni.crypto.sodium;
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.util.Objects;
+import javax.security.auth.Destroyable;
+
+import jnr.ffi.Pointer;
+
+/**
+ * SHA-512 hashing.
+ *
+ * The SHA-256 and SHA-512 functions are provided for interoperability with other applications. If you are looking for a
+ * generic hash function and not specifically SHA-2, using crypto_generichash() (BLAKE2b) might be a better choice.
+ * <p>
+ * These functions are also not suitable for hashing passwords or deriving keys from passwords. Use one of the password
+ * hashing APIs instead.
+ * <p>
+ * These functions are not keyed and are thus deterministic. In addition, the untruncated versions are vulnerable to
+ * length extension attacks.
+ * <p>
+ *
+ * @see <a href="https://libsodium.gitbook.io/doc/advanced/sha-2_hash_function">SHA-2</a>
+ */
+public class SHA512Hash {
+
+  /**
+   * Input of a SHA-512 hash function
+   */
+  public static final class Input implements Destroyable {
+    /**
+     * Create a hash input from a Diffie-Helman secret
+     *
+     * @param secret a Diffie-Helman secret
+     * @return a hash input
+     */
+    public static SHA512Hash.Input fromSecret(DiffieHelman.Secret secret) {
+      return new SHA512Hash.Input(
+          Sodium.dup(secret.value.pointer(), DiffieHelman.Secret.length()),
+          DiffieHelman.Secret.length());
+    }
+
+    /**
+     * Create a {@link SHA512Hash.Input} from a pointer.
+     *
+     * @param allocated the allocated pointer
+     * @return An input.
+     */
+    public static SHA512Hash.Input fromPointer(Allocated allocated) {
+      return new SHA512Hash.Input(Sodium.dup(allocated.pointer(), allocated.length()), allocated.length());
+    }
+
+    /**
+     * Create a {@link SHA512Hash.Input} from a hash.
+     *
+     * @param hash the hash
+     * @return An input.
+     */
+    public static SHA512Hash.Input fromHash(SHA512Hash.Hash hash) {
+      return new SHA512Hash.Input(Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length());
+    }
+
+    /**
+     * Create a {@link SHA512Hash.Input} from an array of bytes.
+     *
+     * @param bytes The bytes for the input.
+     * @return An input.
+     */
+    public static SHA512Hash.Input fromBytes(Bytes bytes) {
+      return fromBytes(bytes.toArrayUnsafe());
+    }
+
+    /**
+     * Create a {@link SHA512Hash.Input} from an array of bytes.
+     *
+     * @param bytes The bytes for the input.
+     * @return An input.
+     */
+    public static SHA512Hash.Input fromBytes(byte[] bytes) {
+      return Sodium.dup(bytes, SHA512Hash.Input::new);
+    }
+
+    private final Allocated value;
+
+    private Input(Pointer ptr, int length) {
+      this.value = new Allocated(ptr, length);
+    }
+
+    @Override
+    public void destroy() {
+      value.destroy();
+    }
+
+    @Override
+    public boolean isDestroyed() {
+      return value.isDestroyed();
+    }
+
+    /**
+     * Provides the length of the input
+     * 
+     * @return the length of the input
+     */
+    public int length() {
+      return value.length();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof SHA512Hash.Input)) {
+        return false;
+      }
+      SHA512Hash.Input other = (SHA512Hash.Input) obj;
+      return other.value.equals(value);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    /**
+     * Provides the bytes of this key
+     * 
+     * @return The bytes of this key.
+     */
+    public Bytes bytes() {
+      return value.bytes();
+    }
+
+    /**
+     * Provides the bytes of this key
+     * 
+     * @return The bytes of this key.
+     */
+    public byte[] bytesArray() {
+      return value.bytesArray();
+    }
+  }
+
+  /**
+   * SHA-512 hash output
+   */
+  public static final class Hash implements Destroyable {
+    Allocated value;
+
+    Hash(Pointer ptr, int length) {
+      this.value = new Allocated(ptr, length);
+    }
+
+
+    @Override
+    public void destroy() {
+      value.destroy();
+    }
+
+    @Override
+    public boolean isDestroyed() {
+      return value.isDestroyed();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof SHA512Hash.Hash)) {
+        return false;
+      }
+      SHA512Hash.Hash other = (SHA512Hash.Hash) obj;
+      return other.value.equals(value);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    /**
+     * Obtain the bytes of this hash.
+     *
+     * WARNING: This will cause the hash to be copied into heap memory.
+     *
+     * @return The bytes of this hash.
+     */
+    public Bytes bytes() {
+      return value.bytes();
+    }
+
+    /**
+     * Obtain the bytes of this hash.
+     *
+     * WARNING: This will cause the hash to be copied into heap memory. The returned array should be overwritten when no
+     * longer required.
+     *
+     * @return The bytes of this hash.
+     */
+    public byte[] bytesArray() {
+      return value.bytesArray();
+    }
+
+    /**
+     * Obtain the length of the hash in bytes (32).
+     *
+     * @return The length of the hash in bytes (32).
+     */
+    public static int length() {
+      long hashbytes = Sodium.crypto_hash_sha512_bytes();
+      if (hashbytes > Integer.MAX_VALUE) {
+        throw new SodiumException("crypto_hash_sha512_bytes: " + hashbytes + " is too large");
+      }
+      return (int) hashbytes;
+    }
+  }
+
+  /**
+   * Hashes input to a SHA-512 hash
+   * 
+   * @param input the input of the hash function
+   * @return a SHA-512 hash of the input
+   */
+  public static SHA512Hash.Hash hash(SHA512Hash.Input input) {
+    Pointer output = Sodium.malloc(SHA512Hash.Hash.length());
+    Sodium.crypto_hash_sha512(output, input.value.pointer(), input.length());
+    return new SHA512Hash.Hash(output, SHA512Hash.Hash.length());
+  }
+}
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
index be9d1bd..56f8f6b 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
@@ -771,7 +771,7 @@
     return libSodium().crypto_hash_sha512_bytes();
   }
 
-  static int crypto_hash_sha512(byte[] out, byte[] in, long inlen) {
+  static int crypto_hash_sha512(Pointer out, Pointer in, long inlen) {
     return libSodium().crypto_hash_sha512(out, in, inlen);
   }
 
diff --git a/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java b/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
index 178d319..32503a7 100644
--- a/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
+++ b/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
@@ -48,6 +48,29 @@
   }
 
   @Test
+  void sha2_256_withoutSodium() {
+    Hash.USE_SODIUM = false;
+    try {
+      String horseSha2 = "fd62862b6dc213bee77c2badd6311528253c6cb3107e03c16051aa15584eca1c";
+      String cowSha2 = "beb134754910a4b4790c69ab17d3975221f4c534b70c8d6e82b30c165e8c0c09";
+
+      Bytes resultHorse = Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8)));
+      assertEquals(Bytes.fromHexString(horseSha2), resultHorse);
+
+      byte[] resultHorse2 = Hash.sha2_256("horse".getBytes(UTF_8));
+      assertArrayEquals(Bytes.fromHexString(horseSha2).toArray(), resultHorse2);
+
+      Bytes resultCow = Hash.sha2_256(Bytes.wrap("cow".getBytes(UTF_8)));
+      assertEquals(Bytes.fromHexString(cowSha2), resultCow);
+
+      byte[] resultCow2 = Hash.sha2_256("cow".getBytes(UTF_8));
+      assertArrayEquals(Bytes.fromHexString(cowSha2).toArray(), resultCow2);
+    } finally {
+      Hash.USE_SODIUM = true;
+    }
+  }
+
+  @Test
   void sha2_512_256() {
     String horseSha2 = "6d64886cd066b81cf2dcf16ae70e97017d35f2f4ab73c5c5810aaa9ab573dab3";
     String cowSha2 = "7d26bad15e2f266cb4cbe9b1913978cb8a8bd08d92ee157b6be87c92dfce2d3e";
@@ -125,6 +148,7 @@
   void testWithoutProviders() {
     Provider[] providers = Security.getProviders();
     Stream.of(Security.getProviders()).map(Provider::getName).forEach(Security::removeProvider);
+    Hash.USE_SODIUM = false;
     try {
       assertThrows(IllegalStateException.class, () -> Hash.sha2_256("horse".getBytes(UTF_8)));
       assertThrows(IllegalStateException.class, () -> Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8))));
@@ -140,6 +164,7 @@
       for (Provider p : providers) {
         Security.addProvider(p);
       }
+      Hash.USE_SODIUM = true;
     }
   }
 }
diff --git a/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java b/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
index 25715b5..f3e5a0b 100644
--- a/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
+++ b/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
@@ -37,9 +37,10 @@
 
   @Test
   void checkCryptoHashSha512MultiPart() {
-    byte[] message = "This is a test message".getBytes(UTF_8);
-    byte[] hash = new byte[(int) Sodium.crypto_hash_sha512_bytes()];
-    int rc = Sodium.crypto_hash_sha512(hash, message, message.length);
+    byte[] messageBytes = "This is a test message".getBytes(UTF_8);
+    Pointer message = Sodium.dup(messageBytes);
+    Pointer hash = Sodium.malloc(SHA512Hash.Hash.length());
+    int rc = Sodium.crypto_hash_sha512(hash, message, messageBytes.length);
     assertEquals(0, rc);
 
     Pointer state = Sodium.sodium_malloc(Sodium.crypto_hash_sha512_statebytes());
@@ -55,7 +56,8 @@
       byte[] hash2 = new byte[(int) Sodium.crypto_hash_sha512_bytes()];
       Sodium.crypto_hash_sha512_final(state, hash2);
 
-      assertArrayEquals(hash, hash2);
+      byte[] hashBytes = Sodium.reify(hash, 64);
+      assertArrayEquals(hashBytes, hash2);
     } finally {
       Sodium.sodium_free(state);
     }
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index b003a43..7ffd204 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -97,6 +97,8 @@
 
     dependency('org.rocksdb:rocksdbjni:5.17.2')
     dependency('org.slf4j:slf4j-api:1.7.30')
+    dependency('org.connid:framework:1.3.2')
+    dependency('org.connid:framework-internal:1.3.2')
 
     dependency('org.webjars:bootstrap:4.1.3')
     dependency('org.webjars:webjars-locator:0.40')
diff --git a/LICENSE-binary b/dist/LICENSE-binary
similarity index 99%
rename from LICENSE-binary
rename to dist/LICENSE-binary
index de371ff..ce5a6cc 100644
--- a/LICENSE-binary
+++ b/dist/LICENSE-binary
@@ -364,9 +364,9 @@
 the cause of action arose. Each party waives its rights to a jury trial in any 
 resulting litigation.
 ------------------------------------------------------------------------------------
-This product is distributed with the javax.activation, jaxb-api, jboss-transaction 
-and javax.servlet libraries under the  Common Development and Distribution License 
-1.0 (https://opensource.org/licenses/CDDL-1.0):
+This product is distributed with the connid, javax.activation, jaxb-api,
+jboss-transaction and javax.servlet libraries under the  Common Development and
+Distribution License 1.0 (https://opensource.org/licenses/CDDL-1.0):
 COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
 
 1. Definitions.
diff --git a/NOTICE-binary b/dist/NOTICE-binary
similarity index 100%
rename from NOTICE-binary
rename to dist/NOTICE-binary
diff --git a/dist/build.gradle b/dist/build.gradle
index 466a2af..7cb69eb 100644
--- a/dist/build.gradle
+++ b/dist/build.gradle
@@ -29,7 +29,7 @@
     new File("$project.buildDir/license").mkdirs()
     def binaryNoticeFile = new File("$project.buildDir/license/LICENSE")
     binaryNoticeFile.write(new File("$rootProject.projectDir/LICENSE").text)
-    binaryNoticeFile.append(new File("$rootProject.projectDir/LICENSE-binary").text)
+    binaryNoticeFile.append(new File("$project.projectDir/LICENSE-binary").text)
   }
 }
 
@@ -40,7 +40,7 @@
     new File("$project.buildDir/notice").mkdirs()
     def binaryNoticeFile = new File("$project.buildDir/notice/NOTICE")
     binaryNoticeFile.write(new File("$rootProject.projectDir/NOTICE").text)
-    binaryNoticeFile.append(new File("$rootProject.projectDir/NOTICE-binary").text)
+    binaryNoticeFile.append(new File("$project.projectDir/NOTICE-binary").text)
   }
 }
 
@@ -129,12 +129,12 @@
       into('') {
         from 'build'
         include 'gradle.properties'
-        include 'NOTICE'
       }
       mandatoryFiles(it)
       into('') {
         from ".."
         include 'LICENSE'
+        include 'NOTICE'
         include 'README.md'
         include 'build.sh'
         include 'build.bat'
@@ -142,6 +142,8 @@
         include '*.gradle'
         include 'dependency-versions.gradle'
         include 'gradle/resources/*'
+        include 'dist/LICENSE-binary'
+        include 'dist/NOTICE-binary'
         include 'gradle/*'
         include 'gradle/docker/*'
       }
diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle
index 627cf69..80f5a77 100644
--- a/gradle/check-licenses.gradle
+++ b/gradle/check-licenses.gradle
@@ -116,6 +116,7 @@
     ],
     (cddl1): [
       'CDDL-1.0',
+      'CDDL 1.0',
       'Common Development and Distribution License',
       'Common Development and Distribution License 1.0',
       'Dual license consisting of the CDDL v1.1 and GPL v2',
diff --git a/gradle/stage.gradle b/gradle/stage.gradle
index 25cdc8d..3976b32 100644
--- a/gradle/stage.gradle
+++ b/gradle/stage.gradle
@@ -171,7 +171,7 @@
 We're voting on the source distributions available here:
 https://dist.apache.org/repos/dist/dev/incubator/tuweni/$project.version-incubating/
 The release tag is present here:
-https://github.com/apache/incubator-tuweni/releases/tag/v$project.version-incubating
+https://github.com/apache/incubator-tuweni/releases/tag/v$project.version-incubating-rc
 
 This release includes the following changes:
 
diff --git a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
index 0c858c7..fcad407 100644
--- a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
+++ b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
@@ -34,16 +34,15 @@
 /**
  * Wallet containing a private key that is secured with symmetric encryption.
  *
- * This is vastly insecure - do not use in anything remotely close to production.
+ * This has not been audited for security concerns and should not be used in production.
  *
- * This wallet encrypts the key pair at rest, but keeps the keypair in memory, which may be dumped.
+ * This wallet encrypts the key pair at rest, and encrypts the key in memory.
  *
- * Nonce is also based on the password, should be instead stored in the wallet and unique.
+ * Nonce is based on the password, should be instead stored in the wallet and unique.
  *
  * The wallet loads from a file.
  */
 class Wallet(file: Path, password: String) {
-  // FIXME do not store keys in memory
   private val keyPair: SECP256K1.KeyPair
 
   init {
@@ -54,7 +53,10 @@
     val key = AES256GCM.Key.fromBytes(hash)
     val nonce = AES256GCM.Nonce.fromBytes(nonceBytes)
     val decrypted = AES256GCM.decrypt(encrypted, key, nonce)
-    keyPair = SECP256K1.KeyPair.fromSecretKey(SECP256K1.SecretKey.fromBytes(decrypted as Bytes32))
+    keyPair =
+      SECP256K1.KeyPair.fromSecretKey(
+        SECP256K1.SecretKey.fromBytes(Bytes32.secure(decrypted!!.toArrayUnsafe()))
+      )
   }
 
   companion object {