You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by ji...@apache.org on 2019/04/03 14:00:02 UTC

[incubator-tuweni] 01/09: Initial code load: https://github.com/ConsenSys/cava/archive/v1.0-asf.tar.gz

This is an automated email from the ASF dual-hosted git repository.

jim pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git

commit 73e4230d73d448acbd115e99e121f7c760c6156e
Author: Jim Jagielski <ji...@gmail.com>
AuthorDate: Tue Apr 2 15:22:24 2019 -0400

    Initial code load: https://github.com/ConsenSys/cava/archive/v1.0-asf.tar.gz
---
 .circleci/config.yml                               |  144 +
 .editorconfig                                      |    5 +
 .gitattributes                                     |    3 +
 .gitignore                                         |   30 +
 .gitmodules                                        |    6 +
 .idea/codeStyles/Project.xml                       |   16 +
 .idea/codeStyles/codeStyleConfig.xml               |    5 +
 CONTRIBUTING.md                                    |   61 +
 LICENSE                                            |  201 ++
 PACKAGES.md                                        |  137 +
 README.md                                          |   53 +
 build.gradle                                       |  528 ++++
 bytes/build.gradle                                 |   12 +
 .../net/consensys/cava/bytes/AbstractBytes.java    |   67 +
 .../consensys/cava/bytes/ArrayWrappingBytes.java   |  194 ++
 .../consensys/cava/bytes/ArrayWrappingBytes32.java |   56 +
 .../consensys/cava/bytes/ArrayWrappingBytes48.java |   56 +
 .../consensys/cava/bytes/BufferWrappingBytes.java  |  109 +
 .../consensys/cava/bytes/ByteBufWrappingBytes.java |  113 +
 .../cava/bytes/ByteBufferWrappingBytes.java        |  129 +
 .../main/java/net/consensys/cava/bytes/Bytes.java  | 1466 ++++++++++
 .../java/net/consensys/cava/bytes/Bytes32.java     |  282 ++
 .../java/net/consensys/cava/bytes/Bytes48.java     |  283 ++
 .../java/net/consensys/cava/bytes/BytesValues.java |   89 +
 .../consensys/cava/bytes/ConcatenatedBytes.java    |  213 ++
 .../consensys/cava/bytes/DelegatingBytes32.java    |  240 ++
 .../consensys/cava/bytes/DelegatingBytes48.java    |  240 ++
 .../cava/bytes/DelegatingMutableBytes32.java       |  280 ++
 .../cava/bytes/DelegatingMutableBytes48.java       |  280 ++
 .../cava/bytes/MutableArrayWrappingBytes.java      |   92 +
 .../cava/bytes/MutableArrayWrappingBytes32.java    |   34 +
 .../cava/bytes/MutableArrayWrappingBytes48.java    |   34 +
 .../cava/bytes/MutableBufferWrappingBytes.java     |   76 +
 .../cava/bytes/MutableByteBufWrappingBytes.java    |   81 +
 .../cava/bytes/MutableByteBufferWrappingBytes.java |   70 +
 .../net/consensys/cava/bytes/MutableBytes.java     |  363 +++
 .../net/consensys/cava/bytes/MutableBytes32.java   |  110 +
 .../net/consensys/cava/bytes/MutableBytes48.java   |  110 +
 .../net/consensys/cava/bytes/package-info.java     |   11 +
 .../net/consensys/cava/bytes/BufferBytesTest.java  |   38 +
 .../net/consensys/cava/bytes/ByteBufBytesTest.java |   38 +
 .../consensys/cava/bytes/ByteBufferBytesTest.java  |   38 +
 .../java/net/consensys/cava/bytes/Bytes32Test.java |   69 +
 .../java/net/consensys/cava/bytes/Bytes48Test.java |   69 +
 .../java/net/consensys/cava/bytes/BytesTest.java   |  465 +++
 .../net/consensys/cava/bytes/CommonBytesTests.java |  659 +++++
 .../cava/bytes/ConcatenatedBytesTest.java          |   99 +
 concurrent-coroutines/build.gradle                 |   11 +
 .../cava/concurrent/coroutines/AsyncCompletion.kt  |  194 ++
 .../cava/concurrent/coroutines/AsyncResult.kt      |  179 ++
 .../cava/concurrent/coroutines/CoroutineLatch.kt   |  101 +
 .../cava/concurrent/coroutines/Retryable.kt        |  154 +
 .../concurrent/coroutines/CoroutineLatchTest.kt    |  114 +
 .../cava/concurrent/coroutines/RetryableTest.kt    |  105 +
 concurrent/build.gradle                            |   12 +
 .../consensys/cava/concurrent/AsyncCompletion.java |  478 +++
 .../net/consensys/cava/concurrent/AsyncResult.java |  517 ++++
 .../consensys/cava/concurrent/AtomicSlotMap.java   |  253 ++
 .../concurrent/CompletableAsyncCompletion.java     |   36 +
 .../cava/concurrent/CompletableAsyncResult.java    |   40 +
 .../DefaultCompletableAsyncCompletion.java         |  374 +++
 .../concurrent/DefaultCompletableAsyncResult.java  |  409 +++
 .../net/consensys/cava/concurrent/ExpiringMap.java |  458 +++
 .../net/consensys/cava/concurrent/ExpiringSet.java |  277 ++
 .../consensys/cava/concurrent/package-info.java    |   11 +
 .../cava/concurrent/AtomicSlotMapTest.java         |  130 +
 .../DefaultCompletableAsyncCompletionTest.java     |  291 ++
 .../DefaultCompletableAsyncResultTest.java         |  199 ++
 .../consensys/cava/concurrent/ExpiringMapTest.java |  144 +
 .../consensys/cava/concurrent/ExpiringSetTest.java |  119 +
 config/build.gradle                                |   11 +
 .../net/consensys/cava/config/Configuration.java   |  371 +++
 .../consensys/cava/config/ConfigurationError.java  |   83 +
 .../consensys/cava/config/ConfigurationErrors.java |   78 +
 .../cava/config/ConfigurationValidator.java        |   34 +
 .../consensys/cava/config/DocumentPosition.java    |   90 +
 .../consensys/cava/config/EmptyConfiguration.java  |  148 +
 .../InvalidConfigurationPropertyTypeException.java |   54 +
 .../config/NoConfigurationPropertyException.java   |   27 +
 .../consensys/cava/config/PropertyValidator.java   |  203 ++
 .../consensys/cava/config/PropertyValidators.java  |   27 +
 .../java/net/consensys/cava/config/Schema.java     |  410 +++
 .../net/consensys/cava/config/SchemaBuilder.java   |  673 +++++
 .../cava/config/TomlBackedConfiguration.java       |  302 ++
 .../net/consensys/cava/config/TomlSerializer.java  |  182 ++
 .../net/consensys/cava/config/package-info.java    |   10 +
 .../cava/config/PropertyValidatorTest.java         |   84 +
 .../consensys/cava/config/SchemaBuilderTest.java   |   78 +
 .../cava/config/TomlBackedConfigurationTest.java   |  567 ++++
 crypto/build.gradle                                |   22 +
 .../main/java/net/consensys/cava/crypto/Hash.java  |  245 ++
 .../InvalidSEC256K1SecretKeyStoreException.java    |   19 +
 .../java/net/consensys/cava/crypto/SECP256K1.java  |  945 ++++++
 .../consensys/cava/crypto/mikuli/AtePairing.java   |   33 +
 .../net/consensys/cava/crypto/mikuli/BLS12381.java |  136 +
 .../net/consensys/cava/crypto/mikuli/G1Point.java  |   99 +
 .../net/consensys/cava/crypto/mikuli/G2Point.java  |   97 +
 .../net/consensys/cava/crypto/mikuli/GTPoint.java  |   53 +
 .../net/consensys/cava/crypto/mikuli/Group.java    |   23 +
 .../net/consensys/cava/crypto/mikuli/KeyPair.java  |   58 +
 .../consensys/cava/crypto/mikuli/PublicKey.java    |  114 +
 .../net/consensys/cava/crypto/mikuli/Scalar.java   |   48 +
 .../consensys/cava/crypto/mikuli/SecretKey.java    |   78 +
 .../consensys/cava/crypto/mikuli/Signature.java    |  107 +
 .../cava/crypto/mikuli/SignatureAndPublicKey.java  |   71 +
 .../consensys/cava/crypto/mikuli/package-info.java |   11 +
 .../net/consensys/cava/crypto/package-info.java    |    8 +
 .../consensys/cava/crypto/sodium/AES256GCM.java    | 1034 +++++++
 .../consensys/cava/crypto/sodium/Allocated.java    |  134 +
 .../net/consensys/cava/crypto/sodium/Auth.java     |  243 ++
 .../java/net/consensys/cava/crypto/sodium/Box.java | 1217 ++++++++
 .../consensys/cava/crypto/sodium/Concatenate.java  |  148 +
 .../sodium/DefaultDetachedEncryptionResult.java    |   46 +
 .../crypto/sodium/DetachedEncryptionResult.java    |   41 +
 .../consensys/cava/crypto/sodium/DiffieHelman.java |  464 +++
 .../consensys/cava/crypto/sodium/GenericHash.java  |  215 ++
 .../consensys/cava/crypto/sodium/HMACSHA256.java   |  199 ++
 .../consensys/cava/crypto/sodium/HMACSHA512.java   |  199 ++
 .../cava/crypto/sodium/HMACSHA512256.java          |  199 ++
 .../cava/crypto/sodium/KeyDerivation.java          |  309 ++
 .../consensys/cava/crypto/sodium/KeyExchange.java  |  705 +++++
 .../consensys/cava/crypto/sodium/LibSodium.java    | 2618 +++++++++++++++++
 .../consensys/cava/crypto/sodium/PasswordHash.java | 1022 +++++++
 .../consensys/cava/crypto/sodium/SHA256Hash.java   |  239 ++
 .../consensys/cava/crypto/sodium/SecretBox.java    | 1787 ++++++++++++
 .../cava/crypto/sodium/SecretDecryptionStream.java |   44 +
 .../cava/crypto/sodium/SecretEncryptionStream.java |   91 +
 .../consensys/cava/crypto/sodium/Signature.java    |  671 +++++
 .../net/consensys/cava/crypto/sodium/Sodium.java   | 3030 ++++++++++++++++++++
 .../cava/crypto/sodium/SodiumException.java        |   26 +
 .../cava/crypto/sodium/SodiumVersion.java          |   59 +
 .../cava/crypto/sodium/XChaCha20Poly1305.java      |  908 ++++++
 .../consensys/cava/crypto/sodium/package-info.java |   19 +
 .../java/net/consensys/cava/crypto/HashTest.java   |  118 +
 .../net/consensys/cava/crypto/SECP256K1Test.java   |  349 +++
 .../cava/crypto/mikuli/SignatureTest.java          |  121 +
 .../cava/crypto/sodium/AES256GCMTest.java          |  106 +
 .../cava/crypto/sodium/AllocatedTest.java          |   47 +
 .../net/consensys/cava/crypto/sodium/AuthTest.java |   42 +
 .../net/consensys/cava/crypto/sodium/BoxTest.java  |  194 ++
 .../cava/crypto/sodium/ConcatenateTest.java        |   42 +
 .../cava/crypto/sodium/DiffieHelmanTest.java       |   38 +
 .../cava/crypto/sodium/GenericHashTest.java        |   37 +
 .../cava/crypto/sodium/HMACSHA256Test.java         |   54 +
 .../cava/crypto/sodium/HMACSHA512256Test.java      |   54 +
 .../cava/crypto/sodium/HMACSHA512Test.java         |   54 +
 .../cava/crypto/sodium/KeyDerivationTest.java      |   43 +
 .../cava/crypto/sodium/PasswordHashTest.java       |  118 +
 .../cava/crypto/sodium/SHA256HashTest.java         |   37 +
 .../cava/crypto/sodium/SecretBoxTest.java          |  200 ++
 .../cava/crypto/sodium/SignatureTest.java          |   54 +
 .../consensys/cava/crypto/sodium/SodiumTest.java   |   89 +
 .../cava/crypto/sodium/XChaCha20Poly1305Test.java  |   94 +
 dependency-versions.gradle                         |   59 +
 devp2p/build.gradle                                |   22 +
 .../consensys/cava/devp2p/AtomicLongProperty.kt    |   34 +
 .../net/consensys/cava/devp2p/DiscoveryService.kt  |  867 ++++++
 .../kotlin/net/consensys/cava/devp2p/Endpoint.kt   |  123 +
 .../kotlin/net/consensys/cava/devp2p/EnodeUri.kt   |   62 +
 .../main/kotlin/net/consensys/cava/devp2p/Node.kt  |   41 +
 .../kotlin/net/consensys/cava/devp2p/Packet.kt     |  364 +++
 .../kotlin/net/consensys/cava/devp2p/PacketType.kt |   80 +
 .../main/kotlin/net/consensys/cava/devp2p/Peer.kt  |   88 +
 .../net/consensys/cava/devp2p/PeerRepository.kt    |  191 ++
 .../net/consensys/cava/devp2p/PeerRoutingTable.kt  |   95 +
 .../cava/devp2p/DiscoveryServiceJavaTest.java      |   71 +
 .../consensys/cava/devp2p/DiscoveryServiceTest.kt  |  282 ++
 .../net/consensys/cava/devp2p/EndpointTest.kt      |   95 +
 .../cava/devp2p/EphemeralPeerRepositoryTest.kt     |  251 ++
 .../consensys/cava/devp2p/FindNodePacketTest.kt    |   62 +
 .../consensys/cava/devp2p/NeighborsPacketTest.kt   |   81 +
 .../net/consensys/cava/devp2p/PingPacketTest.kt    |  102 +
 .../net/consensys/cava/devp2p/PongPacketTest.kt    |   65 +
 eth-reference-tests/build.gradle                   |   17 +
 .../cava/eth/reference/BlockRLPTestSuite.java      |  144 +
 .../cava/eth/reference/MerkleTrieTestSuite.java    |   99 +
 .../cava/eth/reference/RLPReferenceTestSuite.java  |  137 +
 .../consensys/cava/eth/reference/SSZTestSuite.java |  178 ++
 .../cava/eth/reference/TransactionTestSuite.java   |  144 +
 eth-repository/build.gradle                        |   16 +
 .../cava/eth/repository/BlockHeaderFields.kt       |   37 +
 .../cava/eth/repository/BlockchainIndex.kt         |  634 ++++
 .../cava/eth/repository/BlockchainRepository.kt    |  348 +++
 .../eth/repository/TransactionReceiptFields.kt     |   35 +
 .../cava/eth/repository/BlockchainIndexTest.kt     |  342 +++
 .../eth/repository/BlockchainRepositoryTest.kt     |  477 +++
 eth/build.gradle                                   |   15 +
 .../main/java/net/consensys/cava/eth/Address.java  |   99 +
 .../main/java/net/consensys/cava/eth/Block.java    |  134 +
 .../java/net/consensys/cava/eth/BlockBody.java     |  133 +
 .../java/net/consensys/cava/eth/BlockHeader.java   |  383 +++
 eth/src/main/java/net/consensys/cava/eth/Hash.java |  116 +
 eth/src/main/java/net/consensys/cava/eth/Log.java  |  118 +
 .../net/consensys/cava/eth/LogsBloomFilter.java    |  132 +
 .../java/net/consensys/cava/eth/Transaction.java   |  439 +++
 .../net/consensys/cava/eth/TransactionReceipt.java |  231 ++
 .../java/net/consensys/cava/eth/package-info.java  |   11 +
 .../java/net/consensys/cava/eth/BlockBodyTest.java |   46 +
 .../net/consensys/cava/eth/BlockHeaderTest.java    |   56 +
 .../java/net/consensys/cava/eth/BlockTest.java     |   47 +
 .../test/java/net/consensys/cava/eth/LogTest.java  |   37 +
 .../consensys/cava/eth/LogsBloomFilterTest.java    |   48 +
 .../consensys/cava/eth/TransactionReceiptTest.java |   63 +
 .../net/consensys/cava/eth/TransactionTest.java    |   79 +
 gradle.properties                                  |    1 +
 gradle/check-licenses.gradle                       |  156 +
 gradle/eclipse-java-consensys-style.xml            |  343 +++
 gradle/greclipse-gradle-consensys-style.properties |   51 +
 gradle/spotless.license.java                       |   12 +
 gradle/wrapper/gradle-wrapper.jar                  |  Bin 0 -> 56177 bytes
 gradle/wrapper/gradle-wrapper.properties           |    5 +
 gradlew                                            |  172 ++
 gradlew.bat                                        |   84 +
 io/build.gradle                                    |   14 +
 io/src/main/java/net/consensys/cava/io/Base64.java |   68 +
 .../java/net/consensys/cava/io/IOConsumer.java     |   30 +
 .../net/consensys/cava/io/NullOutputStream.java    |   24 +
 .../main/java/net/consensys/cava/io/Resources.java |  334 +++
 .../main/java/net/consensys/cava/io/Streams.java   |   75 +
 .../java/net/consensys/cava/io/file/Files.java     |  200 ++
 .../net/consensys/cava/io/file/package-info.java   |   11 +
 .../java/net/consensys/cava/io/package-info.java   |   11 +
 .../java/net/consensys/cava/io/Base64Test.java     |   47 +
 .../java/net/consensys/cava/io/ResourcesTest.java  |   71 +
 .../java/net/consensys/cava/io/StreamsTest.java    |   34 +
 .../java/net/consensys/cava/io/file/FilesTest.java |   63 +
 .../io/file/resourceresolver/subdir/test3.yaml     |    0
 .../cava/io/file/resourceresolver/test1.txt        |    0
 .../cava/io/file/resourceresolver/test2.txt        |    0
 .../resources/net/consensys/cava/io/file/test.txt  |    3 +
 io/src/test/resources/resourceresolver-test.jar    |  Bin 0 -> 2446 bytes
 junit/build.gradle                                 |   18 +
 .../cava/junit/BouncyCastleExtension.java          |   31 +
 .../java/net/consensys/cava/junit/LuceneIndex.java |   26 +
 .../consensys/cava/junit/LuceneIndexWriter.java    |   26 +
 .../cava/junit/LuceneIndexWriterExtension.java     |   79 +
 .../java/net/consensys/cava/junit/RedisPort.java   |   27 +
 .../consensys/cava/junit/RedisServerExtension.java |  110 +
 .../net/consensys/cava/junit/TempDirectory.java    |   26 +
 .../cava/junit/TempDirectoryExtension.java         |   63 +
 .../net/consensys/cava/junit/VertxExtension.java   |   54 +
 .../net/consensys/cava/junit/VertxInstance.java    |   26 +
 .../net/consensys/cava/junit/package-info.java     |    8 +
 .../cava/junit/LuceneIndexWriterExtensionTest.java |   38 +
 .../cava/junit/RedisServerExtensionTest.java       |   38 +
 .../cava/junit/TempDirectoryExtensionTest.java     |   32 +
 kademlia/build.gradle                              |   12 +
 .../cava/kademlia/KademliaRoutingTable.kt          |  285 ++
 .../cava/kademlia/KademliaRoutingTableTest.kt      |  153 +
 .../cava/kademlia/LogarithmicDistanceTest.kt       |   51 +
 .../consensys/cava/kademlia/OrderedInsertTest.kt   |   54 +
 kv/build.gradle                                    |   34 +
 .../net/consensys/cava/kv/RedisBytesCodec.java     |   55 +
 .../java/net/consensys/cava/kv/package-info.java   |   11 +
 .../consensys/cava/kv/InfinispanKeyValueStore.kt   |   48 +
 .../kotlin/net/consensys/cava/kv/KeyValueStore.kt  |   91 +
 .../net/consensys/cava/kv/LevelDBKeyValueStore.kt  |   93 +
 .../net/consensys/cava/kv/MapDBKeyValueStore.kt    |   98 +
 .../net/consensys/cava/kv/MapKeyValueStore.kt      |   58 +
 .../net/consensys/cava/kv/RedisKeyValueStore.kt    |  111 +
 .../net/consensys/cava/kv/RocksDBKeyValueStore.kt  |  105 +
 .../net/consensys/cava/kv/SQLKeyValueStore.kt      |  113 +
 .../net/consensys/cava/kv/KeyValueStoreTest.java   |   74 +
 .../consensys/cava/kv/RedisKeyValueStoreTest.java  |   66 +
 .../net/consensys/cava/kv/KeyValueStoreSpec.kt     |  264 ++
 les/build.gradle                                   |   25 +
 .../net/consensys/cava/les/BlockBodiesMessage.kt   |   47 +
 .../net/consensys/cava/les/BlockHeadersMessage.kt  |   52 +
 .../consensys/cava/les/GetBlockBodiesMessage.kt    |   40 +
 .../consensys/cava/les/GetBlockHeadersMessage.kt   |   72 +
 .../net/consensys/cava/les/GetReceiptsMessage.kt   |   40 +
 .../kotlin/net/consensys/cava/les/LESPeerState.kt  |   32 +
 .../consensys/cava/les/LESSubProtocolHandler.kt    |  192 ++
 .../net/consensys/cava/les/LESSubprotocol.kt       |   83 +
 .../kotlin/net/consensys/cava/les/LightClient.kt   |   58 +
 .../net/consensys/cava/les/ReceiptsMessage.kt      |   51 +
 .../kotlin/net/consensys/cava/les/StatusMessage.kt |  162 ++
 .../cava/les/LESSubProtocolHandlerTest.kt          |  478 +++
 .../net/consensys/cava/les/LESSubprotocolTest.kt   |  101 +
 .../kotlin/net/consensys/cava/les/MessagesTest.kt  |  226 ++
 merkle-trie/build.gradle                           |   17 +
 .../net/consensys/cava/trie/CompactEncoding.java   |  131 +
 .../java/net/consensys/cava/trie/package-info.java |   14 +
 .../kotlin/net/consensys/cava/trie/BranchNode.kt   |  145 +
 .../net/consensys/cava/trie/DefaultNodeFactory.kt  |   56 +
 .../net/consensys/cava/trie/ExtensionNode.kt       |   78 +
 .../kotlin/net/consensys/cava/trie/GetVisitor.kt   |   55 +
 .../kotlin/net/consensys/cava/trie/LeafNode.kt     |   65 +
 .../net/consensys/cava/trie/MerklePatriciaTrie.kt  |  112 +
 .../net/consensys/cava/trie/MerkleStorage.kt       |   73 +
 .../consensys/cava/trie/MerkleStorageException.kt  |   37 +
 .../kotlin/net/consensys/cava/trie/MerkleTrie.kt   |  142 +
 .../main/kotlin/net/consensys/cava/trie/Node.kt    |   33 +
 .../kotlin/net/consensys/cava/trie/NodeFactory.kt  |   26 +
 .../kotlin/net/consensys/cava/trie/NodeVisitor.kt  |   26 +
 .../kotlin/net/consensys/cava/trie/NullNode.kt     |   44 +
 .../kotlin/net/consensys/cava/trie/PutVisitor.kt   |   92 +
 .../net/consensys/cava/trie/RemoveVisitor.kt       |   58 +
 .../cava/trie/StoredMerklePatriciaTrie.kt          |  194 ++
 .../kotlin/net/consensys/cava/trie/StoredNode.kt   |  112 +
 .../net/consensys/cava/trie/StoredNodeFactory.kt   |  203 ++
 .../consensys/cava/trie/CompactEncodingTest.java   |   52 +
 .../cava/trie/MerklePatriciaTrieJavaTest.java      |  246 ++
 .../trie/MerklePatriciaTriePerformanceTest.java    |  103 +
 .../trie/StoredMerklePatriciaTrieJavaTest.java     |  305 ++
 .../cava/trie/MerklePatriciaTrieKotlinTest.kt      |  265 ++
 .../trie/StoredMerklePatriciaTrieKotlinTest.kt     |  324 +++
 net-coroutines/build.gradle                        |   14 +
 .../cava/net/coroutines/CoroutineByteChannel.kt    |  311 ++
 .../cava/net/coroutines/CoroutineChannelGroup.kt   |  205 ++
 .../net/coroutines/CoroutineDatagramChannel.kt     |  180 ++
 .../cava/net/coroutines/CoroutineNetworkChannel.kt |  156 +
 .../cava/net/coroutines/CoroutineSelector.kt       |  466 +++
 .../net/coroutines/CoroutineServerSocketChannel.kt |  111 +
 .../cava/net/coroutines/CoroutineSocketChannel.kt  |  125 +
 .../cava/net/coroutines/SelectorTest.java          |  169 ++
 .../net/coroutines/CoroutineChannelGroupTest.kt    |  103 +
 .../net/coroutines/CoroutineDatagramChannelTest.kt |   92 +
 .../cava/net/coroutines/CoroutineSelectorTest.kt   |  233 ++
 .../net/coroutines/CoroutineSocketChannelTest.kt   |  137 +
 net/build.gradle                                   |   21 +
 .../java/net/consensys/cava/net/package-info.java  |    8 +
 .../net/tls/ClientFingerprintTrustManager.java     |  132 +
 .../net/tls/DelegatingTrustManagerFactory.java     |  232 ++
 .../net/tls/FileBackedFingerprintRepository.java   |  172 ++
 .../cava/net/tls/FingerprintRepository.java        |   48 +
 .../net/tls/ServerFingerprintTrustManager.java     |  127 +
 .../cava/net/tls/SingleTrustManagerFactory.java    |   39 +
 .../main/java/net/consensys/cava/net/tls/TLS.java  |  194 ++
 .../cava/net/tls/TLSEnvironmentException.java      |   24 +
 .../cava/net/tls/TrustManagerFactories.java        |  723 +++++
 .../cava/net/tls/TrustManagerFactoryWrapper.java   |   54 +
 .../consensys/cava/net/tls/VertxTrustOptions.java  |  649 +++++
 .../net/consensys/cava/net/tls/package-info.java   |    7 +
 .../cava/net/tls/ClientCaOrRecordTest.java         |  230 ++
 .../consensys/cava/net/tls/ClientCaOrTofuTest.java |  191 ++
 .../cava/net/tls/ClientCaOrWhitelistTest.java      |  161 ++
 .../consensys/cava/net/tls/ClientRecordTest.java   |  211 ++
 .../net/consensys/cava/net/tls/ClientTofuTest.java |  172 ++
 .../cava/net/tls/ClientWhitelistTest.java          |  169 ++
 .../tls/FileBackedFingerprintRepositoryTest.java   |   75 +
 .../cava/net/tls/InsecureTrustOptions.java         |   36 +
 .../consensys/cava/net/tls/SecurityTestUtils.java  |   76 +
 .../cava/net/tls/ServerCaOrRecordTest.java         |  207 ++
 .../consensys/cava/net/tls/ServerCaOrTofaTest.java |  163 ++
 .../cava/net/tls/ServerCaOrWhitelistTest.java      |  156 +
 .../consensys/cava/net/tls/ServerRecordTest.java   |  210 ++
 .../net/consensys/cava/net/tls/ServerTofaTest.java |  166 ++
 .../cava/net/tls/ServerWhitelistTest.java          |  157 +
 .../java/net/consensys/cava/net/tls/TLSTest.java   |  112 +
 plumtree/build.gradle                              |   12 +
 .../cava/plumtree/EphemeralPeerRepository.java     |   74 +
 .../consensys/cava/plumtree/MessageHashing.java    |   25 +
 .../net/consensys/cava/plumtree/MessageSender.java |   39 +
 .../java/net/consensys/cava/plumtree/Peer.java     |   16 +
 .../consensys/cava/plumtree/PeerRepository.java    |   73 +
 .../java/net/consensys/cava/plumtree/State.java    |  198 ++
 .../net/consensys/cava/plumtree/StateActor.java    |   16 +
 .../consensys/cava/plumtree/StateActorFactory.java |   18 +
 .../net/consensys/cava/plumtree/package-info.java  |   11 +
 .../net/consensys/cava/plumtree/StateTest.java     |  223 ++
 rlp/build.gradle                                   |   12 +
 .../consensys/cava/rlp/AccumulatingRLPWriter.java  |   96 +
 .../consensys/cava/rlp/ByteBufferRLPWriter.java    |   80 +
 .../net/consensys/cava/rlp/BytesRLPReader.java     |  269 ++
 .../net/consensys/cava/rlp/BytesRLPWriter.java     |   32 +
 .../consensys/cava/rlp/DelegatingRLPWriter.java    |   78 +
 .../net/consensys/cava/rlp/EndOfRLPException.java  |   22 +
 .../cava/rlp/InvalidRLPEncodingException.java      |   22 +
 .../cava/rlp/InvalidRLPTypeException.java          |   22 +
 rlp/src/main/java/net/consensys/cava/rlp/RLP.java  |  504 ++++
 .../java/net/consensys/cava/rlp/RLPException.java  |   30 +
 .../java/net/consensys/cava/rlp/RLPReader.java     |  402 +++
 .../java/net/consensys/cava/rlp/RLPWriter.java     |  140 +
 .../java/net/consensys/cava/rlp/package-info.java  |   13 +
 .../consensys/cava/rlp/ByteBufferWriterTest.java   |  162 ++
 .../net/consensys/cava/rlp/BytesRLPReaderTest.java |  222 ++
 .../net/consensys/cava/rlp/BytesRLPWriterTest.java |  174 ++
 rlpx/build.gradle                                  |   21 +
 .../cava/rlpx/EthereumIESEncryptionEngine.java     |  534 ++++
 .../net/consensys/cava/rlpx/HandshakeMessage.java  |   32 +
 .../cava/rlpx/InitiatorHandshakeMessage.java       |   94 +
 .../consensys/cava/rlpx/InvalidMACException.java   |   27 +
 .../cava/rlpx/MemoryWireConnectionsRepository.java |   51 +
 .../net/consensys/cava/rlpx/RLPxConnection.java    |  315 ++
 .../consensys/cava/rlpx/RLPxConnectionFactory.java |  333 +++
 .../java/net/consensys/cava/rlpx/RLPxMessage.java  |   74 +
 .../java/net/consensys/cava/rlpx/RLPxService.java  |   85 +
 .../cava/rlpx/ResponderHandshakeMessage.java       |   69 +
 .../cava/rlpx/WireConnectionRepository.java        |   52 +
 .../java/net/consensys/cava/rlpx/package-info.java |   12 +
 .../cava/rlpx/vertx/VertxRLPxService.java          |  390 +++
 .../consensys/cava/rlpx/vertx/package-info.java    |   12 +
 .../net/consensys/cava/rlpx/wire/Capability.java   |   56 +
 .../rlpx/wire/DefaultSubProtocolIdentifier.java    |   37 +
 .../cava/rlpx/wire/DefaultWireConnection.java      |  242 ++
 .../cava/rlpx/wire/DisconnectMessage.java          |   47 +
 .../consensys/cava/rlpx/wire/DisconnectReason.java |   39 +
 .../net/consensys/cava/rlpx/wire/HelloMessage.java |  148 +
 .../net/consensys/cava/rlpx/wire/PingMessage.java  |   33 +
 .../net/consensys/cava/rlpx/wire/PongMessage.java  |   32 +
 .../net/consensys/cava/rlpx/wire/SubProtocol.java  |   49 +
 .../cava/rlpx/wire/SubProtocolHandler.java         |   47 +
 .../cava/rlpx/wire/SubProtocolIdentifier.java      |   35 +
 .../consensys/cava/rlpx/wire/WireConnection.java   |   26 +
 .../cava/rlpx/wire/WireProtocolMessage.java        |   32 +
 .../net/consensys/cava/rlpx/wire/package-info.java |   12 +
 .../cava/rlpx/RLPxConnectionFactoryTest.java       |  263 ++
 .../cava/rlpx/vertx/VertxAcceptanceTest.java       |  310 ++
 .../cava/rlpx/vertx/VertxRLPxServiceTest.java      |  229 ++
 .../cava/rlpx/wire/DefaultWireConnectionTest.java  |  224 ++
 .../cava/rlpx/wire/DisconnectMessageTest.java      |   31 +
 .../consensys/cava/rlpx/wire/HelloMessageTest.java |   46 +
 .../net/consensys/cava/rlpx/wire/PingPongTest.java |   88 +
 .../wire/RLPxConnectionMessageExchangeTest.java    |  123 +
 scuttlebutt-discovery/build.gradle                 |   19 +
 .../cava/scuttlebutt/discovery/LocalIdentity.java  |  123 +
 .../ScuttlebuttLocalDiscoveryService.java          |  192 ++
 .../cava/scuttlebutt/discovery/package-info.java   |   14 +
 .../scuttlebutt/discovery/LocalIdentityTest.java   |   71 +
 .../ScuttlebuttLocalDiscoveryServiceTest.java      |  136 +
 scuttlebutt-handshake/build.gradle                 |   24 +
 .../scuttlebutt/handshake/HandshakeException.java  |   23 +
 .../SecureScuttlebuttHandshakeClient.java          |  283 ++
 .../SecureScuttlebuttHandshakeServer.java          |  273 ++
 .../handshake/SecureScuttlebuttStream.java         |  152 +
 .../handshake/SecureScuttlebuttStreamClient.java   |   46 +
 .../handshake/SecureScuttlebuttStreamServer.java   |   56 +
 .../scuttlebutt/handshake/StreamException.java     |   20 +
 .../cava/scuttlebutt/handshake/package-info.java   |   14 +
 .../scuttlebutt/handshake/vertx/ClientHandler.java |   33 +
 .../handshake/vertx/ClientHandlerFactory.java      |   31 +
 .../vertx/SecureScuttlebuttVertxClient.java        |  218 ++
 .../vertx/SecureScuttlebuttVertxServer.java        |  193 ++
 .../scuttlebutt/handshake/vertx/ServerHandler.java |   33 +
 .../handshake/vertx/ServerHandlerFactory.java      |   31 +
 .../scuttlebutt/handshake/vertx/package-info.java  |   14 +
 .../SecureScuttlebuttHandshakeClientTest.java      |  152 +
 .../handshake/SecureScuttlebuttStreamTest.java     |  160 ++
 .../handshake/vertx/VertxIntegrationTest.java      |  144 +
 scuttlebutt-rpc/build.gradle                       |   23 +
 .../consensys/cava/scuttlebutt/rpc/RPCCodec.java   |  130 +
 .../consensys/cava/scuttlebutt/rpc/RPCFlag.java    |  124 +
 .../consensys/cava/scuttlebutt/rpc/RPCMessage.java |  140 +
 .../scuttlebutt/rpc/PatchworkIntegrationTest.java  |  183 ++
 .../cava/scuttlebutt/rpc/RPCEncodingTest.java      |   85 +
 .../cava/scuttlebutt/rpc/RPCFlagTest.java          |   56 +
 scuttlebutt/build.gradle                           |   14 +
 .../cava/scuttlebutt/Ed25519KeyPairIdentity.java   |   82 +
 .../cava/scuttlebutt/Ed25519PublicKeyIdentity.java |   83 +
 .../net/consensys/cava/scuttlebutt/Identity.java   |  192 ++
 .../net/consensys/cava/scuttlebutt/Invite.java     |  102 +
 .../cava/scuttlebutt/SECP256K1KeyPairIdentity.java |   78 +
 .../scuttlebutt/SECP256K1PublicKeyIdentity.java    |   83 +
 .../consensys/cava/scuttlebutt/package-info.java   |   14 +
 .../consensys/cava/scuttlebutt/IdentityTest.java   |  167 ++
 .../net/consensys/cava/scuttlebutt/InviteTest.java |   54 +
 settings.gradle                                    |   28 +
 ssz/build.gradle                                   |   17 +
 .../consensys/cava/ssz/ByteBufferSSZWriter.java    |   36 +
 .../net/consensys/cava/ssz/BytesSSZReader.java     |  250 ++
 .../net/consensys/cava/ssz/BytesSSZWriter.java     |   35 +
 .../net/consensys/cava/ssz/EndOfSSZException.java  |   22 +
 .../cava/ssz/InvalidSSZTypeException.java          |   22 +
 ssz/src/main/java/net/consensys/cava/ssz/SSZ.java  | 1861 ++++++++++++
 .../java/net/consensys/cava/ssz/SSZException.java  |   30 +
 .../java/net/consensys/cava/ssz/SSZReader.java     |  621 ++++
 .../java/net/consensys/cava/ssz/SSZWriter.java     |  673 +++++
 .../java/net/consensys/cava/ssz/package-info.java  |   13 +
 .../cava/ssz/experimental/BytesSSZReader.kt        |   87 +
 .../cava/ssz/experimental/BytesSSZWriter.kt        |  117 +
 .../net/consensys/cava/ssz/experimental/SSZ.kt     |  187 ++
 .../consensys/cava/ssz/experimental/SSZReader.kt   |  524 ++++
 .../consensys/cava/ssz/experimental/SSZWriter.kt   |  566 ++++
 .../consensys/cava/ssz/ByteBufferWriterTest.java   |  127 +
 .../net/consensys/cava/ssz/BytesSSZReaderTest.java |  197 ++
 .../net/consensys/cava/ssz/BytesSSZWriterTest.java |  429 +++
 .../net/consensys/cava/ssz/HashTreeRootTest.java   |  113 +
 .../net/consensys/cava/ssz/experimental/SSZTest.kt |   38 +
 toml/README.md                                     |   48 +
 toml/build.gradle                                  |   29 +
 .../net/consensys/cava/toml/internal/TomlLexer.g4  |  189 ++
 .../net/consensys/cava/toml/internal/TomlParser.g4 |  174 ++
 .../cava/toml/AccumulatingErrorListener.java       |  124 +
 .../java/net/consensys/cava/toml/ArrayVisitor.java |   49 +
 .../net/consensys/cava/toml/ErrorReporter.java     |   17 +
 .../consensys/cava/toml/InlineTableVisitor.java    |   51 +
 .../net/consensys/cava/toml/JsonSerializer.java    |  168 ++
 .../java/net/consensys/cava/toml/KeyVisitor.java   |   48 +
 .../java/net/consensys/cava/toml/LineVisitor.java  |  112 +
 .../net/consensys/cava/toml/LocalDateVisitor.java  |  102 +
 .../net/consensys/cava/toml/LocalTimeVisitor.java  |  119 +
 .../net/consensys/cava/toml/MutableTomlArray.java  |  156 +
 .../net/consensys/cava/toml/MutableTomlTable.java  |  236 ++
 .../main/java/net/consensys/cava/toml/Parser.java  |  100 +
 .../consensys/cava/toml/QuotedStringVisitor.java   |   99 +
 .../java/net/consensys/cava/toml/TokenName.java    |   73 +
 .../main/java/net/consensys/cava/toml/Toml.java    |  246 ++
 .../java/net/consensys/cava/toml/TomlArray.java    |  304 ++
 .../cava/toml/TomlInvalidTypeException.java        |   23 +
 .../net/consensys/cava/toml/TomlParseError.java    |   43 +
 .../net/consensys/cava/toml/TomlParseResult.java   |   35 +
 .../java/net/consensys/cava/toml/TomlPosition.java |  102 +
 .../java/net/consensys/cava/toml/TomlTable.java    | 1168 ++++++++
 .../java/net/consensys/cava/toml/TomlType.java     |   61 +
 .../java/net/consensys/cava/toml/TomlVersion.java  |   62 +
 .../java/net/consensys/cava/toml/ValueVisitor.java |  173 ++
 .../net/consensys/cava/toml/ZoneOffsetVisitor.java |   84 +
 .../java/net/consensys/cava/toml/package-info.java |   13 +
 .../consensys/cava/toml/MutableTomlArrayTest.java  |   90 +
 .../consensys/cava/toml/MutableTomlTableTest.java  |  194 ++
 .../net/consensys/cava/toml/TokenNameTest.java     |   36 +
 .../java/net/consensys/cava/toml/TomlTest.java     |  563 ++++
 .../net/consensys/cava/toml/example-v0.4.0.toml    |  244 ++
 .../net/consensys/cava/toml/hard_example.toml      |   33 +
 .../consensys/cava/toml/hard_example_unicode.toml  |   36 +
 .../cava/toml/toml-v0.5.0-spec-example.toml        |   33 +
 units/build.gradle                                 |   11 +
 .../cava/units/bigints/BaseUInt256Value.java       |  335 +++
 .../cava/units/bigints/BaseUInt384Value.java       |  335 +++
 .../cava/units/bigints/BaseUInt64Value.java        |  334 +++
 .../net/consensys/cava/units/bigints/UInt256.java  |  782 +++++
 .../cava/units/bigints/UInt256Domain.java          |   52 +
 .../consensys/cava/units/bigints/UInt256Value.java |  388 +++
 .../cava/units/bigints/UInt256ValueDomain.java     |   65 +
 .../net/consensys/cava/units/bigints/UInt256s.java |   42 +
 .../net/consensys/cava/units/bigints/UInt384.java  |  782 +++++
 .../cava/units/bigints/UInt384Domain.java          |   52 +
 .../consensys/cava/units/bigints/UInt384Value.java |  388 +++
 .../cava/units/bigints/UInt384ValueDomain.java     |   65 +
 .../net/consensys/cava/units/bigints/UInt384s.java |   42 +
 .../net/consensys/cava/units/bigints/UInt64.java   |  559 ++++
 .../consensys/cava/units/bigints/UInt64Domain.java |   52 +
 .../consensys/cava/units/bigints/UInt64Value.java  |  382 +++
 .../cava/units/bigints/UInt64ValueDomain.java      |   65 +
 .../net/consensys/cava/units/bigints/UInt64s.java  |   42 +
 .../consensys/cava/units/bigints/package-info.java |    7 +
 .../net/consensys/cava/units/ethereum/Gas.java     |  151 +
 .../net/consensys/cava/units/ethereum/Wei.java     |   93 +
 .../cava/units/ethereum/package-info.java          |    7 +
 .../net/consensys/cava/units/package-info.java     |    8 +
 .../cava/units/bigints/BaseUInt256ValueTest.java   |  904 ++++++
 .../cava/units/bigints/BaseUInt384ValueTest.java   |  813 ++++++
 .../cava/units/bigints/BaseUInt64ValueTest.java    |  745 +++++
 .../consensys/cava/units/bigints/UInt256Test.java  |  928 ++++++
 .../consensys/cava/units/bigints/UInt384Test.java  |  947 ++++++
 .../consensys/cava/units/bigints/UInt64Test.java   |  760 +++++
 .../net/consensys/cava/units/ethereum/GasTest.java |   86 +
 .../net/consensys/cava/units/ethereum/WeiTest.java |   63 +
 549 files changed, 92440 insertions(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..24f8d83
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,144 @@
+version: 2
+jobs:
+  build:
+    docker:
+      - image: circleci/openjdk:11-jdk-sid
+
+    working_directory: ~/repo
+
+    environment:
+      TERM: dumb
+      JAVA_TOOL_OPTIONS: -Xmx768m
+      GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2
+      GRADLE_MAX_TEST_FORKS: 2
+
+    steps:
+      - checkout
+      - run:
+          name: Check submodule status
+          command: git submodule status | tee ~/submodule-status
+
+      - restore_cache:
+          name: Restoring cached submodules
+          keys:
+          - v1-submodules-{{ checksum "~/submodule-status" }}
+
+      - run:
+          name: Update submodules
+          command: git submodule update --init --recursive
+
+      - run:
+          name: Install Sodium Library
+          command: |
+            sudo sh -c "echo 'deb http://deb.debian.org/debian unstable main contrib non-free' > /etc/apt/sources.list"
+            sudo apt-get update
+            sudo apt-get install -y libsodium23
+
+      - restore_cache:
+          name: Restoring cached gradle dependencies
+          keys:
+          - v1-gradle-dir-{{ checksum "build.gradle" }}
+          - v1-gradle-dir-
+
+      - run:
+          name: Downloading dependencies
+          command: ./gradlew allDependencies checkLicenses
+
+      - run:
+          name: Compiling
+          command: ./gradlew spotlessCheck assemble
+
+      - run:
+          name: Collecting artifacts
+          command: |
+            mkdir -p ~/jars
+            find . -type f -regex ".*/build/libs/.*jar" -exec cp {} ~/jars/ \;
+          when: always
+
+      - store_artifacts:
+          name: Uploading artifacts
+          path: ~/jars
+          destination: jars
+          when: always
+
+      - run:
+          name: Running tests
+          command: ./gradlew --stacktrace test
+
+      - run:
+          name: Collecting test results
+          command: |
+            ./gradlew jacocoTestReport
+            mkdir -p ~/test-results/
+            find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
+          when: always
+
+      - store_test_results:
+          name: Uploading test results
+          path: ~/test-results
+          destination: tests
+          when: always
+
+      - run:
+          name: Collecting reports
+          command: |
+            mkdir -p ~/reports/license
+            (cd ./build/reports/license && tar c .) | (cd ~/reports/license && tar x)
+            find . -type d -regex ".*/build/reports/tests/test" | while read dir; do
+                  module=`echo $dir | sed -e 's/build\/reports\/tests\/test//'`
+                  mkdir -p ~/reports/test/"$module"
+                  (cd "$dir" && tar c .) | (cd ~/reports/test/"$module" && tar x)
+              done
+              find . -type d -regex ".*/build/reports/jacoco/test/html" | while read dir; do
+                  module=`echo $dir | sed -e 's/build\/reports\/jacoco\/test\/html//'`
+                  mkdir -p ~/reports/jacoco/"$module"
+                  (cd "$dir" && tar c .) | (cd ~/reports/jacoco/"$module" && tar x)
+              done
+          when: always
+
+      - store_artifacts:
+          name: Uploading reports
+          path: ~/reports
+          destination: reports
+
+      - run:
+          name: Building JavaDoc
+          command: ./gradlew :javadoc
+
+      - store_artifacts:
+          name: Uploading JavaDoc
+          path: build/docs/javadoc
+          destination: javadoc
+
+      - run:
+          name: Building Dokka docs
+          command: ./gradlew :dokka
+
+      - store_artifacts:
+          name: Uploading Dokka docs
+          path: build/docs/dokka
+          destination: dokka
+
+      - deploy:
+          name: Deploying snapshot to Bintray (release branches only)
+          command: |
+            if [ -z "${CIRCLE_PULL_REQUEST}" ] && echo "${CIRCLE_BRANCH}" | grep -q -E '^master$|^[0-9]+\.[0-9]+$'; then
+              echo "Start deployment"
+              BINTRAY_DEPLOY=true ./gradlew deploy
+            else
+              echo "Start dry run deployment"
+              ./gradlew deploy
+            fi
+
+      - save_cache:
+          name: Caching gradle dependencies
+          paths:
+          - .gradle
+          - ~/.gradle
+          key: v1-gradle-dir-{{ checksum "build.gradle" }}-{{ .Branch }}-{{ .BuildNum }}
+
+      - save_cache:
+          name: Caching submodules
+          paths:
+            - .git/modules
+          key: v1-submodules-{{ checksum "~/submodule-status" }}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..470eda2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+[*.{kt,kts}]
+indent_size=2
+continuation_indent_size=2
+insert_final_newline=true
+max_line_length=120
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..e9de15c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+* text eol=lf
+*.jar -text
+*.bat -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..957d9b3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+*.bak
+*.swp
+*.tmp
+*~.nib
+*.iml
+*.launch
+*.swp
+*.tokens
+.classpath
+.externalToolBuilders/
+.gradle/
+.vscode/
+*.code-workspace
+.idea/*
+!.idea/codeStyles/
+.loadpath
+.metadata
+.prefs
+.project
+.recommenders/
+.settings
+.springBeans
+.vertx
+bin/
+classes/
+local.properties
+target/
+tmp/
+build/
+out/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..bfe62e7
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "eth-reference-tests/src/test/resources/tests"]
+	path = eth-reference-tests/src/test/resources/tests
+	url = https://github.com/ethereum/tests.git
+[submodule "eth-reference-tests/src/test/resources/eth2.0-tests"]
+	path = eth-reference-tests/src/test/resources/eth2.0-tests
+	url = https://github.com/ethereum/eth2.0-tests.git
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..8b54990
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,16 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <JavaCodeStyleSettings>
+      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
+      <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
+    </JavaCodeStyleSettings>
+    <JetCodeStyleSettings>
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </codeStyleSettings>
+  </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..723abef
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+# Contributing to Cava
+
+Welcome to the Cava repository! This document describes the procedure and guidelines for contributing to the Cava project. The subsequent sections encapsulate the criteria used to evaluate additions to, and modifications of, the existing codebase.
+
+## Contributor Workflow
+
+The codebase is maintained using the "*contributor workflow*" where everyone without exception contributes patch proposals using "*pull-requests*". This facilitates social contribution, easy testing and peer review.
+
+To contribute a patch, the workflow is as follows:
+
+* Fork repository
+* Create topic branch
+* Commit patch
+* Create pull-request, adhering to the coding conventions herein set forth
+
+In general a commit serves a single purpose and diffs should be easily comprehensible. For this reason do not mix any formatting fixes or code moves with actual code changes.
+
+## Style Guide
+
+`La mode se démode, le style jamais.`
+
+Guided by the immortal words of Gabrielle Bonheur, we strive to adhere strictly to best stylistic practices for each line of code in this software.
+
+At this stage one should expect comments and reviews from fellow contributors. You can add more commits to your pull request by committing them locally and pushing to your fork until you have satisfied all feedback. Before merging, you should aim to have a clean commit history where each commit identifies an specific change, or where all
+commits are squashed together.
+
+#### Stylistic
+
+The fundamental resource Cava contributors should familiarize themselves with is Oracle's [Code Conventions for the Java TM Programming Language](http://www.oracle.com/technetwork/java/codeconvtoc-136057.html), to establish a general programme on Java coding. Furthermore, all pull-requests should be formatted according to the (slightly modified) [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html), as it will be checked by our continuous integration architecture, [...]
+
+#### Architectural Best Practices
+
+Questions on architectural best practices will be guided by the principles set forth in [Effective Java](http://index-of.es/Java/Effective%20Java.pdf) by Joshua Bloch
+
+#### Clear Commit/PR Messages
+
+Commit messages should be verbose by default consisting of a short subject line (50 chars max), a blank line and detailed explanatory text as separate paragraph(s), unless the title alone is self-explanatory (such as "`Implement EXP EVM opcode`") in which case a single title line is sufficient. Commit messages should be helpful to people reading your code in the future, so explain the reasoning for your decisions. Further explanation on commit messages can be found [here](https://chris.b [...]
+
+#### Test coverage
+
+The test cases are sufficient enough to provide confidence in the code’s robustness, while avoiding redundant tests.
+
+#### Readability
+
+The code is easy to understand.
+
+#### Simplicity
+
+The code is not over-engineered, sufficient effort is made to minimize the cyclomatic complexity of the software.  
+
+#### Functional
+
+Insofar as is possible the code intuitively and expeditiously executes the designated task.
+
+#### Clean
+
+The code is free from glaring typos (*e.g. misspelled comments*), thinkos, or formatting issues (*e.g. incorrect indentation*).
+
+#### Appropriately Commented
+
+Ambiguous or unclear code segments are commented. The comments are written in complete sentences.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   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.
diff --git a/PACKAGES.md b/PACKAGES.md
new file mode 100644
index 0000000..0ecbe08
--- /dev/null
+++ b/PACKAGES.md
@@ -0,0 +1,137 @@
+# Module cava
+
+In the spirit of [Google Guava](https://github.com/google/guava/), Cava is a set of libraries and other tools to aid development of blockchain and other decentralized software in Java and other JVM languages.
+
+# Package net.consensys.cava.bytes
+
+Classes and utilities for working with byte arrays.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-bytes` (`cava-bytes.jar`).
+
+# Package net.consensys.cava.concurrent
+
+Classes and utilities for working with concurrency.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-concurrent` (`cava-concurrent.jar`).
+
+# Package net.consensys.cava.concurrent.coroutines
+
+Extensions for mapping [AsyncResult][net.consensys.cava.concurrent.AsyncResult] and [AsyncCompletion][net.consensys.cava.concurrent.AsyncCompletion] objects to and from Kotlin coroutines.
+
+# Package net.consensys.cava.config
+
+A general-purpose library for managing configuration data.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-config` (`cava-config.jar`).
+
+# Package net.consensys.cava.crypto
+
+Classes and utilities for working with cryptography.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-crypto` (`cava-crypto.jar`).
+
+# Package net.consensys.cava.crypto.sodium
+
+Classes and utilities for working with the sodium native library.
+
+Classes and utilities in this package provide an interface to the native Sodium crypto library (https://www.libsodium.org/), which must be installed on the same system as the JVM. It will be searched for in common library locations, or its it can be loaded explicitly using [net.consensys.cava.crypto.sodium.Sodium.loadLibrary].
+
+Classes in this package depend upon the JNR-FFI library, which is not automatically included when using the complete Cava distribution. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency `com.github.jnr:jnr-ffi`.
+
+# Package net.consensys.cava.devp2p
+
+Kotlin coroutine based implementation of the Ethereum ÐΞVp2p protocol.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-devp2p` (`cava-devp2p.jar`).
+
+# Package net.consensys.cava.eth
+
+Classes and utilities for working in the Ethereum domain.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-eth` (`cava-eth.jar`).
+
+# Package net.consensys.cava.io
+
+Classes and utilities for handling file and network IO.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-io` (`cava-io.jar`).
+
+# Package net.consensys.cava.io.file
+
+General utilities for working with files and the filesystem.
+
+# Package net.consensys.cava.junit
+
+Utilities for better junit testing.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-junit` (`cava-junit.jar`).
+
+# Package net.consensys.cava.kademlia
+
+An implementation of the kademlia distributed hash (routing) table.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-kademlia` (`cava-kademlia.jar`).
+
+# Package net.consensys.cava.kv
+
+Classes and utilities for working with key/value stores.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-kv` (`cava-kv.jar`).
+
+# Package net.consensys.cava.net
+
+Classes and utilities for working with networking.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-net` (`cava-net.jar`).
+
+# Package net.consensys.cava.net.coroutines
+
+Classes and utilities for coroutine based networking.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-net-coroutines` (`cava-net-coroutines.jar`).
+
+# Package net.consensys.cava.net.tls
+
+Utilities for doing fingerprint based TLS certificate checking.
+
+# Package net.consensys.cava.rlp
+
+Recursive Length Prefix (RLP) encoding and decoding.
+
+An implementation of the Ethereum Recursive Length Prefix (RLP) algorithm, as described at https://github.com/ethereum/wiki/wiki/RLP.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-rlp` (`cava-rlp.jar`).
+
+# Package net.consensys.cava.toml
+
+A parser for Tom's Obvious, Minimal Language (TOML).
+
+A parser and semantic checker for Tom's Obvious, Minimal Language (TOML), as described at https://github.com/toml-lang/toml/.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-toml` (cava-toml.jar).
+
+# Package net.consensys.cava.trie
+
+Merkle Trie implementations.
+
+Implementations of the Ethereum Patricia Trie, as described at https://github.com/ethereum/wiki/wiki/Patricia-Tree.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-merkle-trie` (`cava-merkle-trie.jar`).
+
+# Package net.consensys.cava.trie
+
+Merkle Trie implementations using Kotlin coroutines.
+
+# Package net.consensys.cava.units
+
+Classes and utilities for working with 256 bit integers and Ethereum units.
+
+These classes are included in the complete Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-units` (`cava-units.jar`).
+
+# Package net.consensys.cava.units.bigints
+
+Classes and utilities for working with 256 bit integers.
+
+# Package net.consensys.cava.units.ethereum
+
+Classes and utilities for working with Ethereum units.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e4ce4b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# Cava: ConsenSys Core Libraries for Java (& Kotlin)
+
+[![Build Status](https://circleci.com/gh/ConsenSys/cava.svg?style=shield&circle-token=440c81af8cae3c059b516a8e375471258d7e0229)](https://circleci.com/gh/ConsenSys/cava)
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ConsenSys/cava/blob/master/LICENSE)
+[![Download](https://api.bintray.com/packages/consensys/consensys/cava/images/download.svg?version=0.6.0) ](https://bintray.com/consensys/consensys/cava/0.6.0)
+
+In the spirit of [Google Guava](https://github.com/google/guava/), Cava is a set of libraries and other tools to aid development of blockchain and other decentralized software in Java and other JVM languages.
+
+It includes a low-level bytes library, serialization and deserialization codecs (e.g. [RLP](https://github.com/ethereum/wiki/wiki/RLP)), various cryptography functions and primatives, and lots of other helpful utilities.
+
+Cava is developed for JDK 1.8 or higher, and depends on various other FOSS libraries, including Guava.
+
+## Getting cava
+
+> Note that these libraries are experimental and are subject to change.
+
+The libraries are published to [ConsenSys bintray repository](https://consensys.bintray.com/consensys/), synced to JCenter and Maven Central.
+
+You can import all modules using the cava jar.
+
+With Maven:
+```xml
+<dependency>
+  <groupId>net.consensys.cava</groupId>
+  <artifactId>cava</artifactId>
+  <version>0.6.0</version>
+</dependency>
+```
+
+With Gradle: `compile 'net.consensys.cava:cava:0.6.0'`
+
+[PACKAGES.md](PACKAGES.md) contains the list of modules and instructions to import them separately.
+
+## Build Instructions
+
+To build, clone this repo and run with `./gradlew` like so:
+
+```sh
+git clone --recursive https://github.com/ConsenSys/cava
+cd cava
+./gradlew
+```
+
+After a successful build, libraries will be available in `build/libs`.
+
+## Links
+
+- [GitHub project](https://github.com/ConsenSys/cava)
+- [Online Kotlin documentation](https://consensys.github.io/cava/docs/kotlin/0.6.0/cava)
+- [Online Java documentation](https://consensys.github.io/cava/docs/java/0.6.0)
+- [Issue tracker: Report a defect or feature request](https://github.com/ConsenSys/cava/issues/new)
+- [StackOverflow: Ask "how-to" and "why-didn't-it-work" questions](https://stackoverflow.com/questions/ask?tags=cava+java)
+- [cava-discuss: For open-ended questions and discussion](http://groups.google.com/group/cava-discuss)
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..94eaa36
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,528 @@
+import java.util.regex.Pattern
+import net.ltgt.gradle.errorprone.CheckSeverity
+
+buildscript {
+  repositories {
+    maven { url 'https://consensys.bintray.com/consensys/' }
+    jcenter()
+  }
+}
+plugins {
+  id 'com.diffplug.gradle.spotless' version '3.16.0'
+  id 'net.ltgt.errorprone' version '0.6'
+  id 'io.spring.dependency-management' version '1.0.6.RELEASE'
+  id 'com.github.hierynomus.license' version '0.15.0'
+  id 'com.jfrog.bintray' version '1.8.3'
+  id 'org.jetbrains.kotlin.jvm' version '1.3.20'
+  id 'org.jetbrains.dokka' version '0.9.17'
+}
+
+description = 'A set of libraries and other tools to aid development of blockchain and other decentralized software in Java and other JVM languages'
+
+
+//////
+// Sanity checks
+
+if (!file("${rootDir}/eth-reference-tests/src/test/resources/tests/README.md").exists()) {
+  throw new GradleException("eth-reference-tests/src/test/resources/tests/README.md missing: please clone submodules (git submodule update --init --recursive)")
+}
+if (!file("${rootDir}/eth-reference-tests/src/test/resources/eth2.0-tests/README.md").exists()) {
+  throw new GradleException("eth-reference-tests/src/test/resources/eth2.0-tests/README.md missing: please clone submodules (git submodule update --init --recursive)")
+}
+
+
+//////
+// Version numbering
+
+def versionNumber = '1.0.0'
+def buildVersion = versionNumber + buildTag()
+
+static String buildTag() {
+  if (System.getenv('BUILD_RELEASE') == 'true') {
+    return ''
+  }
+  if (System.getenv('CIRCLECI')) {
+    def buildNumber = System.getenv('CIRCLE_SHA1').take(4).toUpperCase() +
+      String.format('%02X', System.getenv('CIRCLE_BUILD_NUM').toInteger() % 256, 16)
+    return '-' + buildNumber + '-snapshot'
+  }
+  return '-dev'
+}
+
+
+//////
+// Default tasks and build aliases
+
+defaultTasks 'checkLicenses', 'spotlessCheck', 'jar', 'test', ':javadoc'
+
+def buildAliases = ['dev': [
+    'spotlessApply',
+    'checkLicenses',
+    ':jar',
+    'test',
+    ':javadoc'
+  ]]
+
+def expandedTaskList = []
+gradle.startParameter.taskNames.each {
+  expandedTaskList << (buildAliases[it] ? buildAliases[it] : it)
+}
+gradle.startParameter.taskNames = expandedTaskList.flatten()
+
+
+//////
+// Gradle script formatting
+
+spotless {
+  groovyGradle {
+    target '**/*.gradle'
+    greclipse().configFile(rootProject.file('gradle/greclipse-gradle-consensys-style.properties'))
+    endWithNewline()
+  }
+}
+
+
+subprojects {
+
+  //////
+  // Source formatting
+
+  apply plugin: 'com.diffplug.gradle.spotless'
+  spotless {
+    java {
+      target project.fileTree(project.projectDir) {
+        include '**/*.java'
+        exclude '**/generated-src/**/*.*'
+      }
+      removeUnusedImports()
+      licenseHeaderFile rootProject.file('gradle/spotless.license.java')
+      eclipse().configFile(rootProject.file('gradle/eclipse-java-consensys-style.xml'))
+      importOrder 'net.consensys', 'java', ''
+      endWithNewline()
+    }
+    kotlin {
+      licenseHeaderFile rootProject.file('gradle/spotless.license.java')
+      ktlint().userData(['indent_size': '2', 'continuation_indent_size' : '2', 'max_line_length': '120'])
+      endWithNewline()
+    }
+  }
+
+
+  //////
+  // Parallel build execution
+
+  tasks.withType(Test) {
+    // If GRADLE_MAX_TEST_FORKS is not set, use half the available processors
+    maxParallelForks = (System.getenv('GRADLE_MAX_TEST_FORKS') ?:
+      (Runtime.runtime.availableProcessors().intdiv(2) ?: 1)).toInteger()
+  }
+
+  tasks.withType(JavaCompile) {
+    options.fork = true
+    options.incremental = true
+    options.encoding = 'UTF-8'
+  }
+
+  task allDependencies(type: DependencyReportTask) {}
+}
+
+
+//////
+// Top-level target for deploy (bintrayUpload depends on it)
+
+task deploy() {}
+
+configurations.archives.artifacts.removeAll { PublishArtifact publishArtifact ->
+  (publishArtifact.type == 'jar' && publishArtifact.name == 'cava')}
+
+
+//////
+// Project defaults
+
+allprojects {
+  apply plugin: 'java-library'
+  apply plugin: 'kotlin'
+  apply plugin: 'io.spring.dependency-management'
+  apply plugin: 'jacoco'
+  apply plugin: 'net.ltgt.errorprone'
+  apply plugin: 'com.jfrog.bintray'
+  apply plugin: 'maven-publish'
+  apply plugin: 'org.jetbrains.dokka'
+  apply plugin: 'signing'
+  apply from: "${rootDir}/dependency-versions.gradle"
+  apply from: "${rootDir}/gradle/check-licenses.gradle"
+
+  version = buildVersion
+
+  repositories { jcenter() }
+
+
+  //////
+  // Compiler arguments
+
+  sourceCompatibility = '1.8'
+  targetCompatibility = '1.8'
+
+  jacoco { toolVersion = '0.8.2' }
+
+  dependencies {
+    errorprone 'com.google.errorprone:error_prone_core'
+    if (JavaVersion.current().isJava8()) {
+      errorproneJavac("com.google.errorprone:javac")
+    }
+  }
+
+  tasks.withType(JavaCompile) {
+    // Until https://github.com/gradle/gradle/issues/2510 is resolved and
+    // something like a `releaseCompability` property is added, we have to add
+    // the --release flag explicitly
+    if (JavaVersion.current() > JavaVersion.VERSION_1_8) {
+      options.compilerArgs.addAll(['--release', '8'])
+    }
+    options.compilerArgs += [
+      '-Xlint:unchecked',
+      '-Xlint:cast',
+      '-Xlint:rawtypes',
+      '-Xlint:overloads',
+      '-Xlint:divzero',
+      '-Xlint:finally',
+      '-Xlint:static',
+      '-Werror'
+    ]
+
+    options.errorprone {
+      excludedPaths '.*/generated-src/.*'
+      check('FutureReturnValueIgnored', CheckSeverity.OFF)
+      check('UnnecessaryParentheses', CheckSeverity.OFF)
+      disableWarningsInGeneratedCode = true
+    }
+  }
+
+  tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+    kotlinOptions {
+      jvmTarget = "1.8"
+      allWarningsAsErrors = true
+      freeCompilerArgs = [
+        '-Xjsr305=strict',
+        '-Xjvm-default=enable',
+        '-Xuse-experimental=kotlin.Experimental'
+      ]
+    }
+  }
+
+
+  //////
+  // Use JUnit5 for testing
+
+  test { useJUnitPlatform() { includeEngines 'spek', 'junit-jupiter' } }
+
+
+  //////
+  // Documentation
+
+  dokka {
+    outputFormat = 'html'
+    outputDirectory = "$buildDir/docs/dokka"
+    jdkVersion = 8
+    includeNonPublic = false
+    def relativePath = rootDir.toPath().relativize(projectDir.toPath()).toString()
+    linkMapping {
+      dir = projectDir.toString()
+      url = "https://github.com/consensys/cava/blob/master/$relativePath"
+      suffix = "#L"
+    }
+  }
+
+
+  //////
+  // Artifact locations
+
+  jar {
+    destinationDir = file("${rootProject.buildDir}/libs")
+  }
+
+  task sourcesJar(type: Jar, dependsOn: classes) {
+    destinationDir = file("${rootProject.buildDir}/src")
+    classifier = 'sources'
+    from sourceSets.main.allSource
+  }
+
+  task javadocJar(type: Jar, dependsOn: javadoc) {
+    destinationDir = file("${rootProject.buildDir}/docs")
+    classifier = 'javadoc'
+    from javadoc.destinationDir
+  }
+
+  task dokkaJar(type: Jar, dependsOn: dokka) {
+    destinationDir = file("${rootProject.buildDir}/docs")
+    classifier = 'dokka'
+    from dokka.outputDirectory
+  }
+
+
+  //////
+  // Packaging and deployment
+
+  tasks.withType(Jar) {
+    if (rootProject == project) {
+      baseName = project.name
+    } else {
+      baseName = rootProject.name + '-' + project.name
+    }
+    manifest {
+      attributes('Implementation-Title': baseName,
+      'Implementation-Version': project.version)
+    }
+  }
+
+  if (project.name != 'eth-reference-tests') {
+
+    artifacts {
+      if (project != rootProject) {
+        archives jar
+        archives sourcesJar
+      }
+      archives dokkaJar
+    }
+
+    signing {
+      useGpgCmd()
+      sign configurations.archives
+    }
+
+    publishing {
+      publications {
+        MavenDeployment(MavenPublication) { publication ->
+          if (project != rootProject) {
+            from components.java
+            artifact sourcesJar { classifier 'sources' }
+          }
+          artifact dokkaJar { classifier 'javadoc' }
+          groupId 'net.consensys.cava'
+          artifactId project.jar.baseName
+          version project.version
+
+          pom {
+            name = project.jar.baseName
+            afterEvaluate { description = project.description }
+            url = 'https://github.com/ConsenSys/cava'
+            licenses {
+              license {
+                name = "The Apache License, Version 2.0"
+                url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+              }
+            }
+            scm {
+              connection = 'scm:https://github.com/ConsenSys/cava.git'
+              developerConnection = 'scm:git@github.com:ConsenSys/cava.git'
+              url = 'https://github.com/ConsenSys/cava'
+            }
+            developers {
+              developer {
+                name = 'Chris Leishman'
+                email = 'chris@leishman.org'
+                organization = 'ConsenSys'
+                organizationUrl = 'https://www.consensys.net'
+              }
+              developer {
+                name = 'Antoine Toulme'
+                email = 'antoine@lunar-ocean.com'
+                organization = 'ConsenSys'
+                organizationUrl = 'https://www.consensys.net'
+              }
+            }
+          }
+          pom.withXml {
+            // use inline versions rather than pom dependency management
+            asNode().remove(asNode().dependencyManagement[0])
+
+            def dependenciesNode = asNode().appendNode('dependencies')
+            def addDependencyNode = { dep, optional ->
+              def dependencyNode = dependenciesNode.appendNode('dependency')
+              if (dep instanceof ProjectDependency) {
+                dependencyNode.appendNode('groupId', 'net.consensys.cava')
+                dependencyNode.appendNode('artifactId', rootProject.name + '-' + dep.name)
+                dependencyNode.appendNode('version', dep.version)
+              } else {
+                dependencyNode.appendNode('groupId', dep.group)
+                dependencyNode.appendNode('artifactId', dep.name)
+                if (dep.version != null) {
+                  dependencyNode.appendNode('version', dep.version)
+                } else {
+                  def version = dependencyManagement.managedVersions["$dep.group:$dep.name"]
+                  dependencyNode.appendNode('version', version)
+                }
+              }
+              if (optional) {
+                dependencyNode.appendNode('optional', 'true')
+              }
+
+              def ers = dep.excludeRules
+              if (!ers.empty) {
+                def exclusionsNode = dependencyNode.appendNode('exclusions')
+                ers.each { er ->
+                  def exclusionNode = exclusionsNode.appendNode('exclusion')
+                  exclusionNode.appendNode('groupId', er.group)
+                  exclusionNode.appendNode('artifactId', er.module)
+                }
+              }
+            }
+
+            configurations.compile.allDependencies.each { dep ->
+              addDependencyNode(dep, false)
+            }
+            configurations.compileOnly.allDependencies.each { dep ->
+              addDependencyNode(dep, true)
+            }
+
+            if (System.getenv('ENABLE_SIGNING') == 'true') {
+              def pomFile = file("${project.buildDir}/generated-pom.xml")
+              writeTo(pomFile)
+              def pomAscFile = signing.sign(pomFile).signatureFiles[0]
+              artifact(pomAscFile) {
+                classifier = null
+                extension = 'pom.asc'
+              }
+            }
+          }
+
+          if (System.getenv('ENABLE_SIGNING') == 'true') {
+            // create the signed artifacts
+            tasks.signArchives.signatureFiles.each {
+              artifact(it) {
+                def matcher = it.file =~ /-(sources|javadoc)\.jar\.asc$/
+                if (matcher.find()) {
+                  classifier = matcher.group(1)
+                } else {
+                  classifier = null
+                }
+                extension = 'jar.asc'
+              }
+            }
+          }
+        }
+      }
+    }
+
+    tasks.withType(Sign) {
+      onlyIf {
+        System.getenv('ENABLE_SIGNING') == 'true'
+      }
+    }
+
+    model {
+      tasks.generatePomFileForMavenDeploymentPublication {
+        destination = file("$buildDir/generated-pom.xml")
+      }
+      tasks.publishMavenDeploymentPublicationToMavenLocal { dependsOn project.tasks.signArchives }
+    }
+
+    def artifactIdMatcher = Pattern.compile("(.*)-\\d.*")
+    bintray {
+      user = System.getenv('BINTRAY_USER')
+      key = System.getenv('BINTRAY_KEY')
+      publications = ['MavenDeployment']
+      filesSpec {
+        project.extensions.getByType(PublishingExtension).publications.all { publication ->
+          publication.getArtifacts().all {
+            def ascFile = new File(it.file.getParentFile(), it.file.getName() + '.asc')
+            if (ascFile.exists()) {
+              def matcher = artifactIdMatcher.matcher(it.file.getName())
+              matcher.find()
+              def artifactId = matcher.group(1)
+              from ascFile.getAbsolutePath()
+              into publication.groupId.replaceAll('\\.', '/') + '/' + artifactId + '/' + publication.version + '/'
+            }
+          }
+        }
+      }
+      dryRun = !(System.getenv('BINTRAY_DEPLOY') == 'true')
+      publish = true
+      pkg {
+        repo = 'consensys'
+        name = 'cava'
+        userOrg = 'consensys'
+        licenses = ['Apache-2.0']
+        version {
+          name = project.version
+          desc = 'Cava distribution'
+          released = new Date()
+          vcsTag = project.version
+        }
+      }
+    }
+    deploy.dependsOn bintrayUpload
+  }
+}
+
+
+//////
+// Configure root project as a virtual package that depends on all components
+
+dependencies {
+  subprojects.each { p ->
+    switch (p.name) {
+      case 'eth-reference-tests':
+      // ignore
+        break
+      case 'crypto':
+        compile(p) {
+          exclude group: 'com.github.jnr', module: 'jnr-ffi'
+        }
+        break
+      default:
+        compile p
+        break
+    }
+  }
+}
+
+jar { enabled = false }
+
+javadoc {
+  subprojects.each {
+    source += it.javadoc.source
+    classpath += it.javadoc.classpath
+  }
+}
+
+dokka {
+  moduleName = rootProject.name
+  subprojects.each {
+    dependsOn it.classes
+    it.sourceSets.main.output.each { d ->
+      if (d.exists()) {
+        classpath += d
+      }
+    }
+  }
+  sourceDirs = files(subprojects.collect {
+    return [
+      new File(it.projectDir, '/src/main/kotlin'),
+      new File(it.projectDir, '/src/main/java')
+    ]
+  })
+  linkMapping {
+    dir = rootDir.toString()
+    url = "https://github.com/consensys/cava/blob/master"
+    suffix = "#L"
+  }
+
+  includes = ['PACKAGES.md']
+
+  externalDocumentationLink {
+    url = new URL("https://docs.oracle.com/javase/8/docs/api/")
+  }
+
+  externalDocumentationLink {
+    url = new URL('https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/')
+  }
+}
+
+dokkaJar {
+  baseName = rootProject.name
+  manifest {
+    attributes('Implementation-Title': baseName,
+    'Implementation-Version': project.version)
+  }
+}
diff --git a/bytes/build.gradle b/bytes/build.gradle
new file mode 100644
index 0000000..aefc867
--- /dev/null
+++ b/bytes/build.gradle
@@ -0,0 +1,12 @@
+description = 'Classes and utilities for working with byte arrays.'
+
+dependencies {
+  compile 'com.google.guava:guava'
+  compileOnly 'io.vertx:vertx-core'
+
+  testCompile 'io.vertx:vertx-core'
+  testCompile 'org.junit.jupiter:junit-jupiter-api'
+  testCompile 'org.junit.jupiter:junit-jupiter-params'
+
+  testRuntime 'org.junit.jupiter:junit-jupiter-engine'
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/AbstractBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/AbstractBytes.java
new file mode 100644
index 0000000..614962f
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/AbstractBytes.java
@@ -0,0 +1,67 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+/**
+ * An abstract {@link Bytes} value that provides implementations of {@link #equals(Object)}, {@link #hashCode()} and
+ * {@link #toString()}.
+ */
+public abstract class AbstractBytes implements Bytes {
+
+  static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray();
+
+  /**
+   * Compare this value and the provided one for equality.
+   *
+   * <p>
+   * Two {@link Bytes} values are equal is they have contain the exact same bytes.
+   *
+   * @param obj The object to test for equality with.
+   * @return {@code true} if this value and {@code obj} are equal.
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof Bytes)) {
+      return false;
+    }
+
+    Bytes other = (Bytes) obj;
+    if (this.size() != other.size()) {
+      return false;
+    }
+
+    for (int i = 0; i < size(); i++) {
+      if (this.get(i) != other.get(i)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = 1;
+    for (int i = 0; i < size(); i++) {
+      result = 31 * result + get(i);
+    }
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return toHexString();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes.java
new file mode 100644
index 0000000..2fa1c29
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes.java
@@ -0,0 +1,194 @@
+/*
+ * 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 net.consensys.cava.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.Arrays;
+
+import io.vertx.core.buffer.Buffer;
+
+class ArrayWrappingBytes extends AbstractBytes {
+
+  protected final byte[] bytes;
+  protected final int offset;
+  protected final int length;
+
+  ArrayWrappingBytes(byte[] bytes) {
+    this(bytes, 0, bytes.length);
+  }
+
+  ArrayWrappingBytes(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 = bytes;
+    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());
+    return bytes[offset + i];
+  }
+
+  @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);
+
+    return length == Bytes32.SIZE ? new ArrayWrappingBytes32(bytes, offset + i)
+        : new ArrayWrappingBytes(bytes, offset + i, length);
+  }
+
+  // MUST be overridden by mutable implementations
+  @Override
+  public Bytes copy() {
+    if (offset == 0 && length == bytes.length) {
+      return this;
+    }
+    return new ArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return new MutableArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public int commonPrefixLength(Bytes other) {
+    if (!(other instanceof ArrayWrappingBytes)) {
+      return super.commonPrefixLength(other);
+    }
+    ArrayWrappingBytes o = (ArrayWrappingBytes) other;
+    int i = 0;
+    while (i < length && i < o.length && bytes[offset + i] == o.bytes[o.offset + i]) {
+      i++;
+    }
+    return i;
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    digest.update(bytes, 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(bytes, offset, d.bytes, d.offset + destinationOffset, size);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    byteBuffer.put(bytes, offset, length);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    buffer.appendBytes(bytes, offset, length);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof ArrayWrappingBytes)) {
+      return super.equals(obj);
+    }
+    ArrayWrappingBytes other = (ArrayWrappingBytes) obj;
+    if (length != other.length) {
+      return false;
+    }
+    for (int i = 0; i < length; ++i) {
+      if (bytes[offset + i] != other.bytes[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 + bytes[offset + i];
+    }
+    return result;
+  }
+
+  @Override
+  public byte[] toArray() {
+    return Arrays.copyOfRange(bytes, offset, offset + length);
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    if (offset == 0 && length == bytes.length) {
+      return bytes;
+    }
+    return toArray();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes32.java
new file mode 100644
index 0000000..fa60064
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes32.java
@@ -0,0 +1,56 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class ArrayWrappingBytes32 extends ArrayWrappingBytes implements Bytes32 {
+
+  ArrayWrappingBytes32(byte[] bytes) {
+    this(checkLength(bytes), 0);
+  }
+
+  ArrayWrappingBytes32(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() {
+    if (offset == 0 && length == bytes.length) {
+      return this;
+    }
+    return new ArrayWrappingBytes32(toArray());
+  }
+
+  @Override
+  public MutableBytes32 mutableCopy() {
+    return new MutableArrayWrappingBytes32(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes48.java
new file mode 100644
index 0000000..8f33bae
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes48.java
@@ -0,0 +1,56 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class ArrayWrappingBytes48 extends ArrayWrappingBytes implements Bytes48 {
+
+  ArrayWrappingBytes48(byte[] bytes) {
+    this(checkLength(bytes), 0);
+  }
+
+  ArrayWrappingBytes48(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 Bytes48 copy() {
+    if (offset == 0 && length == bytes.length) {
+      return this;
+    }
+    return new ArrayWrappingBytes48(toArray());
+  }
+
+  @Override
+  public MutableBytes48 mutableCopy() {
+    return new MutableArrayWrappingBytes48(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/BufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/BufferWrappingBytes.java
new file mode 100644
index 0000000..197937d
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/BufferWrappingBytes.java
@@ -0,0 +1,109 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import io.vertx.core.buffer.Buffer;
+
+class BufferWrappingBytes extends AbstractBytes {
+
+  protected final Buffer buffer;
+
+  BufferWrappingBytes(Buffer buffer) {
+    this.buffer = buffer;
+  }
+
+  BufferWrappingBytes(Buffer buffer, int offset, int length) {
+    checkArgument(length >= 0, "Invalid negative length");
+    int bufferLength = buffer.length();
+    checkElementIndex(offset, bufferLength + 1);
+    checkArgument(
+        offset + length <= bufferLength,
+        "Provided length %s is too big: the buffer has size %s and has only %s bytes from %s",
+        length,
+        bufferLength,
+        bufferLength - offset,
+        offset);
+
+    if (offset == 0 && length == bufferLength) {
+      this.buffer = buffer;
+    } else {
+      this.buffer = buffer.slice(offset, offset + length);
+    }
+  }
+
+  @Override
+  public int size() {
+    return buffer.length();
+  }
+
+  @Override
+  public byte get(int i) {
+    return buffer.getByte(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return buffer.getInt(i);
+  }
+
+  @Override
+  public long getLong(int i) {
+    return buffer.getLong(i);
+  }
+
+  @Override
+  public Bytes slice(int i, int length) {
+    int size = buffer.length();
+    if (i == 0 && length == size) {
+      return this;
+    }
+    if (length == 0) {
+      return Bytes.EMPTY;
+    }
+
+    checkElementIndex(i, size);
+    checkArgument(
+        i + length <= size,
+        "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+        length,
+        size,
+        size - i,
+        i);
+
+    return new BufferWrappingBytes(buffer.slice(i, i + length));
+  }
+
+  // MUST be overridden by mutable implementations
+  @Override
+  public Bytes copy() {
+    return Bytes.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return MutableBytes.wrap(toArray());
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    buffer.appendBuffer(this.buffer);
+  }
+
+  @Override
+  public byte[] toArray() {
+    return buffer.getBytes();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ByteBufWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufWrappingBytes.java
new file mode 100644
index 0000000..4031f38
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufWrappingBytes.java
@@ -0,0 +1,113 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import io.netty.buffer.ByteBuf;
+import io.vertx.core.buffer.Buffer;
+
+class ByteBufWrappingBytes extends AbstractBytes {
+
+  protected final ByteBuf byteBuf;
+
+  ByteBufWrappingBytes(ByteBuf byteBuf) {
+    this.byteBuf = byteBuf;
+  }
+
+  ByteBufWrappingBytes(ByteBuf byteBuf, int offset, int length) {
+    checkArgument(length >= 0, "Invalid negative length");
+    int bufferLength = byteBuf.capacity();
+    checkElementIndex(offset, bufferLength + 1);
+    checkArgument(
+        offset + length <= bufferLength,
+        "Provided length %s is too big: the buffer has size %s and has only %s bytes from %s",
+        length,
+        bufferLength,
+        bufferLength - offset,
+        offset);
+
+    if (offset == 0 && length == bufferLength) {
+      this.byteBuf = byteBuf;
+    } else {
+      this.byteBuf = byteBuf.slice(offset, length);
+    }
+  }
+
+  @Override
+  public int size() {
+    return byteBuf.capacity();
+  }
+
+  @Override
+  public byte get(int i) {
+    return byteBuf.getByte(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return byteBuf.getInt(i);
+  }
+
+  @Override
+  public long getLong(int i) {
+    return byteBuf.getLong(i);
+  }
+
+  @Override
+  public Bytes slice(int i, int length) {
+    int size = byteBuf.capacity();
+    if (i == 0 && length == size) {
+      return this;
+    }
+    if (length == 0) {
+      return Bytes.EMPTY;
+    }
+
+    checkElementIndex(i, size);
+    checkArgument(
+        i + length <= size,
+        "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+        length,
+        size,
+        size - i,
+        i);
+
+    return new ByteBufWrappingBytes(byteBuf.slice(i, length));
+  }
+
+  // MUST be overridden by mutable implementations
+  @Override
+  public Bytes copy() {
+    return Bytes.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return MutableBytes.wrap(toArray());
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    buffer.appendBuffer(Buffer.buffer(this.byteBuf));
+  }
+
+  @Override
+  public byte[] toArray() {
+    int size = byteBuf.capacity();
+    byte[] array = new byte[size];
+    byteBuf.getBytes(0, array);
+    return array;
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ByteBufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufferWrappingBytes.java
new file mode 100644
index 0000000..53547da
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufferWrappingBytes.java
@@ -0,0 +1,129 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+class ByteBufferWrappingBytes extends AbstractBytes {
+
+  protected final ByteBuffer byteBuffer;
+  protected final int offset;
+  protected final int length;
+
+  ByteBufferWrappingBytes(ByteBuffer byteBuffer) {
+    this(byteBuffer, 0, byteBuffer.limit());
+  }
+
+  ByteBufferWrappingBytes(ByteBuffer byteBuffer, int offset, int length) {
+    checkArgument(length >= 0, "Invalid negative length");
+    int bufferLength = byteBuffer.capacity();
+    if (bufferLength > 0) {
+      checkElementIndex(offset, bufferLength);
+    }
+    checkArgument(
+        offset + length <= bufferLength,
+        "Provided length %s is too big: the value has only %s bytes from offset %s",
+        length,
+        bufferLength - offset,
+        offset);
+
+    this.byteBuffer = byteBuffer;
+    this.offset = offset;
+    this.length = length;
+  }
+
+  @Override
+  public int size() {
+    return length;
+  }
+
+  @Override
+  public int getInt(int i) {
+    return byteBuffer.getInt(offset + i);
+  }
+
+  @Override
+  public long getLong(int i) {
+    return byteBuffer.getLong(offset + i);
+  }
+
+  @Override
+  public byte get(int i) {
+    return byteBuffer.get(offset + i);
+  }
+
+  @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);
+
+    return new ByteBufferWrappingBytes(byteBuffer, offset + i, length);
+  }
+
+  // MUST be overridden by mutable implementations
+  @Override
+  public Bytes copy() {
+    if (offset == 0 && length == byteBuffer.limit()) {
+      return this;
+    }
+    return new ArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return new MutableArrayWrappingBytes(toArray());
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    byteBuffer.put(this.byteBuffer);
+  }
+
+  @Override
+  public byte[] toArray() {
+    if (!byteBuffer.hasArray()) {
+      return super.toArray();
+    }
+    int arrayOffset = byteBuffer.arrayOffset();
+    return Arrays.copyOfRange(byteBuffer.array(), arrayOffset + offset, arrayOffset + offset + length);
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    if (!byteBuffer.hasArray()) {
+      return toArray();
+    }
+    byte[] array = byteBuffer.array();
+    if (array.length != length || byteBuffer.arrayOffset() != 0) {
+      return toArray();
+    }
+    return array;
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/Bytes.java b/bytes/src/main/java/net/consensys/cava/bytes/Bytes.java
new file mode 100644
index 0000000..40340a0
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/Bytes.java
@@ -0,0 +1,1466 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ReadOnlyBufferException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Random;
+
+import io.netty.buffer.ByteBuf;
+import io.vertx.core.buffer.Buffer;
+
+/**
+ * A value made of bytes.
+ *
+ * <p>
+ * This interface makes no thread-safety guarantee, and a {@link Bytes} value is generally not thread safe. However,
+ * specific implementations may be thread-safe. For instance, the value returned by {@link #copy} is guaranteed to be
+ * thread-safe as it is immutable.
+ */
+public interface Bytes {
+
+  /**
+   * The empty value (with 0 bytes).
+   */
+  Bytes EMPTY = wrap(new byte[0]);
+
+  /**
+   * Wrap the provided byte array as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that value is not copied and thus any future update to {@code value} will be reflected in the returned value.
+   *
+   * @param value The value to wrap.
+   * @return A {@link Bytes} value wrapping {@code value}.
+   */
+  static Bytes wrap(byte[] value) {
+    return wrap(value, 0, value.length);
+  }
+
+  /**
+   * Wrap a slice of a byte array as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that value is not copied and thus any future update to {@code value} within the slice will be reflected in the
+   * returned value.
+   *
+   * @param value The value to wrap.
+   * @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 expose the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + length} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length > 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > value.length}.
+   */
+  static Bytes wrap(byte[] value, int offset, int length) {
+    checkNotNull(value);
+    if (length == 32) {
+      return new ArrayWrappingBytes32(value, offset);
+    }
+    return new ArrayWrappingBytes(value, offset, length);
+  }
+
+  /**
+   * Wrap a list of other values into a concatenated view.
+   *
+   * <p>
+   * Note that the values are not copied and thus any future update to the values will be reflected in the returned
+   * value. If copying the inputs is desired, use {@link #concatenate(Bytes...)}.
+   *
+   * @param values The values to wrap.
+   * @return A value representing a view over the concatenation of all {@code values}.
+   * @throws IllegalArgumentException if the result overflows an int.
+   */
+  static Bytes wrap(Bytes... values) {
+    return ConcatenatedBytes.wrap(values);
+  }
+
+  /**
+   * Create a value containing the concatenation of the values provided.
+   *
+   * @param values The values to copy and concatenate.
+   * @return A value containing the result of concatenating the value from {@code values} in their provided order.
+   * @throws IllegalArgumentException if the result overflows an int.
+   */
+  static Bytes concatenate(Bytes... values) {
+    if (values.length == 0) {
+      return EMPTY;
+    }
+
+    int size;
+    try {
+      size = Arrays.stream(values).mapToInt(Bytes::size).reduce(0, Math::addExact);
+    } catch (ArithmeticException e) {
+      throw new IllegalArgumentException("Combined length of values is too long (> Integer.MAX_VALUE)");
+    }
+
+    MutableBytes result = MutableBytes.create(size);
+    int offset = 0;
+    for (Bytes value : values) {
+      value.copyTo(result, offset);
+      offset += value.size();
+    }
+    return result;
+  }
+
+  /**
+   * Wrap a full Vert.x {@link Buffer} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param buffer The buffer to wrap.
+   * @return A {@link Bytes} value.
+   */
+  static Bytes wrapBuffer(Buffer buffer) {
+    checkNotNull(buffer);
+    if (buffer.length() == 0) {
+      return EMPTY;
+    }
+    return new BufferWrappingBytes(buffer);
+  }
+
+  /**
+   * Wrap a slice of a Vert.x {@link Buffer} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param buffer The buffer to wrap.
+   * @param offset The offset in {@code buffer} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link Bytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (buffer.length() > 0 && offset >=
+   *     buffer.length())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > buffer.length()}.
+   */
+  static Bytes wrapBuffer(Buffer buffer, int offset, int size) {
+    checkNotNull(buffer);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new BufferWrappingBytes(buffer, offset, size);
+  }
+
+  /**
+   * Wrap a full Netty {@link ByteBuf} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the byteBuf may be reflected in the returned value.
+   *
+   * @param byteBuf The {@link ByteBuf} to wrap.
+   * @return A {@link Bytes} value.
+   */
+  static Bytes wrapByteBuf(ByteBuf byteBuf) {
+    checkNotNull(byteBuf);
+    if (byteBuf.capacity() == 0) {
+      return EMPTY;
+    }
+    return new ByteBufWrappingBytes(byteBuf);
+  }
+
+  /**
+   * Wrap a slice of a Netty {@link ByteBuf} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param byteBuf The {@link ByteBuf} to wrap.
+   * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link Bytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (byteBuf.capacity() > 0 && offset >=
+   *     byteBuf.capacity())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > byteBuf.capacity()}.
+   */
+  static Bytes wrapByteBuf(ByteBuf byteBuf, int offset, int size) {
+    checkNotNull(byteBuf);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new ByteBufWrappingBytes(byteBuf, offset, size);
+  }
+
+  /**
+   * Wrap a full Java NIO {@link ByteBuffer} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the byteBuf may be reflected in the returned value.
+   *
+   * @param byteBuffer The {@link ByteBuffer} to wrap.
+   * @return A {@link Bytes} value.
+   */
+  static Bytes wrapByteBuffer(ByteBuffer byteBuffer) {
+    checkNotNull(byteBuffer);
+    if (byteBuffer.limit() == 0) {
+      return EMPTY;
+    }
+    return new ByteBufferWrappingBytes(byteBuffer);
+  }
+
+  /**
+   * Wrap a slice of a Java NIO {@link ByteBuf} as a {@link Bytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param byteBuffer The {@link ByteBuffer} to wrap.
+   * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link Bytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (byteBuffer.limit() > 0 && offset >=
+   *     byteBuf.limit())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > byteBuffer.limit()}.
+   */
+  static Bytes wrapByteBuffer(ByteBuffer byteBuffer, int offset, int size) {
+    checkNotNull(byteBuffer);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new ByteBufferWrappingBytes(byteBuffer, offset, size);
+  }
+
+  /**
+   * Create a value that contains the specified bytes in their specified order.
+   *
+   * @param bytes The bytes that must compose the returned value.
+   * @return A value containing the specified bytes.
+   */
+  static Bytes of(byte... bytes) {
+    return wrap(bytes);
+  }
+
+  /**
+   * Create a value that contains the specified bytes in their specified order.
+   *
+   * @param bytes The bytes.
+   * @return A value containing bytes are the one from {@code bytes}.
+   * @throws IllegalArgumentException if any of the specified would be truncated when storing as a byte.
+   */
+  static Bytes of(int... bytes) {
+    byte[] result = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      int b = bytes[i];
+      checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b);
+      result[i] = (byte) b;
+    }
+    return Bytes.wrap(result);
+  }
+
+  /**
+   * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
+   *
+   * @param value The value, which must be no larger than an unsigned short.
+   * @return A 2 bytes value corresponding to {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 2-byte short
+   *         (that is, if {@code value >= (1 << 16)}).
+   */
+  static Bytes ofUnsignedShort(int value) {
+    return ofUnsignedShort(value, BIG_ENDIAN);
+  }
+
+  /**
+   * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
+   *
+   * @param value The value, which must be no larger than an unsigned short.
+   * @param order The byte-order for the integer encoding.
+   * @return A 2 bytes value corresponding to {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 2-byte short
+   *         (that is, if {@code value >= (1 << 16)}).
+   */
+  static Bytes ofUnsignedShort(int value, ByteOrder order) {
+    checkArgument(
+        value >= 0 && value <= BytesValues.MAX_UNSIGNED_SHORT,
+        "Value %s cannot be represented as an unsigned short (it is negative or too big)",
+        value);
+    byte[] res = new byte[2];
+    if (order == BIG_ENDIAN) {
+      res[0] = (byte) ((value >> 8) & 0xFF);
+      res[1] = (byte) (value & 0xFF);
+    } else {
+      res[0] = (byte) (value & 0xFF);
+      res[1] = (byte) ((value >> 8) & 0xFF);
+    }
+    return Bytes.wrap(res);
+  }
+
+  /**
+   * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
+   *
+   * @param value The value, which must be no larger than an unsigned int.
+   * @return A 4 bytes value corresponding to {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 4-byte int
+   *         (that is, if {@code value >= (1L << 32)}).
+   */
+  static Bytes ofUnsignedInt(long value) {
+    return ofUnsignedInt(value, BIG_ENDIAN);
+  }
+
+  /**
+   * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
+   *
+   * @param value The value, which must be no larger than an unsigned int.
+   * @param order The byte-order for the integer encoding.
+   * @return A 4 bytes value corresponding to the encoded {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 4-byte int
+   *         (that is, if {@code value >= (1L << 32)}).
+   */
+  static Bytes ofUnsignedInt(long value, ByteOrder order) {
+    checkArgument(
+        value >= 0 && value <= BytesValues.MAX_UNSIGNED_INT,
+        "Value %s cannot be represented as an unsigned int (it is negative or too big)",
+        value);
+    byte[] res = new byte[4];
+    if (order == BIG_ENDIAN) {
+      res[0] = (byte) ((value >> 24) & 0xFF);
+      res[1] = (byte) ((value >> 16) & 0xFF);
+      res[2] = (byte) ((value >> 8) & 0xFF);
+      res[3] = (byte) ((value) & 0xFF);
+    } else {
+      res[0] = (byte) ((value) & 0xFF);
+      res[1] = (byte) ((value >> 8) & 0xFF);
+      res[2] = (byte) ((value >> 16) & 0xFF);
+      res[3] = (byte) ((value >> 24) & 0xFF);
+    }
+    return Bytes.wrap(res);
+  }
+
+  /**
+   * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
+   *
+   * @param value The value, which will be interpreted as an unsigned long.
+   * @return A 8 bytes value corresponding to {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 8-byte int
+   *         (that is, if {@code value >= (1L << 64)}).
+   */
+  static Bytes ofUnsignedLong(long value) {
+    return ofUnsignedLong(value, BIG_ENDIAN);
+  }
+
+  /**
+   * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
+   *
+   * @param value The value, which will be interpreted as an unsigned long.
+   * @param order The byte-order for the integer encoding.
+   * @return A 8 bytes value corresponding to {@code value}.
+   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 8-byte int
+   *         (that is, if {@code value >= (1L << 64)}).
+   */
+  static Bytes ofUnsignedLong(long value, ByteOrder order) {
+    byte[] res = new byte[8];
+    if (order == BIG_ENDIAN) {
+      res[0] = (byte) ((value >> 56) & 0xFF);
+      res[1] = (byte) ((value >> 48) & 0xFF);
+      res[2] = (byte) ((value >> 40) & 0xFF);
+      res[3] = (byte) ((value >> 32) & 0xFF);
+      res[4] = (byte) ((value >> 24) & 0xFF);
+      res[5] = (byte) ((value >> 16) & 0xFF);
+      res[6] = (byte) ((value >> 8) & 0xFF);
+      res[7] = (byte) (value & 0xFF);
+    } else {
+      res[0] = (byte) ((value) & 0xFF);
+      res[1] = (byte) ((value >> 8) & 0xFF);
+      res[2] = (byte) ((value >> 16) & 0xFF);
+      res[3] = (byte) ((value >> 24) & 0xFF);
+      res[4] = (byte) ((value >> 32) & 0xFF);
+      res[5] = (byte) ((value >> 40) & 0xFF);
+      res[6] = (byte) ((value >> 48) & 0xFF);
+      res[7] = (byte) ((value >> 56) & 0xFF);
+    }
+    return Bytes.wrap(res);
+  }
+
+  /**
+   * Return the smallest bytes value whose bytes correspond to the provided long. That is, the returned value may be of
+   * size less than 8 if the provided long has leading zero bytes.
+   *
+   * @param value The long from which to create the bytes value.
+   * @return The minimal bytes representation corresponding to {@code l}.
+   */
+  static Bytes minimalBytes(long value) {
+    if (value == 0) {
+      return Bytes.EMPTY;
+    }
+
+    int zeros = Long.numberOfLeadingZeros(value);
+    int resultBytes = 8 - (zeros / 8);
+
+    byte[] result = new byte[resultBytes];
+    int shift = 0;
+    for (int i = 0; i < resultBytes; i++) {
+      result[resultBytes - i - 1] = (byte) ((value >> shift) & 0xFF);
+      shift += 8;
+    }
+    return Bytes.wrap(result);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes} value.
+   *
+   * <p>
+   * This method is lenient in that {@code str} may of an odd length, in which case it will behave exactly as if it had
+   * an additional 0 in front.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation.
+   */
+  static Bytes fromHexStringLenient(CharSequence str) {
+    checkNotNull(str);
+    return BytesValues.fromHexString(str, -1, true);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes} value of the provided size.
+   *
+   * <p>
+   * This method allows for {@code str} to have an odd length, in which case it will behave exactly as if it had an
+   * additional 0 in front.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @param destinationSize The size of the returned value, which must be big enough to hold the bytes represented by
+   *        {@code str}. If it is strictly bigger those bytes from {@code str}, the returned value will be left padded
+   *        with zeros.
+   * @return A value of size {@code destinationSize} corresponding to {@code str} potentially left-padded.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation,
+   *         represents more bytes than {@code destinationSize} or {@code destinationSize &lt; 0}.
+   */
+  static Bytes fromHexStringLenient(CharSequence str, int destinationSize) {
+    checkNotNull(str);
+    checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
+    return BytesValues.fromHexString(str, destinationSize, true);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes} value.
+   *
+   * <p>
+   * This method requires that {@code str} have an even length.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, or is of
+   *         an odd length.
+   */
+  static Bytes fromHexString(CharSequence str) {
+    checkNotNull(str);
+    return BytesValues.fromHexString(str, -1, false);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes} value.
+   *
+   * <p>
+   * This method requires that {@code str} have an even length.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @param destinationSize The size of the returned value, which must be big enough to hold the bytes represented by
+   *        {@code str}. If it is strictly bigger those bytes from {@code str}, the returned value will be left padded
+   *        with zeros.
+   * @return A value of size {@code destinationSize} corresponding to {@code str} potentially left-padded.
+   * @throws IllegalArgumentException if {@code str} does correspond to a valid hexadecimal representation, or is of an
+   *         odd length.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, or is of
+   *         an odd length, or represents more bytes than {@code destinationSize} or {@code destinationSize &lt; 0}.
+   */
+  static Bytes fromHexString(CharSequence str, int destinationSize) {
+    checkNotNull(str);
+    checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
+    return BytesValues.fromHexString(str, destinationSize, false);
+  }
+
+  /**
+   * Parse a base 64 string into a {@link Bytes} value.
+   *
+   * @param str The base 64 string to parse.
+   * @return The value corresponding to {@code str}.
+   */
+  static Bytes fromBase64String(CharSequence str) {
+    return Bytes.wrap(Base64.getDecoder().decode(str.toString()));
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @param size The number of bytes to generate.
+   * @return A value containing the desired number of random bytes.
+   */
+  static Bytes random(int size) {
+    return random(size, new SecureRandom());
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @param size The number of bytes to generate.
+   * @param generator The generator for random bytes.
+   * @return A value containing the desired number of random bytes.
+   */
+  static Bytes random(int size, Random generator) {
+    byte[] array = new byte[size];
+    generator.nextBytes(array);
+    return Bytes.wrap(array);
+  }
+
+  /** @return The number of bytes this value represents. */
+  int size();
+
+  /**
+   * Retrieve a byte in this value.
+   *
+   * @param i The index of the byte to fetch within the value (0-indexed).
+   * @return The byte at index {@code i} in this value.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {i &gt;= size()}.
+   */
+  byte get(int i);
+
+  /**
+   * Retrieve the 4 bytes starting at the provided index in this value as an integer.
+   *
+   * @param i The index from which to get the int, which must less than or equal to {@code size() - 4}.
+   * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 4}.
+   */
+  default int getInt(int i) {
+    return getInt(i, BIG_ENDIAN);
+  }
+
+  /**
+   * Retrieve the 4 bytes starting at the provided index in this value as an integer.
+   *
+   * @param i The index from which to get the int, which must less than or equal to {@code size() - 4}.
+   * @param order The byte-order for decoding the integer.
+   * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 4}.
+   */
+  default int getInt(int i, ByteOrder order) {
+    int size = size();
+    checkElementIndex(i, size);
+    if (i > (size - 4)) {
+      throw new IndexOutOfBoundsException(
+          format("Value of size %s has not enough bytes to read a 4 bytes int from index %s", size, i));
+    }
+
+    int value = 0;
+    if (order == BIG_ENDIAN) {
+      value |= ((int) get(i) & 0xFF) << 24;
+      value |= ((int) get(i + 1) & 0xFF) << 16;
+      value |= ((int) get(i + 2) & 0xFF) << 8;
+      value |= ((int) get(i + 3) & 0xFF);
+    } else {
+      value |= ((int) get(i + 3) & 0xFF) << 24;
+      value |= ((int) get(i + 2) & 0xFF) << 16;
+      value |= ((int) get(i + 1) & 0xFF) << 8;
+      value |= ((int) get(i) & 0xFF);
+    }
+    return value;
+  }
+
+  /**
+   * The value corresponding to interpreting these bytes as an integer.
+   *
+   * @return An value corresponding to this value interpreted as an integer.
+   * @throws IllegalArgumentException if {@code size() &gt; 4}.
+   */
+  default int toInt() {
+    return toInt(BIG_ENDIAN);
+  }
+
+  /**
+   * The value corresponding to interpreting these bytes as an integer.
+   *
+   * @param order The byte-order for decoding the integer.
+   * @return An value corresponding to this value interpreted as an integer.
+   * @throws IllegalArgumentException if {@code size() &gt; 4}.
+   */
+  default int toInt(ByteOrder order) {
+    int size = size();
+    checkArgument(size <= 4, "Value of size %s has more than 4 bytes", size());
+    if (size == 0) {
+      return 0;
+    }
+    if (order == BIG_ENDIAN) {
+      int i = size;
+      int value = ((int) get(--i) & 0xFF);
+      if (i == 0) {
+        return value;
+      }
+      value |= ((int) get(--i) & 0xFF) << 8;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((int) get(--i) & 0xFF) << 16;
+      if (i == 0) {
+        return value;
+      }
+      return value | ((int) get(--i) & 0xFF) << 24;
+    } else {
+      int i = 0;
+      int value = ((int) get(i) & 0xFF);
+      if (++i == size) {
+        return value;
+      }
+      value |= ((int) get(i++) & 0xFF) << 8;
+      if (i == size) {
+        return value;
+      }
+      value |= ((int) get(i++) & 0xFF) << 16;
+      if (i == size) {
+        return value;
+      }
+      return value | ((int) get(i) & 0xFF) << 24;
+    }
+  }
+
+  /**
+   * Whether this value contains no bytes.
+   *
+   * @return true if the value contains no bytes
+   */
+  default boolean isEmpty() {
+    return size() == 0;
+  }
+
+  /**
+   * Retrieves the 8 bytes starting at the provided index in this value as a long.
+   *
+   * @param i The index from which to get the long, which must less than or equal to {@code size() - 8}.
+   * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 8}.
+   */
+  default long getLong(int i) {
+    return getLong(i, BIG_ENDIAN);
+  }
+
+  /**
+   * Retrieves the 8 bytes starting at the provided index in this value as a long.
+   *
+   * @param i The index from which to get the long, which must less than or equal to {@code size() - 8}.
+   * @param order The byte-order for decoding the integer.
+   * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 8}.
+   */
+  default long getLong(int i, ByteOrder order) {
+    int size = size();
+    checkElementIndex(i, size);
+    if (i > (size - 8)) {
+      throw new IndexOutOfBoundsException(
+          format("Value of size %s has not enough bytes to read a 8 bytes long from index %s", size, i));
+    }
+
+    long value = 0;
+    if (order == BIG_ENDIAN) {
+      value |= ((long) get(i) & 0xFF) << 56;
+      value |= ((long) get(i + 1) & 0xFF) << 48;
+      value |= ((long) get(i + 2) & 0xFF) << 40;
+      value |= ((long) get(i + 3) & 0xFF) << 32;
+      value |= ((long) get(i + 4) & 0xFF) << 24;
+      value |= ((long) get(i + 5) & 0xFF) << 16;
+      value |= ((long) get(i + 6) & 0xFF) << 8;
+      value |= ((long) get(i + 7) & 0xFF);
+    } else {
+      value |= ((long) get(i + 7) & 0xFF) << 56;
+      value |= ((long) get(i + 6) & 0xFF) << 48;
+      value |= ((long) get(i + 5) & 0xFF) << 40;
+      value |= ((long) get(i + 4) & 0xFF) << 32;
+      value |= ((long) get(i + 3) & 0xFF) << 24;
+      value |= ((long) get(i + 2) & 0xFF) << 16;
+      value |= ((long) get(i + 1) & 0xFF) << 8;
+      value |= ((long) get(i) & 0xFF);
+    }
+    return value;
+  }
+
+  /**
+   * The value corresponding to interpreting these bytes as a long.
+   *
+   * @return An value corresponding to this value interpreted as a long.
+   * @throws IllegalArgumentException if {@code size() &gt; 8}.
+   */
+  default long toLong() {
+    return toLong(BIG_ENDIAN);
+  }
+
+  /**
+   * The value corresponding to interpreting these bytes as a long.
+   *
+   * @param order The byte-order for decoding the integer.
+   * @return An value corresponding to this value interpreted as a long.
+   * @throws IllegalArgumentException if {@code size() &gt; 8}.
+   */
+  default long toLong(ByteOrder order) {
+    int size = size();
+    checkArgument(size <= 8, "Value of size %s has more than 8 bytes", size());
+    if (size == 0) {
+      return 0;
+    }
+    if (order == BIG_ENDIAN) {
+      int i = size;
+      long value = ((long) get(--i) & 0xFF);
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 8;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 16;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 24;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 32;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 40;
+      if (i == 0) {
+        return value;
+      }
+      value |= ((long) get(--i) & 0xFF) << 48;
+      if (i == 0) {
+        return value;
+      }
+      return value | ((long) get(--i) & 0xFF) << 56;
+    } else {
+      int i = 0;
+      long value = ((long) get(i) & 0xFF);
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 8;
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 16;
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 24;
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 32;
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 40;
+      if (++i == size) {
+        return value;
+      }
+      value |= ((long) get(i) & 0xFF) << 48;
+      if (++i == size) {
+        return value;
+      }
+      return value | ((long) get(i) & 0xFF) << 56;
+    }
+  }
+
+  /**
+   * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer.
+   *
+   * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement signed integer.
+   */
+  default BigInteger toBigInteger() {
+    return toBigInteger(BIG_ENDIAN);
+  }
+
+  /**
+   * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer.
+   *
+   * @param order The byte-order for decoding the integer.
+   * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement signed integer.
+   */
+  default BigInteger toBigInteger(ByteOrder order) {
+    if (size() == 0) {
+      return BigInteger.ZERO;
+    }
+    return new BigInteger((order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
+  }
+
+  /**
+   * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
+   *
+   * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an unsigned integer.
+   */
+  default BigInteger toUnsignedBigInteger() {
+    return toUnsignedBigInteger(BIG_ENDIAN);
+  }
+
+  /**
+   * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
+   *
+   * @param order The byte-order for decoding the integer.
+   * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an unsigned integer.
+   */
+  default BigInteger toUnsignedBigInteger(ByteOrder order) {
+    return new BigInteger(1, (order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
+  }
+
+  /**
+   * Whether this value has only zero bytes.
+   *
+   * @return {@code true} if all the bits of this value are zeros.
+   */
+  default boolean isZero() {
+    for (int i = size() - 1; i >= 0; --i) {
+      if (get(i) != 0)
+        return false;
+    }
+    return true;
+  }
+
+  /**
+   * Whether the bytes start with a zero bit value.
+   *
+   * @return true if the first bit equals zero
+   */
+  default boolean hasLeadingZero() {
+    return size() > 0 && (get(0) & 0x80) == 0;
+  }
+
+  /**
+   * @return The number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code size() * 8} if all bits
+   *         are zero.
+   */
+  default int numberOfLeadingZeros() {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      byte b = get(i);
+      if (b == 0) {
+        continue;
+      }
+
+      return (i * 8) + Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8;
+    }
+    return size * 8;
+  }
+
+  /**
+   * Whether the bytes start with a zero byte value.
+   *
+   * @return true if the first byte equals zero
+   */
+  default boolean hasLeadingZeroByte() {
+    return size() > 0 && get(0) == 0;
+  }
+
+  /**
+   * @return The number of leading zero bytes of the value.
+   */
+  default int numberOfLeadingZeroBytes() {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      if (get(i) != 0) {
+        return i;
+      }
+    }
+    return size;
+  }
+
+  /**
+   * @return The number of trailing zero bytes of the value.
+   */
+  default int numberOfTrailingZeroBytes() {
+    int size = size();
+    for (int i = size; i >= 1; i--) {
+      if (get(i - 1) != 0) {
+        return size - i;
+      }
+    }
+    return size;
+  }
+
+  /**
+   * @return The number of bits following and including the highest-order ("leftmost") one-bit, or zero if all bits are
+   *         zero.
+   */
+  default int bitLength() {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      byte b = get(i);
+      if (b == 0)
+        continue;
+
+      return (size * 8) - (i * 8) - (Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8);
+    }
+    return 0;
+  }
+
+  /**
+   * Return a bit-wise AND of these bytes and the supplied bytes.
+   *
+   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise AND.
+   */
+  default Bytes and(Bytes other) {
+    return and(other, MutableBytes.create(Math.max(size(), other.size())));
+  }
+
+  /**
+   * Calculate a bit-wise AND of these bytes and the supplied bytes.
+   *
+   * <p>
+   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
+   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
+   * they will be truncated to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T and(Bytes other, T result) {
+    checkNotNull(other);
+    checkNotNull(result);
+    int rSize = result.size();
+    int offsetSelf = rSize - size();
+    int offsetOther = rSize - other.size();
+    for (int i = 0; i < rSize; i++) {
+      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
+      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
+      result.set(i, (byte) (b1 & b2));
+    }
+    return result;
+  }
+
+  /**
+   * Return a bit-wise OR of these bytes and the supplied bytes.
+   *
+   * <p>
+   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise OR.
+   */
+  default Bytes or(Bytes other) {
+    return or(other, MutableBytes.create(Math.max(size(), other.size())));
+  }
+
+  /**
+   * Calculate a bit-wise OR of these bytes and the supplied bytes.
+   *
+   * <p>
+   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
+   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
+   * they will be truncated to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T or(Bytes other, T result) {
+    checkNotNull(other);
+    checkNotNull(result);
+    int rSize = result.size();
+    int offsetSelf = rSize - size();
+    int offsetOther = rSize - other.size();
+    for (int i = 0; i < rSize; i++) {
+      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
+      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
+      result.set(i, (byte) (b1 | b2));
+    }
+    return result;
+  }
+
+  /**
+   * Return a bit-wise XOR of these bytes and the supplied bytes.
+   *
+   * <p>
+   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise XOR.
+   */
+  default Bytes xor(Bytes other) {
+    return xor(other, MutableBytes.create(Math.max(size(), other.size())));
+  }
+
+  /**
+   * Calculate a bit-wise XOR of these bytes and the supplied bytes.
+   *
+   * <p>
+   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
+   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
+   * they will be truncated to the left.
+   *
+   * @param other The bytes to perform the operation with.
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T xor(Bytes other, T result) {
+    checkNotNull(other);
+    checkNotNull(result);
+    int rSize = result.size();
+    int offsetSelf = rSize - size();
+    int offsetOther = rSize - other.size();
+    for (int i = 0; i < rSize; i++) {
+      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
+      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
+      result.set(i, (byte) (b1 ^ b2));
+    }
+    return result;
+  }
+
+  /**
+   * Return a bit-wise NOT of these bytes.
+   *
+   * @return The result of a bit-wise NOT.
+   */
+  default Bytes not() {
+    return not(MutableBytes.create(size()));
+  }
+
+  /**
+   * Calculate a bit-wise NOT of these bytes.
+   *
+   * <p>
+   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
+   * this value is longer in length than the output vector, then it will be truncated to the left.
+   *
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T not(T result) {
+    checkNotNull(result);
+    int rSize = result.size();
+    int offsetSelf = rSize - size();
+    for (int i = 0; i < rSize; i++) {
+      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
+      result.set(i, (byte) ~b1);
+    }
+    return result;
+  }
+
+  /**
+   * Shift all bits in this value to the right.
+   *
+   * @param distance The number of bits to shift by.
+   * @return A value containing the shifted bits.
+   */
+  default Bytes shiftRight(int distance) {
+    return shiftRight(distance, MutableBytes.create(size()));
+  }
+
+  /**
+   * Shift all bits in this value to the right.
+   *
+   * <p>
+   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
+   * this value is longer in length than the output vector, then it will be truncated to the left (after shifting).
+   *
+   * @param distance The number of bits to shift by.
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T shiftRight(int distance, T result) {
+    checkNotNull(result);
+    int rSize = result.size();
+    int offsetSelf = rSize - size();
+
+    int d = distance / 8;
+    int s = distance % 8;
+    int resIdx = rSize - 1;
+    for (int i = rSize - 1 - d; i >= 0; i--) {
+      byte res;
+      if (i < offsetSelf) {
+        res = 0;
+      } else {
+        int selfIdx = i - offsetSelf;
+        int leftSide = (get(selfIdx) & 0xFF) >>> s;
+        int rightSide = (selfIdx == 0) ? 0 : get(selfIdx - 1) << (8 - s);
+        res = (byte) (leftSide | rightSide);
+      }
+      result.set(resIdx--, res);
+    }
+    for (; resIdx >= 0; resIdx--) {
+      result.set(resIdx, (byte) 0);
+    }
+    return result;
+  }
+
+  /**
+   * Shift all bits in this value to the left.
+   *
+   * @param distance The number of bits to shift by.
+   * @return A value containing the shifted bits.
+   */
+  default Bytes shiftLeft(int distance) {
+    return shiftLeft(distance, MutableBytes.create(size()));
+  }
+
+  /**
+   * Shift all bits in this value to the left.
+   *
+   * <p>
+   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
+   * this value is longer in length than the output vector, then it will be truncated to the left.
+   *
+   * @param distance The number of bits to shift by.
+   * @param result The mutable output vector for the result.
+   * @param <T> The {@link MutableBytes} value type.
+   * @return The {@code result} output vector.
+   */
+  default <T extends MutableBytes> T shiftLeft(int distance, T result) {
+    checkNotNull(result);
+    int size = size();
+    int rSize = result.size();
+    int offsetSelf = rSize - size;
+
+    int d = distance / 8;
+    int s = distance % 8;
+    int resIdx = 0;
+    for (int i = d; i < rSize; i++) {
+      byte res;
+      if (i < offsetSelf) {
+        res = 0;
+      } else {
+        int selfIdx = i - offsetSelf;
+        int leftSide = get(selfIdx) << s;
+        int rightSide = (selfIdx == size - 1) ? 0 : (get(selfIdx + 1) & 0xFF) >>> (8 - s);
+        res = (byte) (leftSide | rightSide);
+      }
+      result.set(resIdx++, res);
+    }
+    for (; resIdx < rSize; resIdx++) {
+      result.set(resIdx, (byte) 0);
+    }
+    return result;
+  }
+
+  /**
+   * Create a new value representing (a view of) a slice of the bytes of this value.
+   *
+   * <p>
+   * Please note that the resulting slice is only a view and as such maintains a link to the underlying full value. So
+   * holding a reference to the returned slice may hold more memory than the slide represents. Use {@link #copy} on the
+   * returned slice if that is not what you want.
+   *
+   * @param i The start index for the slice.
+   * @return A new value providing a view over the bytes from index {@code i} (included) to the end.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0}.
+   */
+  default Bytes slice(int i) {
+    if (i == 0) {
+      return this;
+    }
+    int size = size();
+    if (i >= size) {
+      return EMPTY;
+    }
+    return slice(i, size - i);
+  }
+
+  /**
+   * Create a new value representing (a view of) a slice of the bytes of this value.
+   *
+   * <p>
+   * Please note that the resulting slice is only a view and as such maintains a link to the underlying full value. So
+   * holding a reference to the returned slice may hold more memory than the slide represents. Use {@link #copy} on the
+   * returned slice if that is not what you want.
+   *
+   * @param i The start index for the slice.
+   * @param length The length of the resulting value.
+   * @return A new value providing a view over the bytes from index {@code i} (included) to {@code i + length}
+   *         (excluded).
+   * @throws IllegalArgumentException if {@code length &lt; 0}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {i &gt;= size()} or {i + length &gt; size()} .
+   */
+  Bytes slice(int i, int length);
+
+  /**
+   * Return a value equivalent to this one but guaranteed to 1) be deeply immutable (i.e. the underlying value will be
+   * immutable) and 2) to not retain more bytes than exposed by the value.
+   *
+   * @return A value, equals to this one, but deeply immutable and that doesn't retain any "unreachable" bytes. For
+   *         performance reasons, this is allowed to return this value however if it already fit those constraints.
+   */
+  Bytes copy();
+
+  /**
+   * Return a new mutable value initialized with the content of this value.
+   *
+   * @return A mutable copy of this value. This will copy bytes, modifying the returned value will <b>not</b> modify
+   *         this value.
+   */
+  MutableBytes mutableCopy();
+
+  /**
+   * Copy the bytes of this value to the provided mutable one, which must have the same size.
+   *
+   * @param destination The mutable value to which to copy the bytes to, which must have the same size as this value. If
+   *        you want to copy value where size differs, you should use {@link #slice} and/or
+   *        {@link MutableBytes#mutableSlice} and apply the copy to the result.
+   * @throws IllegalArgumentException if {@code this.size() != destination.size()}.
+   */
+  default void copyTo(MutableBytes destination) {
+    checkNotNull(destination);
+    checkArgument(
+        destination.size() == size(),
+        "Cannot copy %s bytes to destination of non-equal size %s",
+        size(),
+        destination.size());
+    copyTo(destination, 0);
+  }
+
+  /**
+   * Copy the bytes of this value to the provided mutable one from a particular offset.
+   *
+   * <p>
+   * This is a (potentially slightly more efficient) shortcut for {@code
+   * copyTo(destination.mutableSlice(destinationOffset, this.size()))}.
+   *
+   * @param destination The mutable value to which to copy the bytes to, which must have enough bytes from
+   *        {@code destinationOffset} for the copied value.
+   * @param destinationOffset The offset in {@code destination} at which the copy starts.
+   * @throws IllegalArgumentException if the destination doesn't have enough room, that is if {@code
+   *     this.size() &gt; (destination.size() - destinationOffset)}.
+   */
+  default void copyTo(MutableBytes destination, int destinationOffset) {
+    checkNotNull(destination);
+
+    // Special casing an empty source or the following checks might throw (even though we have
+    // nothing to copy anyway) and this gets inconvenient for generic methods using copyTo() as
+    // they may have to special case empty values because of this. As an example,
+    // concatenate(EMPTY, EMPTY) would need to be special cased without this.
+    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);
+
+    for (int i = 0; i < size; i++) {
+      destination.set(destinationOffset + i, get(i));
+    }
+  }
+
+  /**
+   * Append the bytes of this value to the {@link ByteBuffer}.
+   *
+   * @param byteBuffer The {@link ByteBuffer} to which to append this value.
+   * @throws BufferOverflowException If the writer attempts to write more than the provided buffer can hold.
+   * @throws ReadOnlyBufferException If the provided buffer is read-only.
+   */
+  default void appendTo(ByteBuffer byteBuffer) {
+    checkNotNull(byteBuffer);
+    for (int i = 0; i < size(); i++) {
+      byteBuffer.put(get(i));
+    }
+  }
+
+  /**
+   * Append the bytes of this value to the provided Vert.x {@link Buffer}.
+   *
+   * <p>
+   * Note that since a Vert.x {@link Buffer} will grow as necessary, this method never fails.
+   *
+   * @param buffer The {@link Buffer} to which to append this value.
+   */
+  default void appendTo(Buffer buffer) {
+    checkNotNull(buffer);
+    for (int i = 0; i < size(); i++) {
+      buffer.appendByte(get(i));
+    }
+  }
+
+  /**
+   * Append this value as a sequence of hexadecimal characters.
+   *
+   * @param appendable The appendable
+   * @param <T> The appendable type.
+   * @return The appendable.
+   * @throws IOException If an IO error occurs.
+   */
+  default <T extends Appendable> T appendHexTo(T appendable) throws IOException {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      byte b = get(i);
+      appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
+      appendable.append(AbstractBytes.HEX_CODE[b & 15]);
+    }
+    return appendable;
+  }
+
+  /**
+   * Return the number of bytes in common between this set of bytes and another.
+   *
+   * @param other The bytes to compare to.
+   * @return The number of common bytes.
+   */
+  default int commonPrefixLength(Bytes other) {
+    checkNotNull(other);
+    int ourSize = size();
+    int otherSize = other.size();
+    int i = 0;
+    while (i < ourSize && i < otherSize && get(i) == other.get(i)) {
+      i++;
+    }
+    return i;
+  }
+
+  /**
+   * Return a slice over the common prefix between this set of bytes and another.
+   *
+   * @param other The bytes to compare to.
+   * @return A slice covering the common prefix.
+   */
+  default Bytes commonPrefix(Bytes other) {
+    return slice(0, commonPrefixLength(other));
+  }
+
+  /**
+   * Return a slice of representing the same value but without any leading zero bytes.
+   *
+   * @return {@code value} if its left-most byte is non zero, or a slice that exclude any leading zero bytes.
+   */
+  default Bytes trimLeadingZeros() {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      if (get(i) != 0) {
+        return slice(i);
+      }
+    }
+    return Bytes.EMPTY;
+  }
+
+  /**
+   * Update the provided message digest with the bytes of this value.
+   *
+   * @param digest The digest to update.
+   */
+  default void update(MessageDigest digest) {
+    checkNotNull(digest);
+    digest.update(toArrayUnsafe());
+  }
+
+  /**
+   * Computes the reverse array of bytes of the current bytes.
+   *
+   * @return a new Bytes value, containing the bytes in reverse order
+   */
+  default Bytes reverse() {
+    byte[] reverse = new byte[size()];
+    for (int i = 0; i < size(); i++) {
+      reverse[size() - i - 1] = get(i);
+    }
+    return Bytes.wrap(reverse);
+  }
+
+  /**
+   * Extract the bytes of this value into a byte array.
+   *
+   * @return A byte array with the same content than this value.
+   */
+  default byte[] toArray() {
+    int size = size();
+    byte[] array = new byte[size];
+    for (int i = 0; i < size; i++) {
+      array[i] = get(i);
+    }
+    return array;
+  }
+
+  /**
+   * Get the bytes represented by this value as byte array.
+   *
+   * <p>
+   * Contrarily to {@link #toArray()}, this may avoid allocating a new array and directly return the backing array of
+   * this value if said value is array backed and doing so is possible. As such, modifications to the returned array may
+   * or may not impact this value. As such, this method should be used with care and hence the "unsafe" moniker.
+   *
+   * @return A byte array with the same content than this value, which may or may not be the direct backing of this
+   *         value.
+   */
+  default byte[] toArrayUnsafe() {
+    return toArray();
+  }
+
+  /**
+   * Return the hexadecimal string representation of this value.
+   *
+   * @return The hexadecimal representation of this value, starting with "0x".
+   */
+  @Override
+  String toString();
+
+  /**
+   * @return This value represented as hexadecimal, starting with "0x".
+   */
+  default String toHexString() {
+    try {
+      return appendHexTo(new StringBuilder("0x")).toString();
+    } catch (IOException e) {
+      // not thrown
+      throw new RuntimeException(e);
+    }
+  }
+
+  /** @return This value represented as a minimal hexadecimal string (without any leading zero). */
+  default String toShortHexString() {
+    StringBuilder hex;
+    try {
+      hex = appendHexTo(new StringBuilder());
+    } catch (IOException e) {
+      // not thrown
+      throw new RuntimeException(e);
+    }
+
+    int i = 0;
+    while (i < hex.length() && hex.charAt(i) == '0') {
+      i++;
+    }
+    return "0x" + hex.substring(i);
+  }
+
+  /**
+   * @return This value represented as base 64.
+   */
+  default String toBase64String() {
+    return Base64.getEncoder().encodeToString(toArrayUnsafe());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/Bytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/Bytes32.java
new file mode 100644
index 0000000..f962f52
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/Bytes32.java
@@ -0,0 +1,282 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * A {@link Bytes} value that is guaranteed to contain exactly 32 bytes.
+ */
+public interface Bytes32 extends Bytes {
+  /** The number of bytes in this value - i.e. 32 */
+  int SIZE = 32;
+
+  /** A {@code Bytes32} containing all zero bytes */
+  Bytes32 ZERO = wrap(new byte[32]);
+
+  /**
+   * Wrap the provided byte array, which must be of length 32, as a {@link Bytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param bytes The bytes to wrap.
+   * @return A {@link Bytes32} wrapping {@code value}.
+   * @throws IllegalArgumentException if {@code value.length != 32}.
+   */
+  static Bytes32 wrap(byte[] bytes) {
+    checkNotNull(bytes);
+    checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+    return wrap(bytes, 0);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided array as a {@link Bytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param bytes The bytes to wrap.
+   * @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 exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 32} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length &gt; 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 32 &gt; value.length}.
+   */
+  static Bytes32 wrap(byte[] bytes, int offset) {
+    checkNotNull(bytes);
+    return new ArrayWrappingBytes32(bytes, offset);
+  }
+
+  /**
+   * Wrap a the provided value, which must be of size 32, as a {@link Bytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param value The bytes to wrap.
+   * @return A {@link Bytes32} that exposes the bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() != 32}.
+   */
+  static Bytes32 wrap(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes32) {
+      return (Bytes32) value;
+    }
+    return DelegatingBytes32.delegateTo(value);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided value as a {@link Bytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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.get(i)}.
+   * @return A {@link Bytes32} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 32} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.size() &gt; 0 && offset >=
+   *     value.size())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 32 &gt; value.size()}.
+   */
+  static Bytes32 wrap(Bytes value, int offset) {
+    checkNotNull(value);
+    if (value instanceof Bytes32) {
+      return (Bytes32) value;
+    }
+    Bytes slice = value.slice(offset, Bytes32.SIZE);
+    if (slice instanceof Bytes32) {
+      return (Bytes32) slice;
+    }
+    return DelegatingBytes32.delegateTo(slice);
+  }
+
+  /**
+   * Left pad a {@link Bytes} value with zero bytes to create a {@link Bytes32}.
+   *
+   * @param value The bytes value pad.
+   * @return A {@link Bytes32} that exposes the left-padded bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() &gt; 32}.
+   */
+  static Bytes32 leftPad(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes32) {
+      return (Bytes32) value;
+    }
+    checkArgument(value.size() <= SIZE, "Expected at most %s bytes but got %s", SIZE, value.size());
+    MutableBytes32 result = MutableBytes32.create();
+    value.copyTo(result, SIZE - value.size());
+    return result;
+  }
+
+  /**
+   * Right pad a {@link Bytes} value with zero bytes to create a {@link Bytes32}.
+   *
+   * @param value The bytes value pad.
+   * @return A {@link Bytes32} that exposes the rightw-padded bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() &gt; 32}.
+   */
+  static Bytes32 rightPad(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes32) {
+      return (Bytes32) value;
+    }
+    checkArgument(value.size() <= SIZE, "Expected at most %s bytes but got %s", SIZE, value.size());
+    MutableBytes32 result = MutableBytes32.create();
+    value.copyTo(result, 0);
+    return result;
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes32}.
+   *
+   * <p>
+   * This method is lenient in that {@code str} may of an odd length, in which case it will behave exactly as if it had
+   * an additional 0 in front.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x". That representation may contain
+   *        less than 32 bytes, in which case the result is left padded with zeros (see {@link #fromHexStringStrict} if
+   *        this is not what you want).
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation or
+   *         contains more than 32 bytes.
+   */
+  static Bytes32 fromHexStringLenient(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, SIZE, true));
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes32}.
+   *
+   * <p>
+   * This method is strict in that {@code str} must of an even length.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x". That representation may contain
+   *        less than 32 bytes, in which case the result is left padded with zeros (see {@link #fromHexStringStrict} if
+   *        this is not what you want).
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, is of an
+   *         odd length, or contains more than 32 bytes.
+   */
+  static Bytes32 fromHexString(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, SIZE, false));
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @return A value containing random bytes.
+   */
+  static Bytes32 random() {
+    return random(new SecureRandom());
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @param generator The generator for random bytes.
+   * @return A value containing random bytes.
+   */
+  static Bytes32 random(Random generator) {
+    byte[] array = new byte[32];
+    generator.nextBytes(array);
+    return wrap(array);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes32}.
+   *
+   * <p>
+   * This method is extra strict in that {@code str} must of an even length and the provided representation must have
+   * exactly 32 bytes.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, is of an
+   *         odd length or does not contain exactly 32 bytes.
+   */
+  static Bytes32 fromHexStringStrict(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, -1, false));
+  }
+
+  @Override
+  default int size() {
+    return SIZE;
+  }
+
+  /**
+   * Return a bit-wise AND of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise AND.
+   */
+  default Bytes32 and(Bytes32 other) {
+    return and(other, MutableBytes32.create());
+  }
+
+  /**
+   * Return a bit-wise OR of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise OR.
+   */
+  default Bytes32 or(Bytes32 other) {
+    return or(other, MutableBytes32.create());
+  }
+
+  /**
+   * Return a bit-wise XOR of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise XOR.
+   */
+  default Bytes32 xor(Bytes32 other) {
+    return xor(other, MutableBytes32.create());
+  }
+
+  @Override
+  default Bytes32 not() {
+    return not(MutableBytes32.create());
+  }
+
+  @Override
+  default Bytes32 shiftRight(int distance) {
+    return shiftRight(distance, MutableBytes32.create());
+  }
+
+  @Override
+  default Bytes32 shiftLeft(int distance) {
+    return shiftLeft(distance, MutableBytes32.create());
+  }
+
+  @Override
+  Bytes32 copy();
+
+  @Override
+  MutableBytes32 mutableCopy();
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/Bytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/Bytes48.java
new file mode 100644
index 0000000..c75b91f
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/Bytes48.java
@@ -0,0 +1,283 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * A {@link Bytes} value that is guaranteed to contain exactly 48 bytes.
+ */
+public interface Bytes48 extends Bytes {
+  /** The number of bytes in this value - i.e. 48 */
+  int SIZE = 48;
+
+  /** A {@code Bytes48} containing all zero bytes */
+  Bytes48 ZERO = wrap(new byte[SIZE]);
+
+  /**
+   * Wrap the provided byte array, which must be of length 48, as a {@link Bytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param bytes The bytes to wrap.
+   * @return A {@link Bytes48} wrapping {@code value}.
+   * @throws IllegalArgumentException if {@code value.length != 48}.
+   */
+  static Bytes48 wrap(byte[] bytes) {
+    checkNotNull(bytes);
+    checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+    return wrap(bytes, 0);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided array as a {@link Bytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param bytes The bytes to wrap.
+   * @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 Bytes48} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 48} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length &gt; 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 48 &gt; value.length}.
+   */
+  static Bytes48 wrap(byte[] bytes, int offset) {
+    checkNotNull(bytes);
+    return new ArrayWrappingBytes48(bytes, offset);
+  }
+
+  /**
+   * Wrap a the provided value, which must be of size 48, as a {@link Bytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param value The bytes to wrap.
+   * @return A {@link Bytes48} that exposes the bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() != 48}.
+   */
+  static Bytes48 wrap(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes48) {
+      return (Bytes48) value;
+    }
+    return DelegatingBytes48.delegateTo(value);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided value as a {@link Bytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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.get(i)}.
+   * @return A {@link Bytes48} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 48} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.size() &gt; 0 && offset >=
+   *     value.size())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 48 &gt; value.size()}.
+   */
+  static Bytes48 wrap(Bytes value, int offset) {
+    checkNotNull(value);
+    if (value instanceof Bytes48) {
+      return (Bytes48) value;
+    }
+    Bytes slice = value.slice(offset, Bytes48.SIZE);
+    if (slice instanceof Bytes48) {
+      return (Bytes48) slice;
+    }
+    return DelegatingBytes48.delegateTo(slice);
+  }
+
+  /**
+   * Left pad a {@link Bytes} value with zero bytes to create a {@link Bytes48}.
+   *
+   * @param value The bytes value pad.
+   * @return A {@link Bytes48} that exposes the left-padded bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() &gt; 48}.
+   */
+  static Bytes48 leftPad(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes48) {
+      return (Bytes48) value;
+    }
+    checkArgument(value.size() <= SIZE, "Expected at most %s bytes but got %s", SIZE, value.size());
+    MutableBytes48 result = MutableBytes48.create();
+    value.copyTo(result, SIZE - value.size());
+    return result;
+  }
+
+
+  /**
+   * Right pad a {@link Bytes} value with zero bytes to create a {@link Bytes48}.
+   *
+   * @param value The bytes value pad.
+   * @return A {@link Bytes48} that exposes the rightw-padded bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() &gt; 48}.
+   */
+  static Bytes48 rightPad(Bytes value) {
+    checkNotNull(value);
+    if (value instanceof Bytes48) {
+      return (Bytes48) value;
+    }
+    checkArgument(value.size() <= SIZE, "Expected at most %s bytes but got %s", SIZE, value.size());
+    MutableBytes48 result = MutableBytes48.create();
+    value.copyTo(result, 0);
+    return result;
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes48}.
+   *
+   * <p>
+   * This method is lenient in that {@code str} may of an odd length, in which case it will behave exactly as if it had
+   * an additional 0 in front.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x". That representation may contain
+   *        less than 48 bytes, in which case the result is left padded with zeros (see {@link #fromHexStringStrict} if
+   *        this is not what you want).
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation or
+   *         contains more than 48 bytes.
+   */
+  static Bytes48 fromHexStringLenient(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, SIZE, true));
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes48}.
+   *
+   * <p>
+   * This method is strict in that {@code str} must of an even length.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x". That representation may contain
+   *        less than 48 bytes, in which case the result is left padded with zeros (see {@link #fromHexStringStrict} if
+   *        this is not what you want).
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, is of an
+   *         odd length, or contains more than 48 bytes.
+   */
+  static Bytes48 fromHexString(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, SIZE, false));
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @return A value containing random bytes.
+   */
+  static Bytes48 random() {
+    return random(new SecureRandom());
+  }
+
+  /**
+   * Generate random bytes.
+   *
+   * @param generator The generator for random bytes.
+   * @return A value containing random bytes.
+   */
+  static Bytes48 random(Random generator) {
+    byte[] array = new byte[48];
+    generator.nextBytes(array);
+    return wrap(array);
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link Bytes48}.
+   *
+   * <p>
+   * This method is extra strict in that {@code str} must of an even length and the provided representation must have
+   * exactly 48 bytes.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x".
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, is of an
+   *         odd length or does not contain exactly 48 bytes.
+   */
+  static Bytes48 fromHexStringStrict(CharSequence str) {
+    checkNotNull(str);
+    return wrap(BytesValues.fromRawHexString(str, -1, false));
+  }
+
+  @Override
+  default int size() {
+    return SIZE;
+  }
+
+  /**
+   * Return a bit-wise AND of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise AND.
+   */
+  default Bytes48 and(Bytes48 other) {
+    return and(other, MutableBytes48.create());
+  }
+
+  /**
+   * Return a bit-wise OR of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise OR.
+   */
+  default Bytes48 or(Bytes48 other) {
+    return or(other, MutableBytes48.create());
+  }
+
+  /**
+   * Return a bit-wise XOR of these bytes and the supplied bytes.
+   *
+   * @param other The bytes to perform the operation with.
+   * @return The result of a bit-wise XOR.
+   */
+  default Bytes48 xor(Bytes48 other) {
+    return xor(other, MutableBytes48.create());
+  }
+
+  @Override
+  default Bytes48 not() {
+    return not(MutableBytes48.create());
+  }
+
+  @Override
+  default Bytes48 shiftRight(int distance) {
+    return shiftRight(distance, MutableBytes48.create());
+  }
+
+  @Override
+  default Bytes48 shiftLeft(int distance) {
+    return shiftLeft(distance, MutableBytes48.create());
+  }
+
+  @Override
+  Bytes48 copy();
+
+  @Override
+  MutableBytes48 mutableCopy();
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/BytesValues.java b/bytes/src/main/java/net/consensys/cava/bytes/BytesValues.java
new file mode 100644
index 0000000..4bde33e
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/BytesValues.java
@@ -0,0 +1,89 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class BytesValues {
+  private BytesValues() {}
+
+  static final int MAX_UNSIGNED_SHORT = (1 << 16) - 1;
+  static final long MAX_UNSIGNED_INT = (1L << 32) - 1;
+  static final long MAX_UNSIGNED_LONG = Long.MAX_VALUE;
+
+  static Bytes fromHexString(CharSequence str, int destSize, boolean lenient) {
+    return Bytes.wrap(fromRawHexString(str, destSize, lenient));
+  }
+
+  static byte[] fromRawHexString(CharSequence str, int destSize, boolean lenient) {
+    int len = str.length();
+    CharSequence hex = str;
+    if (len >= 2 && str.charAt(0) == '0' && str.charAt(1) == 'x') {
+      hex = str.subSequence(2, len);
+      len -= 2;
+    }
+
+    int idxShift = 0;
+    if (len % 2 != 0) {
+      if (!lenient) {
+        throw new IllegalArgumentException("Invalid odd-length hex binary representation");
+      }
+
+      hex = "0" + hex;
+      len += 1;
+      idxShift = 1;
+    }
+
+    int size = len / 2;
+    if (destSize < 0) {
+      destSize = size;
+    } else {
+      checkArgument(size <= destSize, "Hex value is too large: expected at most %s bytes but got %s", destSize, size);
+    }
+
+    byte[] out = new byte[destSize];
+
+    int destOffset = (destSize - size);
+    for (int i = 0; i < len; i += 2) {
+      int h = hexToBin(hex.charAt(i));
+      int l = hexToBin(hex.charAt(i + 1));
+      if (h == -1) {
+        throw new IllegalArgumentException(
+            String.format(
+                "Illegal character '%c' found at index %d in hex binary representation",
+                hex.charAt(i),
+                i - idxShift));
+      }
+      if (l == -1) {
+        throw new IllegalArgumentException(
+            String.format(
+                "Illegal character '%c' found at index %d in hex binary representation",
+                hex.charAt(i + 1),
+                i + 1 - idxShift));
+      }
+
+      out[destOffset + (i / 2)] = (byte) (h * 16 + l);
+    }
+    return out;
+  }
+
+  private static int hexToBin(char ch) {
+    if ('0' <= ch && ch <= '9') {
+      return ch - 48;
+    } else if ('A' <= ch && ch <= 'F') {
+      return ch - 65 + 10;
+    } else {
+      return 'a' <= ch && ch <= 'f' ? ch - 97 + 10 : -1;
+    }
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/ConcatenatedBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/ConcatenatedBytes.java
new file mode 100644
index 0000000..c12a189
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/ConcatenatedBytes.java
@@ -0,0 +1,213 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+final class ConcatenatedBytes extends AbstractBytes {
+
+  private final Bytes[] values;
+  private final int size;
+
+  private ConcatenatedBytes(Bytes[] values, int totalSize) {
+    this.values = values;
+    this.size = totalSize;
+  }
+
+  static Bytes wrap(Bytes... values) {
+    if (values.length == 0) {
+      return EMPTY;
+    }
+    if (values.length == 1) {
+      return values[0];
+    }
+
+    int count = 0;
+    int totalSize = 0;
+
+    for (Bytes value : values) {
+      int size = value.size();
+      try {
+        totalSize = Math.addExact(totalSize, size);
+      } catch (ArithmeticException e) {
+        throw new IllegalArgumentException("Combined length of values is too long (> Integer.MAX_VALUE)");
+      }
+      if (value instanceof ConcatenatedBytes) {
+        count += ((ConcatenatedBytes) value).values.length;
+      } else if (size != 0) {
+        count += 1;
+      }
+    }
+
+    if (count == 0) {
+      return Bytes.EMPTY;
+    }
+    if (count == values.length) {
+      return new ConcatenatedBytes(values, totalSize);
+    }
+
+    Bytes[] concatenated = new Bytes[count];
+    int i = 0;
+    for (Bytes value : values) {
+      if (value instanceof ConcatenatedBytes) {
+        Bytes[] subvalues = ((ConcatenatedBytes) value).values;
+        System.arraycopy(subvalues, 0, concatenated, i, subvalues.length);
+        i += subvalues.length;
+      } else if (value.size() != 0) {
+        concatenated[i++] = value;
+      }
+    }
+    return new ConcatenatedBytes(concatenated, totalSize);
+  }
+
+  @Override
+  public int size() {
+    return size;
+  }
+
+  @Override
+  public byte get(int i) {
+    checkElementIndex(i, size);
+    for (Bytes value : values) {
+      int vSize = value.size();
+      if (i < vSize) {
+        return value.get(i);
+      }
+      i -= vSize;
+    }
+    throw new IllegalStateException("element sizes do not match total size");
+  }
+
+  @Override
+  public Bytes slice(int i, final int length) {
+    if (i == 0 && length == size) {
+      return this;
+    }
+    if (length == 0) {
+      return Bytes.EMPTY;
+    }
+
+    checkElementIndex(i, size);
+    checkArgument(
+        (i + length) <= size,
+        "Provided length %s is too large: the value has size %s and has only %s bytes from %s",
+        length,
+        size,
+        size - i,
+        i);
+
+    int j = 0;
+    int vSize;
+    while (true) {
+      vSize = values[j].size();
+      if (i < vSize) {
+        break;
+      }
+      i -= vSize;
+      ++j;
+    }
+
+    if ((i + length) < vSize) {
+      return values[j].slice(i, length);
+    }
+
+    int remaining = length - (vSize - i);
+    Bytes firstValue = this.values[j].slice(i);
+    int firstOffset = j;
+
+    while (remaining > 0) {
+      if (++j >= this.values.length) {
+        throw new IllegalStateException("element sizes do not match total size");
+      }
+      vSize = this.values[j].size();
+      if (length < vSize) {
+        break;
+      }
+      remaining -= vSize;
+    }
+
+    Bytes[] combined = new Bytes[j - firstOffset + 1];
+    combined[0] = firstValue;
+    if (remaining > 0) {
+      if (combined.length > 2) {
+        System.arraycopy(this.values, firstOffset + 1, combined, 1, combined.length - 2);
+      }
+      combined[combined.length - 1] = this.values[j].slice(0, remaining);
+    } else if (combined.length > 1) {
+      System.arraycopy(this.values, firstOffset + 1, combined, 1, combined.length - 1);
+    }
+    return new ConcatenatedBytes(combined, length);
+  }
+
+  @Override
+  public Bytes copy() {
+    if (size == 0) {
+      return Bytes.EMPTY;
+    }
+    MutableBytes result = MutableBytes.create(size);
+    copyToUnchecked(result, 0);
+    return result;
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    if (size == 0) {
+      return MutableBytes.EMPTY;
+    }
+    MutableBytes result = MutableBytes.create(size);
+    copyToUnchecked(result, 0);
+    return result;
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    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);
+
+    copyToUnchecked(destination, destinationOffset);
+  }
+
+  @Override
+  public byte[] toArray() {
+    if (size == 0) {
+      return new byte[0];
+    }
+
+    MutableBytes result = MutableBytes.create(size);
+    copyToUnchecked(result, 0);
+    return result.toArrayUnsafe();
+  }
+
+  private void copyToUnchecked(MutableBytes destination, int destinationOffset) {
+    int offset = 0;
+    for (Bytes value : values) {
+      int vSize = value.size();
+      if ((offset + vSize) > size) {
+        throw new IllegalStateException("element sizes do not match total size");
+      }
+      value.copyTo(destination, destinationOffset);
+      offset += vSize;
+      destinationOffset += vSize;
+    }
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes32.java
new file mode 100644
index 0000000..c1603b8
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes32.java
@@ -0,0 +1,240 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+import io.vertx.core.buffer.Buffer;
+
+final class DelegatingBytes32 implements Bytes32 {
+
+  private final Bytes delegate;
+
+  private DelegatingBytes32(Bytes delegate) {
+    this.delegate = delegate;
+  }
+
+  static Bytes32 delegateTo(Bytes value) {
+    checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size());
+    return new DelegatingBytes32(value);
+  }
+
+  @Override
+  public int size() {
+    return Bytes32.SIZE;
+  }
+
+  @Override
+  public byte get(int i) {
+    return delegate.get(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return delegate.getInt(i);
+  }
+
+  @Override
+  public int toInt() {
+    return delegate.toInt();
+  }
+
+  @Override
+  public long getLong(int i) {
+    return delegate.getLong(i);
+  }
+
+  @Override
+  public long toLong() {
+    return delegate.toLong();
+  }
+
+  @Override
+  public BigInteger toBigInteger() {
+    return delegate.toBigInteger();
+  }
+
+  @Override
+  public BigInteger toUnsignedBigInteger() {
+    return delegate.toUnsignedBigInteger();
+  }
+
+  @Override
+  public boolean isZero() {
+    return delegate.isZero();
+  }
+
+  @Override
+  public int numberOfLeadingZeros() {
+    return delegate.numberOfLeadingZeros();
+  }
+
+  @Override
+  public int numberOfLeadingZeroBytes() {
+    return delegate.numberOfLeadingZeroBytes();
+  }
+
+  @Override
+  public boolean hasLeadingZeroByte() {
+    return delegate.hasLeadingZeroByte();
+  }
+
+  @Override
+  public boolean hasLeadingZero() {
+    return delegate.hasLeadingZero();
+  }
+
+  @Override
+  public int bitLength() {
+    return delegate.bitLength();
+  }
+
+  @Override
+  public Bytes and(Bytes other) {
+    return delegate.and(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T and(Bytes other, T result) {
+    return delegate.and(other, result);
+  }
+
+  @Override
+  public Bytes or(Bytes other) {
+    return delegate.or(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T or(Bytes other, T result) {
+    return delegate.or(other, result);
+  }
+
+  @Override
+  public Bytes xor(Bytes other) {
+    return delegate.xor(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T xor(Bytes other, T result) {
+    return delegate.xor(other, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T not(T result) {
+    return delegate.not(result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftRight(int distance, T result) {
+    return delegate.shiftRight(distance, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftLeft(int distance, T result) {
+    return delegate.shiftLeft(distance, result);
+  }
+
+  @Override
+  public Bytes slice(int index) {
+    return delegate.slice(index);
+  }
+
+  @Override
+  public Bytes slice(int index, int length) {
+    return delegate.slice(index, length);
+  }
+
+  @Override
+  public Bytes32 copy() {
+    return Bytes32.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes32 mutableCopy() {
+    return MutableBytes32.wrap(toArray());
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination) {
+    delegate.copyTo(destination);
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    delegate.copyTo(destination, destinationOffset);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    delegate.appendTo(byteBuffer);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    delegate.appendTo(buffer);
+  }
+
+  @Override
+  public int commonPrefixLength(Bytes other) {
+    return delegate.commonPrefixLength(other);
+  }
+
+  @Override
+  public Bytes commonPrefix(Bytes other) {
+    return delegate.commonPrefix(other);
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    delegate.update(digest);
+  }
+
+  @Override
+  public byte[] toArray() {
+    return delegate.toArray();
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    return delegate.toArrayUnsafe();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public String toHexString() {
+    return delegate.toHexString();
+  }
+
+  @Override
+  public String toShortHexString() {
+    return delegate.toShortHexString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return delegate.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return delegate.hashCode();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes48.java
new file mode 100644
index 0000000..b89cbea
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes48.java
@@ -0,0 +1,240 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+import io.vertx.core.buffer.Buffer;
+
+final class DelegatingBytes48 implements Bytes48 {
+
+  private final Bytes delegate;
+
+  private DelegatingBytes48(Bytes delegate) {
+    this.delegate = delegate;
+  }
+
+  static Bytes48 delegateTo(Bytes value) {
+    checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size());
+    return new DelegatingBytes48(value);
+  }
+
+  @Override
+  public int size() {
+    return Bytes48.SIZE;
+  }
+
+  @Override
+  public byte get(int i) {
+    return delegate.get(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return delegate.getInt(i);
+  }
+
+  @Override
+  public int toInt() {
+    return delegate.toInt();
+  }
+
+  @Override
+  public long getLong(int i) {
+    return delegate.getLong(i);
+  }
+
+  @Override
+  public long toLong() {
+    return delegate.toLong();
+  }
+
+  @Override
+  public BigInteger toBigInteger() {
+    return delegate.toBigInteger();
+  }
+
+  @Override
+  public BigInteger toUnsignedBigInteger() {
+    return delegate.toUnsignedBigInteger();
+  }
+
+  @Override
+  public boolean isZero() {
+    return delegate.isZero();
+  }
+
+  @Override
+  public int numberOfLeadingZeros() {
+    return delegate.numberOfLeadingZeros();
+  }
+
+  @Override
+  public int numberOfLeadingZeroBytes() {
+    return delegate.numberOfLeadingZeroBytes();
+  }
+
+  @Override
+  public boolean hasLeadingZeroByte() {
+    return delegate.hasLeadingZeroByte();
+  }
+
+  @Override
+  public boolean hasLeadingZero() {
+    return delegate.hasLeadingZero();
+  }
+
+  @Override
+  public int bitLength() {
+    return delegate.bitLength();
+  }
+
+  @Override
+  public Bytes and(Bytes other) {
+    return delegate.and(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T and(Bytes other, T result) {
+    return delegate.and(other, result);
+  }
+
+  @Override
+  public Bytes or(Bytes other) {
+    return delegate.or(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T or(Bytes other, T result) {
+    return delegate.or(other, result);
+  }
+
+  @Override
+  public Bytes xor(Bytes other) {
+    return delegate.xor(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T xor(Bytes other, T result) {
+    return delegate.xor(other, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T not(T result) {
+    return delegate.not(result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftRight(int distance, T result) {
+    return delegate.shiftRight(distance, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftLeft(int distance, T result) {
+    return delegate.shiftLeft(distance, result);
+  }
+
+  @Override
+  public Bytes slice(int index) {
+    return delegate.slice(index);
+  }
+
+  @Override
+  public Bytes slice(int index, int length) {
+    return delegate.slice(index, length);
+  }
+
+  @Override
+  public Bytes48 copy() {
+    return Bytes48.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes48 mutableCopy() {
+    return MutableBytes48.wrap(toArray());
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination) {
+    delegate.copyTo(destination);
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    delegate.copyTo(destination, destinationOffset);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    delegate.appendTo(byteBuffer);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    delegate.appendTo(buffer);
+  }
+
+  @Override
+  public int commonPrefixLength(Bytes other) {
+    return delegate.commonPrefixLength(other);
+  }
+
+  @Override
+  public Bytes commonPrefix(Bytes other) {
+    return delegate.commonPrefix(other);
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    delegate.update(digest);
+  }
+
+  @Override
+  public byte[] toArray() {
+    return delegate.toArray();
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    return delegate.toArrayUnsafe();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public String toHexString() {
+    return delegate.toHexString();
+  }
+
+  @Override
+  public String toShortHexString() {
+    return delegate.toShortHexString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return delegate.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return delegate.hashCode();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes32.java
new file mode 100644
index 0000000..a273db8
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes32.java
@@ -0,0 +1,280 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+import io.vertx.core.buffer.Buffer;
+
+final class DelegatingMutableBytes32 implements MutableBytes32 {
+
+  private final MutableBytes delegate;
+
+  private DelegatingMutableBytes32(MutableBytes delegate) {
+    this.delegate = delegate;
+  }
+
+  static MutableBytes32 delegateTo(MutableBytes value) {
+    checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size());
+    return new DelegatingMutableBytes32(value);
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    delegate.set(i, b);
+  }
+
+  @Override
+  public void setInt(int i, int value) {
+    delegate.setInt(i, value);
+  }
+
+  @Override
+  public void setLong(int i, long value) {
+    delegate.setLong(i, value);
+  }
+
+  @Override
+  public MutableBytes increment() {
+    return delegate.increment();
+  }
+
+  @Override
+  public MutableBytes decrement() {
+    return delegate.decrement();
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    return delegate.mutableSlice(i, length);
+  }
+
+  @Override
+  public void fill(byte b) {
+    delegate.fill(b);
+  }
+
+  @Override
+  public void clear() {
+    delegate.clear();
+  }
+
+  @Override
+  public int size() {
+    return delegate.size();
+  }
+
+  @Override
+  public byte get(int i) {
+    return delegate.get(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return delegate.getInt(i);
+  }
+
+  @Override
+  public int toInt() {
+    return delegate.toInt();
+  }
+
+  @Override
+  public long getLong(int i) {
+    return delegate.getLong(i);
+  }
+
+  @Override
+  public long toLong() {
+    return delegate.toLong();
+  }
+
+  @Override
+  public BigInteger toBigInteger() {
+    return delegate.toBigInteger();
+  }
+
+  @Override
+  public BigInteger toUnsignedBigInteger() {
+    return delegate.toUnsignedBigInteger();
+  }
+
+  @Override
+  public boolean isZero() {
+    return delegate.isZero();
+  }
+
+  @Override
+  public int numberOfLeadingZeros() {
+    return delegate.numberOfLeadingZeros();
+  }
+
+  @Override
+  public int numberOfLeadingZeroBytes() {
+    return delegate.numberOfLeadingZeroBytes();
+  }
+
+  @Override
+  public boolean hasLeadingZeroByte() {
+    return delegate.hasLeadingZeroByte();
+  }
+
+  @Override
+  public boolean hasLeadingZero() {
+    return delegate.hasLeadingZero();
+  }
+
+  @Override
+  public int bitLength() {
+    return delegate.bitLength();
+  }
+
+  @Override
+  public Bytes and(Bytes other) {
+    return delegate.and(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T and(Bytes other, T result) {
+    return delegate.and(other, result);
+  }
+
+  @Override
+  public Bytes or(Bytes other) {
+    return delegate.or(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T or(Bytes other, T result) {
+    return delegate.or(other, result);
+  }
+
+  @Override
+  public Bytes xor(Bytes other) {
+    return delegate.xor(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T xor(Bytes other, T result) {
+    return delegate.xor(other, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T not(T result) {
+    return delegate.not(result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftRight(int distance, T result) {
+    return delegate.shiftRight(distance, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftLeft(int distance, T result) {
+    return delegate.shiftLeft(distance, result);
+  }
+
+  @Override
+  public Bytes slice(int index) {
+    return delegate.slice(index);
+  }
+
+  @Override
+  public Bytes slice(int index, int length) {
+    return delegate.slice(index, length);
+  }
+
+  @Override
+  public Bytes32 copy() {
+    return Bytes32.wrap(delegate.toArray());
+  }
+
+  @Override
+  public MutableBytes32 mutableCopy() {
+    return MutableBytes32.wrap(delegate.toArray());
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination) {
+    delegate.copyTo(destination);
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    delegate.copyTo(destination, destinationOffset);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    delegate.appendTo(byteBuffer);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    delegate.appendTo(buffer);
+  }
+
+  @Override
+  public int commonPrefixLength(Bytes other) {
+    return delegate.commonPrefixLength(other);
+  }
+
+  @Override
+  public Bytes commonPrefix(Bytes other) {
+    return delegate.commonPrefix(other);
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    delegate.update(digest);
+  }
+
+  @Override
+  public byte[] toArray() {
+    return delegate.toArray();
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    return delegate.toArrayUnsafe();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public String toHexString() {
+    return delegate.toHexString();
+  }
+
+  @Override
+  public String toShortHexString() {
+    return delegate.toShortHexString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return delegate.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return delegate.hashCode();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes48.java
new file mode 100644
index 0000000..a16e82b
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes48.java
@@ -0,0 +1,280 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+import io.vertx.core.buffer.Buffer;
+
+final class DelegatingMutableBytes48 implements MutableBytes48 {
+
+  private final MutableBytes delegate;
+
+  private DelegatingMutableBytes48(MutableBytes delegate) {
+    this.delegate = delegate;
+  }
+
+  static MutableBytes48 delegateTo(MutableBytes value) {
+    checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size());
+    return new DelegatingMutableBytes48(value);
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    delegate.set(i, b);
+  }
+
+  @Override
+  public void setInt(int i, int value) {
+    delegate.setInt(i, value);
+  }
+
+  @Override
+  public void setLong(int i, long value) {
+    delegate.setLong(i, value);
+  }
+
+  @Override
+  public MutableBytes increment() {
+    return delegate.increment();
+  }
+
+  @Override
+  public MutableBytes decrement() {
+    return delegate.decrement();
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    return delegate.mutableSlice(i, length);
+  }
+
+  @Override
+  public void fill(byte b) {
+    delegate.fill(b);
+  }
+
+  @Override
+  public void clear() {
+    delegate.clear();
+  }
+
+  @Override
+  public int size() {
+    return delegate.size();
+  }
+
+  @Override
+  public byte get(int i) {
+    return delegate.get(i);
+  }
+
+  @Override
+  public int getInt(int i) {
+    return delegate.getInt(i);
+  }
+
+  @Override
+  public int toInt() {
+    return delegate.toInt();
+  }
+
+  @Override
+  public long getLong(int i) {
+    return delegate.getLong(i);
+  }
+
+  @Override
+  public long toLong() {
+    return delegate.toLong();
+  }
+
+  @Override
+  public BigInteger toBigInteger() {
+    return delegate.toBigInteger();
+  }
+
+  @Override
+  public BigInteger toUnsignedBigInteger() {
+    return delegate.toUnsignedBigInteger();
+  }
+
+  @Override
+  public boolean isZero() {
+    return delegate.isZero();
+  }
+
+  @Override
+  public int numberOfLeadingZeros() {
+    return delegate.numberOfLeadingZeros();
+  }
+
+  @Override
+  public int numberOfLeadingZeroBytes() {
+    return delegate.numberOfLeadingZeroBytes();
+  }
+
+  @Override
+  public boolean hasLeadingZeroByte() {
+    return delegate.hasLeadingZeroByte();
+  }
+
+  @Override
+  public boolean hasLeadingZero() {
+    return delegate.hasLeadingZero();
+  }
+
+  @Override
+  public int bitLength() {
+    return delegate.bitLength();
+  }
+
+  @Override
+  public Bytes and(Bytes other) {
+    return delegate.and(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T and(Bytes other, T result) {
+    return delegate.and(other, result);
+  }
+
+  @Override
+  public Bytes or(Bytes other) {
+    return delegate.or(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T or(Bytes other, T result) {
+    return delegate.or(other, result);
+  }
+
+  @Override
+  public Bytes xor(Bytes other) {
+    return delegate.xor(other);
+  }
+
+  @Override
+  public <T extends MutableBytes> T xor(Bytes other, T result) {
+    return delegate.xor(other, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T not(T result) {
+    return delegate.not(result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftRight(int distance, T result) {
+    return delegate.shiftRight(distance, result);
+  }
+
+  @Override
+  public <T extends MutableBytes> T shiftLeft(int distance, T result) {
+    return delegate.shiftLeft(distance, result);
+  }
+
+  @Override
+  public Bytes slice(int index) {
+    return delegate.slice(index);
+  }
+
+  @Override
+  public Bytes slice(int index, int length) {
+    return delegate.slice(index, length);
+  }
+
+  @Override
+  public Bytes48 copy() {
+    return Bytes48.wrap(delegate.toArray());
+  }
+
+  @Override
+  public MutableBytes48 mutableCopy() {
+    return MutableBytes48.wrap(delegate.toArray());
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination) {
+    delegate.copyTo(destination);
+  }
+
+  @Override
+  public void copyTo(MutableBytes destination, int destinationOffset) {
+    delegate.copyTo(destination, destinationOffset);
+  }
+
+  @Override
+  public void appendTo(ByteBuffer byteBuffer) {
+    delegate.appendTo(byteBuffer);
+  }
+
+  @Override
+  public void appendTo(Buffer buffer) {
+    delegate.appendTo(buffer);
+  }
+
+  @Override
+  public int commonPrefixLength(Bytes other) {
+    return delegate.commonPrefixLength(other);
+  }
+
+  @Override
+  public Bytes commonPrefix(Bytes other) {
+    return delegate.commonPrefix(other);
+  }
+
+  @Override
+  public void update(MessageDigest digest) {
+    delegate.update(digest);
+  }
+
+  @Override
+  public byte[] toArray() {
+    return delegate.toArray();
+  }
+
+  @Override
+  public byte[] toArrayUnsafe() {
+    return delegate.toArrayUnsafe();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public String toHexString() {
+    return delegate.toHexString();
+  }
+
+  @Override
+  public String toShortHexString() {
+    return delegate.toShortHexString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return delegate.equals(obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return delegate.hashCode();
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes.java
new file mode 100644
index 0000000..4dc30d2
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes.java
@@ -0,0 +1,92 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import java.util.Arrays;
+
+class MutableArrayWrappingBytes extends ArrayWrappingBytes implements MutableBytes {
+
+  MutableArrayWrappingBytes(byte[] bytes) {
+    super(bytes);
+  }
+
+  MutableArrayWrappingBytes(byte[] bytes, int offset, int length) {
+    super(bytes, offset, length);
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    // Check bounds because while the array access would throw, the error message would be confusing
+    // for the caller.
+    checkElementIndex(i, length);
+    bytes[offset + i] = b;
+  }
+
+  @Override
+  public MutableBytes increment() {
+    for (int i = length - 1; i >= offset; --i) {
+      if (bytes[i] == (byte) 0xFF) {
+        bytes[i] = (byte) 0x00;
+      } else {
+        ++bytes[i];
+        break;
+      }
+    }
+    return this;
+  }
+
+  @Override
+  public MutableBytes decrement() {
+    for (int i = length - 1; i >= offset; --i) {
+      if (bytes[i] == (byte) 0x00) {
+        bytes[i] = (byte) 0xFF;
+      } else {
+        --bytes[i];
+        break;
+      }
+    }
+    return this;
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    if (i == 0 && length == this.length)
+      return this;
+    if (length == 0)
+      return MutableBytes.EMPTY;
+
+    checkElementIndex(i, this.length);
+    checkArgument(
+        i + length <= this.length,
+        "Specified length %s is too large: the value has size %s and has only %s bytes from %s",
+        length,
+        this.length,
+        this.length - i,
+        i);
+    return length == Bytes32.SIZE ? new MutableArrayWrappingBytes32(bytes, offset + i)
+        : new MutableArrayWrappingBytes(bytes, offset + i, length);
+  }
+
+  @Override
+  public void fill(byte b) {
+    Arrays.fill(bytes, offset, offset + length, b);
+  }
+
+  @Override
+  public Bytes copy() {
+    return new ArrayWrappingBytes(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes32.java
new file mode 100644
index 0000000..c54aeaf
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes32.java
@@ -0,0 +1,34 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+final class MutableArrayWrappingBytes32 extends MutableArrayWrappingBytes implements MutableBytes32 {
+
+  MutableArrayWrappingBytes32(byte[] bytes) {
+    this(bytes, 0);
+  }
+
+  MutableArrayWrappingBytes32(byte[] bytes, int offset) {
+    super(bytes, offset, SIZE);
+  }
+
+  @Override
+  public Bytes32 copy() {
+    return new ArrayWrappingBytes32(toArray());
+  }
+
+  @Override
+  public MutableBytes32 mutableCopy() {
+    return new MutableArrayWrappingBytes32(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes48.java
new file mode 100644
index 0000000..62b8b4f
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes48.java
@@ -0,0 +1,34 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+final class MutableArrayWrappingBytes48 extends MutableArrayWrappingBytes implements MutableBytes48 {
+
+  MutableArrayWrappingBytes48(byte[] bytes) {
+    this(bytes, 0);
+  }
+
+  MutableArrayWrappingBytes48(byte[] bytes, int offset) {
+    super(bytes, offset, SIZE);
+  }
+
+  @Override
+  public Bytes48 copy() {
+    return new ArrayWrappingBytes48(toArray());
+  }
+
+  @Override
+  public MutableBytes48 mutableCopy() {
+    return new MutableArrayWrappingBytes48(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableBufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableBufferWrappingBytes.java
new file mode 100644
index 0000000..b1feff5
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBufferWrappingBytes.java
@@ -0,0 +1,76 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import io.vertx.core.buffer.Buffer;
+
+final class MutableBufferWrappingBytes extends BufferWrappingBytes implements MutableBytes {
+
+  MutableBufferWrappingBytes(Buffer buffer) {
+    super(buffer);
+  }
+
+  MutableBufferWrappingBytes(Buffer buffer, int offset, int length) {
+    super(buffer, offset, length);
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    buffer.setByte(i, b);
+  }
+
+  @Override
+  public void setInt(int i, int value) {
+    buffer.setInt(i, value);
+  }
+
+  @Override
+  public void setLong(int i, long value) {
+    buffer.setLong(i, value);
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    int size = size();
+    if (i == 0 && length == size) {
+      return this;
+    }
+    if (length == 0) {
+      return MutableBytes.EMPTY;
+    }
+
+    checkElementIndex(i, size);
+    checkArgument(
+        i + length <= size,
+        "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+        length,
+        size,
+        size - i,
+        i);
+
+    return new MutableBufferWrappingBytes(buffer.slice(i, i + length));
+  }
+
+  @Override
+  public Bytes copy() {
+    return Bytes.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return MutableBytes.wrap(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufWrappingBytes.java
new file mode 100644
index 0000000..6fe907d
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufWrappingBytes.java
@@ -0,0 +1,81 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import io.netty.buffer.ByteBuf;
+
+final class MutableByteBufWrappingBytes extends ByteBufWrappingBytes implements MutableBytes {
+
+  MutableByteBufWrappingBytes(ByteBuf buffer) {
+    super(buffer);
+  }
+
+  MutableByteBufWrappingBytes(ByteBuf buffer, int offset, int length) {
+    super(buffer, offset, length);
+  }
+
+  @Override
+  public void clear() {
+    byteBuf.setZero(0, byteBuf.capacity());
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    byteBuf.setByte(i, b);
+  }
+
+  @Override
+  public void setInt(int i, int value) {
+    byteBuf.setInt(i, value);
+  }
+
+  @Override
+  public void setLong(int i, long value) {
+    byteBuf.setLong(i, value);
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    int size = size();
+    if (i == 0 && length == size) {
+      return this;
+    }
+    if (length == 0) {
+      return MutableBytes.EMPTY;
+    }
+
+    checkElementIndex(i, size);
+    checkArgument(
+        i + length <= size,
+        "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+        length,
+        size,
+        size - i,
+        i);
+
+    return new MutableByteBufWrappingBytes(byteBuf.slice(i, length));
+  }
+
+  @Override
+  public Bytes copy() {
+    return Bytes.wrap(toArray());
+  }
+
+  @Override
+  public MutableBytes mutableCopy() {
+    return MutableBytes.wrap(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufferWrappingBytes.java
new file mode 100644
index 0000000..283e33a
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufferWrappingBytes.java
@@ -0,0 +1,70 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import java.nio.ByteBuffer;
+
+public class MutableByteBufferWrappingBytes extends ByteBufferWrappingBytes implements MutableBytes {
+
+  MutableByteBufferWrappingBytes(ByteBuffer byteBuffer) {
+    super(byteBuffer);
+  }
+
+  MutableByteBufferWrappingBytes(ByteBuffer byteBuffer, int offset, int length) {
+    super(byteBuffer, offset, length);
+  }
+
+  @Override
+  public void setInt(int i, int value) {
+    byteBuffer.putInt(offset + i, value);
+  }
+
+  @Override
+  public void setLong(int i, long value) {
+    byteBuffer.putLong(offset + i, value);
+  }
+
+  @Override
+  public void set(int i, byte b) {
+    byteBuffer.put(offset + i, b);
+  }
+
+  @Override
+  public MutableBytes mutableSlice(int i, int length) {
+    if (i == 0 && length == this.length) {
+      return this;
+    }
+    if (length == 0) {
+      return MutableBytes.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);
+
+    return new MutableByteBufferWrappingBytes(byteBuffer, offset + i, length);
+  }
+
+  @Override
+  public Bytes copy() {
+    return new ArrayWrappingBytes(toArray());
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes.java
new file mode 100644
index 0000000..3ef2f85
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes.java
@@ -0,0 +1,363 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+import java.nio.ByteBuffer;
+
+import io.netty.buffer.ByteBuf;
+import io.vertx.core.buffer.Buffer;
+
+/**
+ * A mutable {@link Bytes} value.
+ */
+public interface MutableBytes extends Bytes {
+
+  /**
+   * The empty value (with 0 bytes).
+   */
+  MutableBytes EMPTY = wrap(new byte[0]);
+
+  /**
+   * Create a new mutable byte value.
+   *
+   * @param size The size of the returned value.
+   * @return A {@link MutableBytes} value.
+   */
+  static MutableBytes create(int size) {
+    if (size == 32) {
+      return MutableBytes32.create();
+    }
+    return new MutableArrayWrappingBytes(new byte[size]);
+  }
+
+  /**
+   * Wrap a byte array in a {@link MutableBytes} value.
+   *
+   * @param value The value to wrap.
+   * @return A {@link MutableBytes} value wrapping {@code value}.
+   */
+  static MutableBytes wrap(byte[] value) {
+    checkNotNull(value);
+    return new MutableArrayWrappingBytes(value);
+  }
+
+  /**
+   * Wrap a slice of a byte array as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that value is not copied and thus any future update to {@code value} within the slice will be reflected in the
+   * returned value.
+   *
+   * @param value The value to wrap.
+   * @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 expose the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + length} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length > 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > value.length}.
+   */
+  static MutableBytes wrap(byte[] value, int offset, int length) {
+    checkNotNull(value);
+    if (length == 32) {
+      return new MutableArrayWrappingBytes32(value, offset);
+    }
+    return new MutableArrayWrappingBytes(value, offset, length);
+  }
+
+  /**
+   * Wrap a full Vert.x {@link Buffer} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param buffer The buffer to wrap.
+   * @return A {@link MutableBytes} value.
+   */
+  static MutableBytes wrapBuffer(Buffer buffer) {
+    checkNotNull(buffer);
+    if (buffer.length() == 0) {
+      return EMPTY;
+    }
+    return new MutableBufferWrappingBytes(buffer);
+  }
+
+  /**
+   * Wrap a slice of a Vert.x {@link Buffer} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value, and any change to the
+   * returned value will be reflected in the buffer.
+   *
+   * @param buffer The buffer to wrap.
+   * @param offset The offset in {@code buffer} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link MutableBytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (buffer.length() > 0 && offset >=
+   *     buffer.length())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > buffer.length()}.
+   */
+  static MutableBytes wrapBuffer(Buffer buffer, int offset, int size) {
+    checkNotNull(buffer);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new MutableBufferWrappingBytes(buffer, offset, size);
+  }
+
+  /**
+   * Wrap a full Netty {@link ByteBuf} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param byteBuf The {@link ByteBuf} to wrap.
+   * @return A {@link MutableBytes} value.
+   */
+  static MutableBytes wrapByteBuf(ByteBuf byteBuf) {
+    checkNotNull(byteBuf);
+    if (byteBuf.capacity() == 0) {
+      return EMPTY;
+    }
+    return new MutableByteBufWrappingBytes(byteBuf);
+  }
+
+  /**
+   * Wrap a slice of a Netty {@link ByteBuf} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value, and any change to the
+   * returned value will be reflected in the buffer.
+   *
+   * @param byteBuf The {@link ByteBuf} to wrap.
+   * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link MutableBytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (byteBuf.capacity() > 0 && offset >=
+   *     byteBuf.capacity())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > byteBuf.capacity()}.
+   */
+  static MutableBytes wrapByteBuf(ByteBuf byteBuf, int offset, int size) {
+    checkNotNull(byteBuf);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new MutableByteBufWrappingBytes(byteBuf, offset, size);
+  }
+
+  /**
+   * Wrap a full Java NIO {@link ByteBuffer} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value.
+   *
+   * @param byteBuffer The {@link ByteBuffer} to wrap.
+   * @return A {@link MutableBytes} value.
+   */
+  static MutableBytes wrapByteBuffer(ByteBuffer byteBuffer) {
+    checkNotNull(byteBuffer);
+    if (byteBuffer.limit() == 0) {
+      return EMPTY;
+    }
+    return new MutableByteBufferWrappingBytes(byteBuffer);
+  }
+
+  /**
+   * Wrap a slice of a Java NIO {@link ByteBuffer} as a {@link MutableBytes} value.
+   *
+   * <p>
+   * Note that any change to the content of the buffer may be reflected in the returned value, and any change to the
+   * returned value will be reflected in the buffer.
+   *
+   * @param byteBuffer The {@link ByteBuffer} to wrap.
+   * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned value. That is,
+   *        {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}.
+   * @param size The size of the returned value.
+   * @return A {@link MutableBytes} value.
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (byteBuffer.limit() > 0 && offset >=
+   *     byteBuffer.limit())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + length > byteBuffer.limit()}.
+   */
+  static MutableBytes wrapByteBuffer(ByteBuffer byteBuffer, int offset, int size) {
+    checkNotNull(byteBuffer);
+    if (size == 0) {
+      return EMPTY;
+    }
+    return new MutableByteBufferWrappingBytes(byteBuffer, offset, size);
+  }
+
+  /**
+   * Create a value that contains the specified bytes in their specified order.
+   *
+   * @param bytes The bytes that must compose the returned value.
+   * @return A value containing the specified bytes.
+   */
+  static MutableBytes of(byte... bytes) {
+    return wrap(bytes);
+  }
+
+  /**
+   * Create a value that contains the specified bytes in their specified order.
+   *
+   * @param bytes The bytes.
+   * @return A value containing bytes are the one from {@code bytes}.
+   * @throws IllegalArgumentException if any of the specified would be truncated when storing as a byte.
+   */
+  static MutableBytes of(int... bytes) {
+    byte[] result = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      int b = bytes[i];
+      checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b);
+      result[i] = (byte) b;
+    }
+    return wrap(result);
+  }
+
+  /**
+   * Set a byte in this value.
+   *
+   * @param i The index of the byte to set.
+   * @param b The value to set that byte to.
+   * @throws IndexOutOfBoundsException if {@code i < 0} or {i &gt;= size()}.
+   */
+  void set(int i, byte b);
+
+  /**
+   * Set the 4 bytes starting at the specified index to the specified integer value.
+   *
+   * @param i The index, which must less than or equal to {@code size() - 4}.
+   * @param value The integer value.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 4}.
+   */
+  default void setInt(int i, int value) {
+    int size = size();
+    checkElementIndex(i, size);
+    if (i > (size - 4)) {
+      throw new IndexOutOfBoundsException(
+          format("Value of size %s has not enough bytes to write a 4 bytes int from index %s", size, i));
+    }
+
+    set(i++, (byte) (value >>> 24));
+    set(i++, (byte) ((value >>> 16) & 0xFF));
+    set(i++, (byte) ((value >>> 8) & 0xFF));
+    set(i, (byte) (value & 0xFF));
+  }
+
+  /**
+   * Set the 8 bytes starting at the specified index to the specified long value.
+   *
+   * @param i The index, which must less than or equal to {@code size() - 8}.
+   * @param value The long value.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {@code i &gt; size() - 8}.
+   */
+  default void setLong(int i, long value) {
+    int size = size();
+    checkElementIndex(i, size);
+    if (i > (size - 8)) {
+      throw new IndexOutOfBoundsException(
+          format("Value of size %s has not enough bytes to write a 8 bytes long from index %s", size, i));
+    }
+
+    set(i++, (byte) (value >>> 56));
+    set(i++, (byte) ((value >>> 48) & 0xFF));
+    set(i++, (byte) ((value >>> 40) & 0xFF));
+    set(i++, (byte) ((value >>> 32) & 0xFF));
+    set(i++, (byte) ((value >>> 24) & 0xFF));
+    set(i++, (byte) ((value >>> 16) & 0xFF));
+    set(i++, (byte) ((value >>> 8) & 0xFF));
+    set(i, (byte) (value & 0xFF));
+  }
+
+  /**
+   * Increments the value of the bytes by 1, treating the value as big endian.
+   *
+   * If incrementing overflows the value then all bits flip, i.e. incrementing 0xFFFF will return 0x0000.
+   *
+   * @return this value
+   */
+  default MutableBytes increment() {
+    for (int i = size() - 1; i >= 0; --i) {
+      if (get(i) == (byte) 0xFF) {
+        set(i, (byte) 0x00);
+      } else {
+        byte currentValue = get(i);
+        set(i, ++currentValue);
+        break;
+      }
+    }
+    return this;
+  }
+
+  /**
+   * Decrements the value of the bytes by 1, treating the value as big endian.
+   *
+   * If decrementing underflows the value then all bits flip, i.e. decrementing 0x0000 will return 0xFFFF.
+   *
+   * @return this value
+   */
+  default MutableBytes decrement() {
+    for (int i = size() - 1; i >= 0; --i) {
+      if (get(i) == (byte) 0x00) {
+        set(i, (byte) 0xFF);
+      } else {
+        byte currentValue = get(i);
+        set(i, --currentValue);
+        break;
+      }
+    }
+    return this;
+  }
+
+  /**
+   * Create a mutable slice of the bytes of this value.
+   *
+   * <p>
+   * Note: the resulting slice is only a view over the original value. Holding a reference to the returned slice may
+   * hold more memory than the slide represents. Use {@link #copy} on the returned slice to avoid this.
+   *
+   * @param i The start index for the slice.
+   * @param length The length of the resulting value.
+   * @return A new mutable view over the bytes of this value from index {@code i} (included) to index {@code i + length}
+   *         (excluded).
+   * @throws IllegalArgumentException if {@code length &lt; 0}.
+   * @throws IndexOutOfBoundsException if {@code i &lt; 0} or {i &gt;= size()} or {i + length &gt; size()} .
+   */
+  MutableBytes mutableSlice(int i, int length);
+
+  /**
+   * Fill all the bytes of this value with the specified byte.
+   *
+   * @param b The byte to use to fill the value.
+   */
+  default void fill(byte b) {
+    int size = size();
+    for (int i = 0; i < size; i++) {
+      set(i, b);
+    }
+  }
+
+  /**
+   * Set all bytes in this value to 0.
+   */
+  default void clear() {
+    fill((byte) 0);
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes32.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes32.java
new file mode 100644
index 0000000..b055c37
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes32.java
@@ -0,0 +1,110 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A mutable {@link Bytes32}, that is a mutable {@link Bytes} value of exactly 32 bytes.
+ */
+public interface MutableBytes32 extends MutableBytes, Bytes32 {
+
+  /**
+   * Create a new mutable 32 bytes value.
+   *
+   * @return A newly allocated {@link MutableBytes} value.
+   */
+  static MutableBytes32 create() {
+    return new MutableArrayWrappingBytes32(new byte[SIZE]);
+  }
+
+  /**
+   * Wrap a 32 bytes array as a mutable 32 bytes value.
+   *
+   * @param value The value to wrap.
+   * @return A {@link MutableBytes32} wrapping {@code value}.
+   * @throws IllegalArgumentException if {@code value.length != 32}.
+   */
+  static MutableBytes32 wrap(byte[] value) {
+    checkNotNull(value);
+    return new MutableArrayWrappingBytes32(value);
+  }
+
+  /**
+   * Wrap a the provided array as a {@link MutableBytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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 MutableBytes32} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 32} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length &gt; 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 32 &gt; value.length}.
+   */
+  static MutableBytes32 wrap(byte[] value, int offset) {
+    checkNotNull(value);
+    return new MutableArrayWrappingBytes32(value, offset);
+  }
+
+  /**
+   * Wrap a the provided value, which must be of size 32, as a {@link MutableBytes32}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param value The bytes to wrap.
+   * @return A {@link MutableBytes32} that exposes the bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() != 32}.
+   */
+  static MutableBytes32 wrap(MutableBytes value) {
+    checkNotNull(value);
+    if (value instanceof MutableBytes32) {
+      return (MutableBytes32) value;
+    }
+    return DelegatingMutableBytes32.delegateTo(value);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided value as a {@link MutableBytes32}.
+   *
+   * <p>
+   * Note that the value is not copied, and thus any future update to {@code value} within the wrapped parts will be
+   * reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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.get(i)}.
+   * @return A {@link Bytes32} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 32} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.size() &gt; 0 && offset >=
+   *     value.size())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 32 &gt; value.size()}.
+   */
+  static MutableBytes32 wrap(MutableBytes value, int offset) {
+    checkNotNull(value);
+    if (value instanceof MutableBytes32) {
+      return (MutableBytes32) value;
+    }
+    MutableBytes slice = value.mutableSlice(offset, Bytes32.SIZE);
+    if (slice instanceof MutableBytes32) {
+      return (MutableBytes32) slice;
+    }
+    return DelegatingMutableBytes32.delegateTo(slice);
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes48.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes48.java
new file mode 100644
index 0000000..2e267e8
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes48.java
@@ -0,0 +1,110 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A mutable {@link Bytes48}, that is a mutable {@link Bytes} value of exactly 48 bytes.
+ */
+public interface MutableBytes48 extends MutableBytes, Bytes48 {
+
+  /**
+   * Create a new mutable 48 bytes value.
+   *
+   * @return A newly allocated {@link MutableBytes} value.
+   */
+  static MutableBytes48 create() {
+    return new MutableArrayWrappingBytes48(new byte[SIZE]);
+  }
+
+  /**
+   * Wrap a 48 bytes array as a mutable 48 bytes value.
+   *
+   * @param value The value to wrap.
+   * @return A {@link MutableBytes48} wrapping {@code value}.
+   * @throws IllegalArgumentException if {@code value.length != 48}.
+   */
+  static MutableBytes48 wrap(byte[] value) {
+    checkNotNull(value);
+    return new MutableArrayWrappingBytes48(value);
+  }
+
+  /**
+   * Wrap a the provided array as a {@link MutableBytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} within the wrapped parts
+   * will be reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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 MutableBytes48} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 48} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.length &gt; 0 && offset >=
+   *     value.length)}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 48 &gt; value.length}.
+   */
+  static MutableBytes48 wrap(byte[] value, int offset) {
+    checkNotNull(value);
+    return new MutableArrayWrappingBytes48(value, offset);
+  }
+
+  /**
+   * Wrap a the provided value, which must be of size 48, as a {@link MutableBytes48}.
+   *
+   * <p>
+   * Note that value is not copied, only wrapped, and thus any future update to {@code value} will be reflected in the
+   * returned value.
+   *
+   * @param value The bytes to wrap.
+   * @return A {@link MutableBytes48} that exposes the bytes of {@code value}.
+   * @throws IllegalArgumentException if {@code value.size() != 48}.
+   */
+  static MutableBytes48 wrap(MutableBytes value) {
+    checkNotNull(value);
+    if (value instanceof MutableBytes48) {
+      return (MutableBytes48) value;
+    }
+    return DelegatingMutableBytes48.delegateTo(value);
+  }
+
+  /**
+   * Wrap a slice/sub-part of the provided value as a {@link MutableBytes48}.
+   *
+   * <p>
+   * Note that the value is not copied, and thus any future update to {@code value} within the wrapped parts will be
+   * reflected in the returned value.
+   *
+   * @param value The bytes to wrap.
+   * @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.get(i)}.
+   * @return A {@link Bytes48} that exposes the bytes of {@code value} from {@code offset} (inclusive) to
+   *         {@code offset + 48} (exclusive).
+   * @throws IndexOutOfBoundsException if {@code offset &lt; 0 || (value.size() &gt; 0 && offset >=
+   *     value.size())}.
+   * @throws IllegalArgumentException if {@code length &lt; 0 || offset + 48 &gt; value.size()}.
+   */
+  static MutableBytes48 wrap(MutableBytes value, int offset) {
+    checkNotNull(value);
+    if (value instanceof MutableBytes48) {
+      return (MutableBytes48) value;
+    }
+    MutableBytes slice = value.mutableSlice(offset, Bytes48.SIZE);
+    if (slice instanceof MutableBytes48) {
+      return (MutableBytes48) slice;
+    }
+    return DelegatingMutableBytes48.delegateTo(slice);
+  }
+}
diff --git a/bytes/src/main/java/net/consensys/cava/bytes/package-info.java b/bytes/src/main/java/net/consensys/cava/bytes/package-info.java
new file mode 100644
index 0000000..3707b77
--- /dev/null
+++ b/bytes/src/main/java/net/consensys/cava/bytes/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * Classes and utilities for working with byte arrays.
+ *
+ * <p>
+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency
+ * 'net.consensys.cava:cava-bytes' (cava-bytes.jar).
+ */
+@ParametersAreNonnullByDefault
+package net.consensys.cava.bytes;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/BufferBytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/BufferBytesTest.java
new file mode 100644
index 0000000..db1719e
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/BufferBytesTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import io.vertx.core.buffer.Buffer;
+
+class BufferBytesTest extends CommonBytesTests {
+
+  @Override
+  Bytes h(String hex) {
+    return Bytes.wrapBuffer(Buffer.buffer(Bytes.fromHexString(hex).toArrayUnsafe()));
+  }
+
+  @Override
+  MutableBytes m(int size) {
+    return MutableBytes.wrapBuffer(Buffer.buffer(new byte[size]));
+  }
+
+  @Override
+  Bytes w(byte[] bytes) {
+    return Bytes.wrapBuffer(Buffer.buffer(Bytes.of(bytes).toArray()));
+  }
+
+  @Override
+  Bytes of(int... bytes) {
+    return Bytes.wrapBuffer(Buffer.buffer(Bytes.of(bytes).toArray()));
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/ByteBufBytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufBytesTest.java
new file mode 100644
index 0000000..06096eb
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufBytesTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import io.netty.buffer.Unpooled;
+
+class ByteBufBytesTest extends CommonBytesTests {
+
+  @Override
+  Bytes h(String hex) {
+    return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.fromHexString(hex).toArrayUnsafe()));
+  }
+
+  @Override
+  MutableBytes m(int size) {
+    return MutableBytes.wrapByteBuf(Unpooled.copiedBuffer(new byte[size]));
+  }
+
+  @Override
+  Bytes w(byte[] bytes) {
+    return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.of(bytes).toArray()));
+  }
+
+  @Override
+  Bytes of(int... bytes) {
+    return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.of(bytes).toArray()));
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/ByteBufferBytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufferBytesTest.java
new file mode 100644
index 0000000..554f086
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufferBytesTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import java.nio.ByteBuffer;
+
+class ByteBufferBytesTest extends CommonBytesTests {
+
+  @Override
+  Bytes h(String hex) {
+    return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.fromHexString(hex).toArrayUnsafe()));
+  }
+
+  @Override
+  MutableBytes m(int size) {
+    return MutableBytes.wrapByteBuffer(ByteBuffer.allocate(size));
+  }
+
+  @Override
+  Bytes w(byte[] bytes) {
+    return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.of(bytes).toArray()));
+  }
+
+  @Override
+  Bytes of(int... bytes) {
+    return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.of(bytes).toArray()));
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/Bytes32Test.java b/bytes/src/test/java/net/consensys/cava/bytes/Bytes32Test.java
new file mode 100644
index 0000000..04df458
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/Bytes32Test.java
@@ -0,0 +1,69 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class Bytes32Test {
+
+  @Test
+  void failsWhenWrappingArraySmallerThan32() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes32.wrap(new byte[31]));
+    assertEquals("Expected 32 bytes but got 31", exception.getMessage());
+  }
+
+  @Test
+  void failsWhenWrappingArrayLargerThan32() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes32.wrap(new byte[33]));
+    assertEquals("Expected 32 bytes but got 33", exception.getMessage());
+  }
+
+  @Test
+  void leftPadAValueToBytes32() {
+    Bytes32 b32 = Bytes32.leftPad(Bytes.of(1, 2, 3));
+    assertEquals(32, b32.size());
+    for (int i = 0; i < 28; ++i) {
+      assertEquals((byte) 0, b32.get(i));
+    }
+    assertEquals((byte) 1, b32.get(29));
+    assertEquals((byte) 2, b32.get(30));
+    assertEquals((byte) 3, b32.get(31));
+  }
+
+  @Test
+  void rightPadAValueToBytes32() {
+    Bytes32 b32 = Bytes32.rightPad(Bytes.of(1, 2, 3));
+    assertEquals(32, b32.size());
+    for (int i = 3; i < 32; ++i) {
+      assertEquals((byte) 0, b32.get(i));
+    }
+    assertEquals((byte) 1, b32.get(0));
+    assertEquals((byte) 2, b32.get(1));
+    assertEquals((byte) 3, b32.get(2));
+  }
+
+  @Test
+  void failsWhenLeftPaddingValueLargerThan32() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes32.leftPad(MutableBytes.create(33)));
+    assertEquals("Expected at most 32 bytes but got 33", exception.getMessage());
+  }
+
+  @Test
+  void failsWhenRightPaddingValueLargerThan32() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes32.rightPad(MutableBytes.create(33)));
+    assertEquals("Expected at most 32 bytes but got 33", exception.getMessage());
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/Bytes48Test.java b/bytes/src/test/java/net/consensys/cava/bytes/Bytes48Test.java
new file mode 100644
index 0000000..89ca1d2
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/Bytes48Test.java
@@ -0,0 +1,69 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class Bytes48Test {
+
+  @Test
+  void failsWhenWrappingArraySmallerThan48() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes48.wrap(new byte[31]));
+    assertEquals("Expected 48 bytes but got 31", exception.getMessage());
+  }
+
+  @Test
+  void failsWhenWrappingArrayLargerThan48() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes48.wrap(new byte[33]));
+    assertEquals("Expected 48 bytes but got 33", exception.getMessage());
+  }
+
+  @Test
+  void rightPadAValueToBytes48() {
+    Bytes48 b48 = Bytes48.rightPad(Bytes.of(1, 2, 3));
+    assertEquals(48, b48.size());
+    for (int i = 3; i < 48; ++i) {
+      assertEquals((byte) 0, b48.get(i));
+    }
+    assertEquals((byte) 1, b48.get(0));
+    assertEquals((byte) 2, b48.get(1));
+    assertEquals((byte) 3, b48.get(2));
+  }
+
+  @Test
+  void leftPadAValueToBytes48() {
+    Bytes48 b48 = Bytes48.leftPad(Bytes.of(1, 2, 3));
+    assertEquals(48, b48.size());
+    for (int i = 0; i < 28; ++i) {
+      assertEquals((byte) 0, b48.get(i));
+    }
+    assertEquals((byte) 1, b48.get(45));
+    assertEquals((byte) 2, b48.get(46));
+    assertEquals((byte) 3, b48.get(47));
+  }
+
+  @Test
+  void failsWhenLeftPaddingValueLargerThan48() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes48.leftPad(MutableBytes.create(49)));
+    assertEquals("Expected at most 48 bytes but got 49", exception.getMessage());
+  }
+
+  @Test
+  void failsWhenRightPaddingValueLargerThan48() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes48.rightPad(MutableBytes.create(49)));
+    assertEquals("Expected at most 48 bytes but got 49", exception.getMessage());
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/BytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/BytesTest.java
new file mode 100644
index 0000000..eb1d0a5
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/BytesTest.java
@@ -0,0 +1,465 @@
+/*
+ * 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 net.consensys.cava.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.assertThrows;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+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 BytesTest 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.wrap(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);
+  }
+
+  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);
+  }
+
+  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 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 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);
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/CommonBytesTests.java b/bytes/src/test/java/net/consensys/cava/bytes/CommonBytesTests.java
new file mode 100644
index 0000000..b16c74a
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/CommonBytesTests.java
@@ -0,0 +1,659 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.function.Function;
+
+import io.netty.buffer.Unpooled;
+import io.vertx.core.buffer.Buffer;
+import org.junit.jupiter.api.Test;
+
+abstract class CommonBytesTests {
+
+  abstract Bytes h(String hex);
+
+  abstract MutableBytes m(int size);
+
+  abstract Bytes w(byte[] bytes);
+
+  abstract Bytes of(int... bytes);
+
+  BigInteger bi(String decimal) {
+    return new BigInteger(decimal);
+  }
+
+  @Test
+  void asUnsignedBigInteger() {
+    // Make sure things are interpreted unsigned.
+    assertEquals(bi("255"), h("0xFF").toUnsignedBigInteger());
+
+    // Try 2^100 + Long.MAX_VALUE, as an easy to define a big not too special big integer.
+    BigInteger expected = BigInteger.valueOf(2).pow(100).add(BigInteger.valueOf(Long.MAX_VALUE));
+
+    // 2^100 is a one followed by 100 zeros, that's 12 bytes of zeros (=96) plus 4 more zeros (so
+    // 0x10 == 16).
+    MutableBytes v = m(13);
+    v.set(0, (byte) 16);
+    v.setLong(v.size() - 8, Long.MAX_VALUE);
+    assertEquals(expected, v.toUnsignedBigInteger());
+  }
+
+  @Test
+  void testAsSignedBigInteger() {
+    // Make sure things are interpreted signed.
+    assertEquals(bi("-1"), h("0xFF").toBigInteger());
+
+    // Try 2^100 + Long.MAX_VALUE, as an easy to define a big but not too special big integer.
+    BigInteger expected = BigInteger.valueOf(2).pow(100).add(BigInteger.valueOf(Long.MAX_VALUE));
+
+    // 2^100 is a one followed by 100 zeros, that's 12 bytes of zeros (=96) plus 4 more zeros (so
+    // 0x10 == 16).
+    MutableBytes v = m(13);
+    v.set(0, (byte) 16);
+    v.setLong(v.size() - 8, Long.MAX_VALUE);
+    assertEquals(expected, v.toBigInteger());
+
+    // And for a large negative one, we use -(2^100 + Long.MAX_VALUE), which is:
+    //  2^100 + Long.MAX_VALUE = 0x10(4 bytes of 0)7F(  7 bytes of 1)
+    //                 inverse = 0xEF(4 bytes of 1)80(  7 bytes of 0)
+    //                      +1 = 0xEF(4 bytes of 1)80(6 bytes of 0)01
+    expected = expected.negate();
+    v = m(13);
+    v.set(0, (byte) 0xEF);
+    for (int i = 1; i < 5; i++) {
+      v.set(i, (byte) 0xFF);
+    }
+    v.set(5, (byte) 0x80);
+    // 6 bytes of 0
+    v.set(12, (byte) 1);
+    assertEquals(expected, v.toBigInteger());
+  }
+
+  @Test
+  void testSize() {
+    assertEquals(0, w(new byte[0]).size());
+    assertEquals(1, w(new byte[1]).size());
+    assertEquals(10, w(new byte[10]).size());
+  }
+
+  @Test
+  void testGet() {
+    Bytes v = w(new byte[] {1, 2, 3, 4});
+    assertEquals((int) (byte) 1, (int) v.get(0));
+    assertEquals((int) (byte) 2, (int) v.get(1));
+    assertEquals((int) (byte) 3, (int) v.get(2));
+    assertEquals((int) (byte) 4, (int) v.get(3));
+  }
+
+  @Test
+  void testGetNegativeIndex() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).get(-1));
+  }
+
+  @Test
+  void testGetOutOfBound() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).get(4));
+  }
+
+  @Test
+  void testGetInt() {
+    Bytes value = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1});
+
+    // 0x00000100 = 256
+    assertEquals(256, value.getInt(0));
+    // 0x000100FF = 65536 + 255 = 65791
+    assertEquals(65791, value.getInt(1));
+    // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751
+    assertEquals(16842751, value.getInt(2));
+    // 0xFFFFFFFF = -1
+    assertEquals(-1, value.getInt(4));
+  }
+
+  @Test
+  void testGetIntNegativeIndex() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(-1));
+  }
+
+  @Test
+  void testGetIntOutOfBound() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(4));
+  }
+
+  @Test
+  void testGetIntNotEnoughBytes() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(1));
+  }
+
+  @Test
+  void testAsInt() {
+    assertEquals(0, Bytes.EMPTY.toInt());
+    Bytes value1 = w(new byte[] {0, 0, 1, 0});
+    // 0x00000100 = 256
+    assertEquals(256, value1.toInt());
+    assertEquals(256, value1.slice(2).toInt());
+
+    Bytes value2 = w(new byte[] {0, 1, 0, -1});
+    // 0x000100FF = 65536 + 255 = 65791
+    assertEquals(65791, value2.toInt());
+    assertEquals(65791, value2.slice(1).toInt());
+
+    Bytes value3 = w(new byte[] {1, 0, -1, -1});
+    // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751
+    assertEquals(16842751, value3.toInt());
+
+    Bytes value4 = w(new byte[] {-1, -1, -1, -1});
+    // 0xFFFFFFFF = -1
+    assertEquals(-1, value4.toInt());
+  }
+
+  @Test
+  void testAsIntTooManyBytes() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> w(new byte[] {1, 2, 3, 4, 5}).toInt());
+    assertEquals("Value of size 5 has more than 4 bytes", exception.getMessage());
+  }
+
+  @Test
+  void testGetLong() {
+    Bytes value1 = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1, 0, 0});
+    // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071
+    assertEquals(1103806595071L, value1.getLong(0));
+    // 0x 000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176
+    assertEquals(282574488338176L, value1.getLong(1));
+
+    Bytes value2 = w(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1});
+    assertEquals(-1L, value2.getLong(0));
+  }
+
+  @Test
+  void testGetLongNegativeIndex() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(-1));
+  }
+
+  @Test
+  void testGetLongOutOfBound() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(8));
+  }
+
+  @Test
+  void testGetLongNotEnoughBytes() {
+    assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getLong(0));
+  }
+
+  @Test
+  void testAsLong() {
+    assertEquals(0, Bytes.EMPTY.toLong());
+    Bytes value1 = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1});
+    // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071
+    assertEquals(1103806595071L, value1.toLong());
+    assertEquals(1103806595071L, value1.slice(2).toLong());
+    Bytes value2 = w(new byte[] {0, 1, 0, -1, -1, -1, -1, 0});
+    // 0x000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176
+    assertEquals(282574488338176L, value2.toLong());
+    assertEquals(282574488338176L, value2.slice(1).toLong());
+
+    Bytes value3 = w(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1});
+    assertEquals(-1L, value3.toLong());
+  }
+
+  @Test
+  void testAsLongTooManyBytes() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}).toLong());
+    assertEquals("Value of size 9 has more than 8 bytes", exception.getMessage());
+  }
+
+  @Test
+  void testSlice() {
+    assertEquals(h("0x"), h("0x0123456789").slice(0, 0));
+    assertEquals(h("0x"), h("0x0123456789").slice(2, 0));
+    assertEquals(h("0x01"), h("0x0123456789").slice(0, 1));
+    assertEquals(h("0x0123"), h("0x0123456789").slice(0, 2));
+
+    assertEquals(h("0x4567"), h("0x0123456789").slice(2, 2));
+    assertEquals(h("0x23456789"), h("0x0123456789").slice(1, 4));
+  }
+
+  @Test
+  void testSliceNegativeOffset() {
+    assertThrows(IndexOutOfBoundsException.class, () -> h("0x012345").slice(-1, 2));
+  }
+
+  @Test
+  void testSliceOffsetOutOfBound() {
+    assertThrows(IndexOutOfBoundsException.class, () -> h("0x012345").slice(3, 2));
+  }
+
+  @Test
+  void testSliceTooLong() {
+    Throwable exception = assertThrows(IllegalArgumentException.class, () -> h("0x012345").slice(1, 3));
+    assertEquals(
+        "Provided length 3 is too big: the value has size 3 and has only 2 bytes from 1",
+        exception.getMessage());
+  }
+
+  @Test
+  void testMutableCopy() {
+    Bytes v = h("0x012345");
+    MutableBytes mutableCopy = v.mutableCopy();
+
+    // Initially, copy must be equal.
+    assertEquals(mutableCopy, v);
+
+    // Upon modification, original should not have been modified.
+    mutableCopy.set(0, (byte) -1);
+    assertNotEquals(mutableCopy, v);
+    assertEquals(h("0x012345"), v);
+    assertEquals(h("0xFF2345"), mutableCopy);
+  }
+
+  @Test
+  void testCopyTo() {
+    MutableBytes dest;
+
+    // The follow does nothing, but simply making sure it doesn't throw.
+    dest = MutableBytes.EMPTY;
+    Bytes.EMPTY.copyTo(dest);
+    assertEquals(Bytes.EMPTY, dest);
+
+    dest = MutableBytes.create(1);
+    of(1).copyTo(dest);
+    assertEquals(h("0x01"), dest);
+
+    dest = MutableBytes.create(1);
+    of(10).copyTo(dest);
+    assertEquals(h("0x0A"), dest);
+
+    dest = MutableBytes.create(2);
+    of(0xff, 0x03).copyTo(dest);
+    assertEquals(h("0xFF03"), dest);
+
+    dest = MutableBytes.create(4);
+    of(0xff, 0x03).copyTo(dest.mutableSlice(1, 2));
+    assertEquals(h("0x00FF0300"), dest);
+  }
+
+  @Test
+  void testCopyToTooSmall() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> of(1, 2, 3).copyTo(MutableBytes.create(2)));
+    assertEquals("Cannot copy 3 bytes to destination of non-equal size 2", exception.getMessage());
+  }
+
+  @Test
+  void testCopyToTooBig() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> of(1, 2, 3).copyTo(MutableBytes.create(4)));
+    assertEquals("Cannot copy 3 bytes to destination of non-equal size 4", exception.getMessage());
+  }
+
+  @Test
+  void testCopyToWithOffset() {
+    MutableBytes dest;
+
+    dest = MutableBytes.wrap(new byte[] {1, 2, 3});
+    Bytes.EMPTY.copyTo(dest, 0);
+    assertEquals(h("0x010203"), dest);
+
+    dest = MutableBytes.wrap(new byte[] {1, 2, 3});
+    of(1).copyTo(dest, 1);
+    assertEquals(h("0x010103"), dest);
+
+    dest = MutableBytes.wrap(new byte[] {1, 2, 3});
+    of(2).copyTo(dest, 0);
+    assertEquals(h("0x020203"), dest);
+
+    dest = MutableBytes.wrap(new byte[] {1, 2, 3});
+    of(1, 1).copyTo(dest, 1);
+    assertEquals(h("0x010101"), dest);
+
+    dest = MutableBytes.create(4);
+    of(0xff, 0x03).copyTo(dest, 1);
+    assertEquals(h("0x00FF0300"), dest);
+  }
+
+  @Test
+  void testCopyToWithOffsetTooSmall() {
+    Throwable exception =
+        assertThrows(IllegalArgumentException.class, () -> of(1, 2, 3).copyTo(MutableBytes.create(4), 2));
+    assertEquals("Cannot copy 3 bytes, destination has only 2 bytes from index 2", exception.getMessage());
+  }
+
+  @Test
+  void testCopyToWithNegativeOffset() {
+    assertThrows(IndexOutOfBoundsException.class, () -> of(1, 2, 3).copyTo(MutableBytes.create(10), -1));
+  }
+
+  @Test
+  void testCopyToWithOutOfBoundIndex() {
+    assertThrows(IndexOutOfBoundsException.class, () -> of(1, 2, 3).copyTo(MutableBytes.create(10), 10));
+  }
+
+  @Test
+  void testAppendTo() {
+    testAppendTo(Bytes.EMPTY, Buffer.buffer(), Bytes.EMPTY);
+    testAppendTo(Bytes.EMPTY, Buffer.buffer(h("0x1234").toArrayUnsafe()), h("0x1234"));
+    testAppendTo(h("0x1234"), Buffer.buffer(), h("0x1234"));
+    testAppendTo(h("0x5678"), Buffer.buffer(h("0x1234").toArrayUnsafe()), h("0x12345678"));
+  }
+
+  private void testAppendTo(Bytes toAppend, Buffer buffer, Bytes expected) {
+    toAppend.appendTo(buffer);
+    assertEquals(expected, Bytes.wrap(buffer.getBytes()));
+  }
+
+  @Test
+  void testIsZero() {
+    assertTrue(Bytes.EMPTY.isZero());
+    assertTrue(Bytes.of(0).isZero());
+    assertTrue(Bytes.of(0, 0, 0).isZero());
+
+    assertFalse(Bytes.of(1).isZero());
+    assertFalse(Bytes.of(1, 0, 0).isZero());
+    assertFalse(Bytes.of(0, 0, 1).isZero());
+    assertFalse(Bytes.of(0, 0, 1, 0, 0).isZero());
+  }
+
+  @Test
+  void testIsEmpty() {
+    assertTrue(Bytes.EMPTY.isEmpty());
+
+    assertFalse(Bytes.of(0).isEmpty());
+    assertFalse(Bytes.of(0, 0, 0).isEmpty());
+    assertFalse(Bytes.of(1).isEmpty());
+  }
+
+  @Test
+  void findsCommonPrefix() {
+    Bytes v = Bytes.of(1, 2, 3, 4, 5, 6, 7);
+    Bytes o = Bytes.of(1, 2, 3, 4, 4, 3, 2);
+    assertEquals(4, v.commonPrefixLength(o));
+    assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o));
+  }
+
+  @Test
+  void findsCommonPrefixOfShorter() {
+    Bytes v = Bytes.of(1, 2, 3, 4, 5, 6, 7);
+    Bytes o = Bytes.of(1, 2, 3, 4);
+    assertEquals(4, v.commonPrefixLength(o));
+    assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o));
+  }
+
+  @Test
+  void findsCommonPrefixOfLonger() {
+    Bytes v = Bytes.of(1, 2, 3, 4);
+    Bytes o = Bytes.of(1, 2, 3, 4, 4, 3, 2);
+    assertEquals(4, v.commonPrefixLength(o));
+    assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o));
+  }
+
+  @Test
+  void findsCommonPrefixOfSliced() {
+    Bytes v = Bytes.of(1, 2, 3, 4).slice(2, 2);
+    Bytes o = Bytes.of(3, 4, 3, 3, 2).slice(3, 2);
+    assertEquals(1, v.commonPrefixLength(o));
+    assertEquals(Bytes.of(3), v.commonPrefix(o));
+  }
+
+  @Test
+  void testTrimLeadingZeroes() {
+    assertEquals(h("0x"), h("0x").trimLeadingZeros());
+    assertEquals(h("0x"), h("0x00").trimLeadingZeros());
+    assertEquals(h("0x"), h("0x00000000").trimLeadingZeros());
+
+    assertEquals(h("0x01"), h("0x01").trimLeadingZeros());
+    assertEquals(h("0x01"), h("0x00000001").trimLeadingZeros());
+
+    assertEquals(h("0x3010"), h("0x3010").trimLeadingZeros());
+    assertEquals(h("0x3010"), h("0x00003010").trimLeadingZeros());
+
+    assertEquals(h("0xFFFFFFFF"), h("0xFFFFFFFF").trimLeadingZeros());
+    assertEquals(h("0xFFFFFFFF"), h("0x000000000000FFFFFFFF").trimLeadingZeros());
+  }
+
+  @Test
+  void slideToEnd() {
+    assertEquals(Bytes.of(1, 2, 3, 4), Bytes.of(1, 2, 3, 4).slice(0));
+    assertEquals(Bytes.of(2, 3, 4), Bytes.of(1, 2, 3, 4).slice(1));
+    assertEquals(Bytes.of(3, 4), Bytes.of(1, 2, 3, 4).slice(2));
+    assertEquals(Bytes.of(4), Bytes.of(1, 2, 3, 4).slice(3));
+  }
+
+  @Test
+  void slicePastEndReturnsEmpty() {
+    assertEquals(Bytes.EMPTY, Bytes.of(1, 2, 3, 4).slice(4));
+    assertEquals(Bytes.EMPTY, Bytes.of(1, 2, 3, 4).slice(5));
+  }
+
+  @Test
+  void testUpdate() throws NoSuchAlgorithmException {
+    // Digest the same byte array in 4 ways:
+    //  1) directly from the array
+    //  2) after wrapped using the update() method
+    //  3) after wrapped and copied using the update() method
+    //  4) after wrapped but getting the byte manually
+    // and check all compute the same digest.
+    MessageDigest md1 = MessageDigest.getInstance("SHA-1");
+    MessageDigest md2 = MessageDigest.getInstance("SHA-1");
+    MessageDigest md3 = MessageDigest.getInstance("SHA-1");
+    MessageDigest md4 = MessageDigest.getInstance("SHA-1");
+
+    byte[] toDigest = new BigInteger("12324029423415041783577517238472017314").toByteArray();
+    Bytes wrapped = w(toDigest);
+
+    byte[] digest1 = md1.digest(toDigest);
+
+    wrapped.update(md2);
+    byte[] digest2 = md2.digest();
+
+    wrapped.copy().update(md3);
+    byte[] digest3 = md3.digest();
+
+    for (int i = 0; i < wrapped.size(); i++)
+      md4.update(wrapped.get(i));
+    byte[] digest4 = md4.digest();
+
+    assertArrayEquals(digest2, digest1);
+    assertArrayEquals(digest3, digest1);
+    assertArrayEquals(digest4, digest1);
+  }
+
+  @Test
+  void testArrayExtraction() {
+    // extractArray() and getArrayUnsafe() have essentially the same contract...
+    testArrayExtraction(Bytes::toArray);
+    testArrayExtraction(Bytes::toArrayUnsafe);
+
+    // But on top of the basic, extractArray() guarantees modifying the returned array is safe from
+    // impacting the original value (not that getArrayUnsafe makes no guarantees here one way or
+    // another, so there is nothing to test).
+    byte[] orig = new byte[] {1, 2, 3, 4};
+    Bytes value = w(orig);
+    byte[] extracted = value.toArray();
+    assertArrayEquals(orig, extracted);
+    Arrays.fill(extracted, (byte) -1);
+    assertArrayEquals(extracted, new byte[] {-1, -1, -1, -1});
+    assertArrayEquals(orig, new byte[] {1, 2, 3, 4});
+    assertEquals(of(1, 2, 3, 4), value);
+  }
+
+  private void testArrayExtraction(Function<Bytes, byte[]> extractor) {
+    byte[] bytes = new byte[0];
+    assertArrayEquals(extractor.apply(Bytes.EMPTY), bytes);
+
+    byte[][] toTest = new byte[][] {new byte[] {1}, new byte[] {1, 2, 3, 4, 5, 6}, new byte[] {-1, -1, 0, -1}};
+    for (byte[] array : toTest) {
+      assertArrayEquals(extractor.apply(w(array)), array);
+    }
+
+    // Test slightly more complex interactions
+    assertArrayEquals(extractor.apply(w(new byte[] {1, 2, 3, 4, 5}).slice(2, 2)), new byte[] {3, 4});
+    assertArrayEquals(extractor.apply(w(new byte[] {1, 2, 3, 4, 5}).slice(2, 0)), new byte[] {});
+  }
+
+  @Test
+  void testToString() {
+    assertEquals("0x", Bytes.EMPTY.toString());
+
+    assertEquals("0x01", of(1).toString());
+    assertEquals("0x0AFF03", of(0x0a, 0xff, 0x03).toString());
+  }
+
+  @Test
+  void testHasLeadingZeroByte() {
+    assertFalse(Bytes.fromHexString("0x").hasLeadingZeroByte());
+    assertTrue(Bytes.fromHexString("0x0012").hasLeadingZeroByte());
+    assertFalse(Bytes.fromHexString("0x120012").hasLeadingZeroByte());
+  }
+
+  @Test
+  void testNumberOfLeadingZeroBytes() {
+    assertEquals(0, Bytes.fromHexString("0x12").numberOfLeadingZeroBytes());
+    assertEquals(1, Bytes.fromHexString("0x0012").numberOfLeadingZeroBytes());
+    assertEquals(2, Bytes.fromHexString("0x000012").numberOfLeadingZeroBytes());
+    assertEquals(0, Bytes.fromHexString("0x").numberOfLeadingZeroBytes());
+    assertEquals(1, Bytes.fromHexString("0x00").numberOfLeadingZeroBytes());
+    assertEquals(2, Bytes.fromHexString("0x0000").numberOfLeadingZeroBytes());
+    assertEquals(3, Bytes.fromHexString("0x000000").numberOfLeadingZeroBytes());
+  }
+
+  @Test
+  void testNumberOfTrailingZeroBytes() {
+    assertEquals(0, Bytes.fromHexString("0x12").numberOfTrailingZeroBytes());
+    assertEquals(1, Bytes.fromHexString("0x1200").numberOfTrailingZeroBytes());
+    assertEquals(2, Bytes.fromHexString("0x120000").numberOfTrailingZeroBytes());
+    assertEquals(0, Bytes.fromHexString("0x").numberOfTrailingZeroBytes());
+    assertEquals(1, Bytes.fromHexString("0x00").numberOfTrailingZeroBytes());
+    assertEquals(2, Bytes.fromHexString("0x0000").numberOfTrailingZeroBytes());
+    assertEquals(3, Bytes.fromHexString("0x000000").numberOfTrailingZeroBytes());
+  }
+
+  @Test
+  void testHasLeadingZeroBit() {
+    assertFalse(Bytes.fromHexString("0x").hasLeadingZero());
+    assertTrue(Bytes.fromHexString("0x01").hasLeadingZero());
+    assertFalse(Bytes.fromHexString("0xFF0012").hasLeadingZero());
+  }
+
+  @Test
+  void testEquals() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes b2 = w(key);
+    assertEquals(b.hashCode(), b2.hashCode());
+  }
+
+  @Test
+  void testEqualsWithOffset() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key).slice(16, 4);
+    Bytes b2 = w(key).slice(16, 8).slice(0, 4);
+    assertEquals(b, b2);
+  }
+
+  @Test
+  void testHashCode() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes b2 = w(key);
+    assertEquals(b.hashCode(), b2.hashCode());
+  }
+
+  @Test
+  void testHashCodeWithOffset() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key).slice(16, 16);
+    Bytes b2 = w(key).slice(16, 16);
+    assertEquals(b.hashCode(), b2.hashCode());
+  }
+
+  @Test
+  void testHashCodeWithByteBufferWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapByteBuffer(ByteBuffer.wrap(key));
+    assertEquals(b.hashCode(), other.hashCode());
+  }
+
+  @Test
+  void testEqualsWithByteBufferWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapByteBuffer(ByteBuffer.wrap(key));
+    assertEquals(b, other);
+  }
+
+  @Test
+  void testHashCodeWithBufferWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapBuffer(Buffer.buffer(key));
+    assertEquals(b.hashCode(), other.hashCode());
+  }
+
+  @Test
+  void testEqualsWithBufferWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapBuffer(Buffer.buffer(key));
+    assertEquals(b, other);
+  }
+
+  @Test
+  void testHashCodeWithByteBufWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapByteBuf(Unpooled.copiedBuffer(key));
+    assertEquals(b.hashCode(), other.hashCode());
+  }
+
+  @Test
+  void testEqualsWithByteBufWrappingBytes() {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[32];
+    random.nextBytes(key);
+    Bytes b = w(key);
+    Bytes other = Bytes.wrapByteBuf(Unpooled.copiedBuffer(key));
+    assertEquals(b, other);
+  }
+}
diff --git a/bytes/src/test/java/net/consensys/cava/bytes/ConcatenatedBytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/ConcatenatedBytesTest.java
new file mode 100644
index 0000000..f3b09e8
--- /dev/null
+++ b/bytes/src/test/java/net/consensys/cava/bytes/ConcatenatedBytesTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 net.consensys.cava.bytes;
+
+import static net.consensys.cava.bytes.Bytes.fromHexString;
+import static net.consensys.cava.bytes.Bytes.wrap;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+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 ConcatenatedBytesTest {
+
+  @ParameterizedTest
+  @MethodSource("concatenatedWrapProvider")
+  void concatenatedWrap(Object arr1, Object arr2) {
+    byte[] first = (byte[]) arr1;
+    byte[] second = (byte[]) arr2;
+    byte[] res = wrap(wrap(first), wrap(second)).toArray();
+    assertArrayEquals(Arrays.copyOfRange(res, 0, first.length), first);
+    assertArrayEquals(Arrays.copyOfRange(res, first.length, res.length), second);
+  }
+
+  private static Stream<Arguments> concatenatedWrapProvider() {
+    return Stream.of(
+        Arguments.of(new byte[] {}, new byte[] {}),
+        Arguments.of(new byte[] {}, new byte[] {1, 2, 3}),
+        Arguments.of(new byte[] {1, 2, 3}, new byte[] {}),
+        Arguments.of(new byte[] {1, 2, 3}, new byte[] {4, 5}));
+  }
+
+  @Test
+  void testConcatenatedWrapReflectsUpdates() {
+    byte[] first = new byte[] {1, 2, 3};
+    byte[] second = new byte[] {4, 5};
+    byte[] expected1 = new byte[] {1, 2, 3, 4, 5};
+    Bytes res = wrap(wrap(first), wrap(second));
+    assertArrayEquals(res.toArray(), expected1);
+
+    first[1] = 42;
+    second[0] = 42;
+    byte[] expected2 = new byte[] {1, 42, 3, 42, 5};
+    assertArrayEquals(res.toArray(), expected2);
+  }
+
+  @Test
+  void shouldReadConcatenatedValue() {
+    Bytes bytes = wrap(fromHexString("0x01234567"), fromHexString("0x89ABCDEF"));
+    assertEquals(8, bytes.size());
+    assertEquals("0x0123456789ABCDEF", bytes.toHexString());
+  }
+
+  @Test
+  void shouldSliceConcatenatedValue() {
+    Bytes bytes = wrap(
+        fromHexString("0x01234567"),
+        fromHexString("0x89ABCDEF"),
+        fromHexString("0x01234567"),
+        fromHexString("0x89ABCDEF"));
+    assertEquals("0x", bytes.slice(4, 0).toHexString());
+    assertEquals("0x0123456789ABCDEF0123456789ABCDEF", bytes.slice(0, 16).toHexString());
+    assertEquals("0x01234567", bytes.slice(0, 4).toHexString());
+    assertEquals("0x0123", bytes.slice(0, 2).toHexString());
+    assertEquals("0x6789", bytes.slice(3, 2).toHexString());
+    assertEquals("0x89ABCDEF", bytes.slice(4, 4).toHexString());
+    assertEquals("0xABCD", bytes.slice(5, 2).toHexString());
+    assertEquals("0xEF012345", bytes.slice(7, 4).toHexString());
+    assertEquals("0x01234567", bytes.slice(8, 4).toHexString());
+    assertEquals("0x456789ABCDEF", bytes.slice(10, 6).toHexString());
+    assertEquals("0x89ABCDEF", bytes.slice(12, 4).toHexString());
+  }
+
+  @Test
+  void shouldReadDeepConcatenatedValue() {
+    Bytes bytes = wrap(
+        wrap(fromHexString("0x01234567"), fromHexString("0x89ABCDEF")),
+        wrap(fromHexString("0x01234567"), fromHexString("0x89ABCDEF")),
+        fromHexString("0x01234567"),
+        fromHexString("0x89ABCDEF"));
+    assertEquals(24, bytes.size());
+    assertEquals("0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", bytes.toHexString());
+  }
+}
diff --git a/concurrent-coroutines/build.gradle b/concurrent-coroutines/build.gradle
new file mode 100644
index 0000000..f686a8d
--- /dev/null
+++ b/concurrent-coroutines/build.gradle
@@ -0,0 +1,11 @@
+description = 'Kotlin coroutine extensions for cava concurrency primitives.'
+
+dependencies {
+  compile project(':concurrent')
+  compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
+
+  testCompile 'org.junit.jupiter:junit-jupiter-api'
+  testCompile 'org.junit.jupiter:junit-jupiter-params'
+
+  testRuntime 'org.junit.jupiter:junit-jupiter-engine'
+}
diff --git a/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncCompletion.kt b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncCompletion.kt
new file mode 100644
index 0000000..4cd0969
--- /dev/null
+++ b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncCompletion.kt
@@ -0,0 +1,194 @@
+/*
+ * 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.newCoroutineContext
+import kotlinx.coroutines.suspendCancellableCoroutine
+import net.consensys.cava.concurrent.AsyncCompletion
+import net.consensys.cava.concurrent.CompletableAsyncCompletion
+import java.util.concurrent.CancellationException
+import java.util.concurrent.CompletionException
+import java.util.function.Consumer
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Starts new co-routine and returns its result as an implementation of [AsyncCompletion].
+ * The running co-routine is cancelled when the resulting future is cancelled or otherwise completed.
+ *
+ * Co-routine context is inherited from a [CoroutineScope], additional context elements can be specified with [context]
+ * argument. If the context does not have any dispatcher nor any other [ContinuationInterceptor], then
+ * [Dispatchers.Default] is used. The parent job is inherited from a [CoroutineScope] as well, but it can also be
+ * overridden with corresponding [coroutineContext] element.
+ *
+ * By default, the co-routine is immediately scheduled for execution. Other options can be specified via `start`
+ * parameter. See [CoroutineStart] for details. A value of [CoroutineStart.LAZY] is not supported (since
+ * `AsyncResult` framework does not provide the corresponding capability) and produces [IllegalArgumentException].
+ *
+ * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are
+ * available for newly created co-routine.
+ *
+ * @param context Additional to [CoroutineScope.coroutineContext] context of the coroutine.
+ * @param start Co-routine start option. The default value is [CoroutineStart.DEFAULT].
+ * @param block The co-routine code.
+ */
+@UseExperimental(InternalCoroutinesApi::class, ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+fun CoroutineScope.asyncCompletion(
+  context: CoroutineContext = Dispatchers.Default,
+  start: CoroutineStart = CoroutineStart.DEFAULT,
+  block: suspend CoroutineScope.() -> Unit
+): AsyncCompletion {
+  require(!start.isLazy) { "$start start is not supported" }
+  val newContext = this.newCoroutineContext(context)
+  val job = Job(newContext[Job])
+  val coroutine = AsyncCompletionCoroutine(newContext + job)
+  job.invokeOnCompletion { coroutine.asynCompletion.cancel() }
+  coroutine.asynCompletion.whenComplete { job.cancel() }
+  start(block, receiver = coroutine, completion = coroutine) // use the specified start strategy
+  return coroutine.asynCompletion
+}
+
+private class AsyncCompletionCoroutine(
+  override val context: CoroutineContext,
+  val asynCompletion: CompletableAsyncCompletion = AsyncCompletion.incomplete()
+) : Continuation<Unit>, CoroutineScope {
+  override val coroutineContext: CoroutineContext get() = context
+  override fun resumeWith(result: Result<Unit>) {
+    result
+      .onSuccess { asynCompletion.complete() }
+      .onFailure { asynCompletion.completeExceptionally(it) }
+  }
+}
+
+/**
+ * Converts this deferred value to a [AsyncCompletion].
+ * The deferred value is cancelled when the returned [AsyncCompletion] is cancelled or otherwise completed.
+ */
+@UseExperimental(ObsoleteCoroutinesApi::class)
+fun Deferred<Unit>.asAsyncCompletion(): AsyncCompletion {
+  val asyncCompletion = AsyncCompletion.incomplete()
+  asyncCompletion.whenComplete { cancel() }
+  invokeOnCompletion {
+    try {
+      asyncCompletion.complete()
+    } catch (exception: Exception) {
+      asyncCompletion.completeExceptionally(exception)
+    }
+  }
+  return asyncCompletion
+}
+
+/**
+ * Converts this job to a [AsyncCompletion].
+ * The job is cancelled when the returned [AsyncCompletion] is cancelled or otherwise completed.
+ */
+@UseExperimental(ObsoleteCoroutinesApi::class)
+fun Job.asAsyncCompletion(): AsyncCompletion {
+  val asyncCompletion = AsyncCompletion.incomplete()
+  asyncCompletion.whenComplete { cancel() }
+  invokeOnCompletion {
+    try {
+      asyncCompletion.complete()
+    } catch (exception: Exception) {
+      asyncCompletion.completeExceptionally(exception)
+    }
+  }
+  return asyncCompletion
+}
+
+/**
+ * Converts this [AsyncCompletion] to an instance of [Deferred].
+ * The [AsyncCompletion] is cancelled when the resulting deferred is cancelled.
+ */
+@UseExperimental(ObsoleteCoroutinesApi::class)
+fun AsyncCompletion.asDeferred(): Deferred<Unit> {
+  // Fast path if already completed
+  if (isDone) {
+    return try {
+      CompletableDeferred(join())
+    } catch (e: Throwable) {
+      // unwrap original cause from CompletionException
+      val original = (e as? CompletionException)?.cause ?: e
+      CompletableDeferred<Unit>().also { it.completeExceptionally(original) }
+    }
+  }
+  val result = CompletableDeferred<Unit>()
+  whenComplete { exception ->
+    if (exception == null) {
+      result.complete(Unit)
+    } else {
+      result.completeExceptionally(exception)
+    }
+  }
+  result.invokeOnCompletion { this.cancel() }
+  return result
+}
+
+/**
+ * Awaits for completion of the [AsyncCompletion] without blocking a thread.
+ *
+ * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this
+ * suspending function is waiting, this function stops waiting for the [AsyncCompletion] and immediately resumes with
+ * [CancellationException].
+ *
+ * Note, that [AsyncCompletion] does not support prompt removal of listeners, so on cancellation of this wait a few
+ * small objects will remain in the [AsyncCompletion] stack of completion actions until it completes itself. However,
+ * care is taken to clear the reference to the waiting coroutine itself, so that its memory can be released even if the
+ * [AsyncCompletion] never completes.
+ */
+suspend fun AsyncCompletion.await() {
+  // fast path when CompletableFuture is already done (does not suspend)
+  if (isDone) {
+    try {
+      return join()
+    } catch (e: CompletionException) {
+      throw e.cause ?: e // unwrap original cause from CompletionException
+    }
+  }
+  // slow path -- suspend
+  return suspendCancellableCoroutine { cont: CancellableContinuation<Unit> ->
+    val consumer = ContinuationConsumer(cont)
+    whenComplete(consumer)
+    cont.invokeOnCancellation {
+      consumer.cont = null // shall clear reference to continuation
+    }
+  }
+}
+
+private class ContinuationConsumer(
+  @Volatile @JvmField var cont: Continuation<Unit>?
+) : Consumer<Throwable?> {
+  override fun accept(exception: Throwable?) {
+    val cont = this.cont ?: return // atomically read current value unless null
+    if (exception == null) {
+      // the future has been completed normally
+      cont.resume(Unit)
+    } else {
+      // the future has completed with an exception, unwrap it to provide consistent view of .await() result and to propagate only original exception
+      cont.resumeWithException((exception as? CompletionException)?.cause ?: exception)
+    }
+  }
+}
diff --git a/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncResult.kt b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncResult.kt
new file mode 100644
index 0000000..7149b2e
--- /dev/null
+++ b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/AsyncResult.kt
@@ -0,0 +1,179 @@
+/*
+ * 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.newCoroutineContext
+import kotlinx.coroutines.suspendCancellableCoroutine
+import net.consensys.cava.concurrent.AsyncResult
+import net.consensys.cava.concurrent.CompletableAsyncResult
+import java.util.concurrent.CancellationException
+import java.util.concurrent.CompletionException
+import java.util.function.BiConsumer
+import kotlin.Result
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Starts new co-routine and returns its result as an implementation of [AsyncResult].
+ * The running co-outine is cancelled when the resulting future is cancelled or otherwise completed.
+ *
+ * Co-routine context is inherited from a [CoroutineScope], additional context elements can be specified with [context]
+ * argument. If the context does not have any dispatcher nor any other [ContinuationInterceptor], then
+ * [Dispatchers.Default] is used. The parent job is inherited from a [CoroutineScope] as well, but it can also be
+ * overridden with corresponding [coroutineContext] element.
+ *
+ * By default, the co-routine is immediately scheduled for execution. Other options can be specified via `start`
+ * parameter. See [CoroutineStart] for details. A value of [CoroutineStart.LAZY] is not supported (since
+ * `AsyncResult` framework does not provide the corresponding capability) and produces [IllegalArgumentException].
+ *
+ * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are
+ * available for newly created co-routine.
+ *
+ * @param context Additional to [CoroutineScope.coroutineContext] context of the coroutine.
+ * @param start Co-routine start option. The default value is [CoroutineStart.DEFAULT].
+ * @param block The co-routine code.
+ */
+@UseExperimental(InternalCoroutinesApi::class, ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+fun <T> CoroutineScope.asyncResult(
+  context: CoroutineContext = Dispatchers.Default,
+  start: CoroutineStart = CoroutineStart.DEFAULT,
+  block: suspend CoroutineScope.() -> T
+): AsyncResult<T> {
+  require(!start.isLazy) { "$start start is not supported" }
+  val newContext = this.newCoroutineContext(context)
+  val job = Job(newContext[Job])
+  val coroutine = AsyncResultCoroutine<T>(newContext + job)
+  job.invokeOnCompletion { coroutine.asyncResult.cancel() }
+  coroutine.asyncResult.whenComplete { _, _ -> job.cancel() }
+  start(block, receiver = coroutine, completion = coroutine) // use the specified start strategy
+  return coroutine.asyncResult
+}
+
+private class AsyncResultCoroutine<T>(
+  override val context: CoroutineContext,
+  val asyncResult: CompletableAsyncResult<T> = AsyncResult.incomplete()
+) : Continuation<T>, CoroutineScope {
+  override val coroutineContext: CoroutineContext get() = context
+  override fun resumeWith(result: Result<T>) {
+    result
+      .onSuccess { asyncResult.complete(it) }
+      .onFailure { asyncResult.completeExceptionally(it) }
+  }
+}
+
+/**
+ * Converts this deferred value to an [AsyncResult].
+ * The deferred value is cancelled when the returned [AsyncResult] is cancelled or otherwise completed.
+ */
+@UseExperimental(ExperimentalCoroutinesApi::class, ObsoleteCoroutinesApi::class)
+fun <T> Deferred<T>.asAsyncResult(): AsyncResult<T> {
+  val asyncResult = AsyncResult.incomplete<T>()
+  asyncResult.whenComplete { _, _ -> cancel() }
+  invokeOnCompletion {
+    try {
+      asyncResult.complete(getCompleted())
+    } catch (exception: Exception) {
+      asyncResult.completeExceptionally(exception)
+    }
+  }
+  return asyncResult
+}
+
+/**
+ * Converts this [AsyncResult] to an instance of [Deferred].
+ * The [AsyncResult] is cancelled when the resulting deferred is cancelled.
+ */
+@UseExperimental(ObsoleteCoroutinesApi::class)
+fun <T> AsyncResult<T>.asDeferred(): Deferred<T> {
+  // Fast path if already completed
+  if (isDone) {
+    return try {
+      @Suppress("UNCHECKED_CAST")
+      CompletableDeferred(get() as T)
+    } catch (e: Throwable) {
+      // unwrap original cause from CompletionException
+      val original = (e as? CompletionException)?.cause ?: e
+      CompletableDeferred<T>().also { it.completeExceptionally(original) }
+    }
+  }
+  val result = CompletableDeferred<T>()
+  whenComplete { value, exception ->
+    if (exception == null) {
+      result.complete(value)
+    } else {
+      result.completeExceptionally(exception)
+    }
+  }
+  result.invokeOnCompletion { this.cancel() }
+  return result
+}
+
+/**
+ * Awaits for completion of the [AsyncResult] without blocking a thread.
+ *
+ * This suspending function is cancellable.
+ * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
+ * stops waiting for the [AsyncResult] and immediately resumes with [CancellationException].
+ *
+ * Note, that [AsyncResult] does not support prompt removal of listeners, so on cancellation of this wait
+ * a few small objects will remain in the [AsyncResult] stack of completion actions until it completes itself.
+ * However, care is taken to clear the reference to the waiting coroutine itself, so that its memory can be
+ * released even if the [AsyncResult] never completes.
+ */
+suspend fun <T> AsyncResult<T>.await(): T {
+  // fast path when CompletableFuture is already done (does not suspend)
+  if (isDone) {
+    try {
+      @Suppress("UNCHECKED_CAST")
+      return get() as T
+    } catch (e: CompletionException) {
+      throw e.cause ?: e // unwrap original cause from CompletionException
+    }
+  }
+  // slow path -- suspend
+  return suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
+    val consumer = ContinuationBiConsumer(cont)
+    whenComplete(consumer)
+    cont.invokeOnCancellation {
+      consumer.cont = null // shall clear reference to continuation
+    }
+  }
+}
+
+private class ContinuationBiConsumer<T>(
+  @Volatile @JvmField var cont: Continuation<T>?
+) : BiConsumer<T?, Throwable?> {
+  @Suppress("UNCHECKED_CAST")
+  override fun accept(result: T?, exception: Throwable?) {
+    val cont = this.cont ?: return // atomically read current value unless null
+    if (exception == null) {
+      // the future has been completed normally
+      cont.resume(result as T)
+    } else {
+      // the future has completed with an exception, unwrap it to provide consistent view of .await() result and to propagate only original exception
+      cont.resumeWithException((exception as? CompletionException)?.cause ?: exception)
+    }
+  }
+}
diff --git a/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatch.kt b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatch.kt
new file mode 100644
index 0000000..ec609e1
--- /dev/null
+++ b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatch.kt
@@ -0,0 +1,101 @@
+/*
+ * 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * A co-routine synchronization aid that allows co-routines to wait until a set of operations being performed
+ * has completed.
+ *
+ * The latch is initialized with a given count. If the latch count is greater than zero, the `await()` method will
+ * suspend until the count reaches zero due to invocations of the `countDown()` method, at which point all suspended
+ * co-routines will be resumed.
+ *
+ * Unlike the Java `CountDownLatch`, this latch allows the count to be increased via invocation of the `countUp()`
+ * method. Increasing the count from zero will result in calls to `await()` suspending again. Note that the count may
+ * be negative, requiring multiple calls to `countUp()` before calls to `await()` suspend.
+ *
+ * @param initial The initial count of the latch, which may be positive, zero, or negative.
+ * @constructor A latch.
+ */
+class CoroutineLatch(initial: Int) {
+
+  private val atomicCount = AtomicInteger(initial)
+  private var waitingCoroutines = mutableListOf<Continuation<Unit>>()
+
+  /**
+   * The current latch count.
+   */
+  val count: Int
+    get() = atomicCount.get()
+
+  /**
+   * Indicates if the latch is open (`count <= 0`).
+   */
+  val isOpen: Boolean
+    get() = atomicCount.get() <= 0
+
+  /**
+   * Decrease the latch count, potentially opening the latch and awakening suspending co-routines.
+   *
+   * @return `true` if the latch was opened as a result of this invocation.
+   */
+  fun countDown(): Boolean {
+    var toAwaken: List<Continuation<Unit>>? = null
+    synchronized(this) {
+      if (atomicCount.decrementAndGet() == 0) {
+        toAwaken = waitingCoroutines
+        waitingCoroutines = mutableListOf()
+      }
+    }
+    toAwaken?.forEach { it.resume(Unit) }
+    return toAwaken != null
+  }
+
+  /**
+   * Increase the latch count, potentially closing the latch.
+   *
+   * @return `true` if the latch was closed as a result of this invocation.
+   */
+  fun countUp(): Boolean = atomicCount.incrementAndGet() == 1
+
+  /**
+   * Await the latch opening. If already open, return without suspending.
+   */
+  suspend fun await() {
+    if (atomicCount.get() <= 0) {
+      return
+    }
+    suspendCancellableCoroutine { cont: Continuation<Unit> ->
+      try {
+        var suspended: Boolean
+        synchronized(this) {
+          suspended = atomicCount.get() > 0
+          if (suspended) {
+            waitingCoroutines.add(cont)
+          }
+        }
+        if (!suspended) {
+          cont.resume(Unit)
+        }
+      } catch (e: Throwable) {
+        cont.resumeWithException(e)
+      }
+    }
+  }
+}
diff --git a/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/Retryable.kt b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/Retryable.kt
new file mode 100644
index 0000000..498e85b
--- /dev/null
+++ b/concurrent-coroutines/src/main/kotlin/net/consensys/cava/concurrent/coroutines/Retryable.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withTimeoutOrNull
+
+/**
+ * Retry a suspending block until a non-null result is obtained.
+ *
+ * @param retryDelay the delay between each attempt
+ * @param block the suspending block to be executed
+ * @return the first non-null result
+ */
+suspend fun <R> CoroutineScope.retry(
+  retryDelay: Long,
+  block: suspend (Int) -> R?
+): R = retry({ retryDelay }, block)!!
+
+/**
+ * Retry a suspending block until a non-null result is obtained.
+ *
+ * @param retryDelay the delay between each attempt
+ * @param maxRetries the maximum number of attempts
+ * @param block the suspending block to be executed
+ * @return the first non-null result, or `null` if all attempts fail
+ */
+suspend fun <R> CoroutineScope.retry(
+  retryDelay: Long,
+  maxRetries: Int,
+  block: suspend (Int) -> R?
+): R? = retry({ i -> if (i > maxRetries) null else retryDelay }, block)
+
+/**
+ * Retry a suspending block until a non-null result is obtained.
+ *
+ * @param retryDelay a function returning the delay that should follow each attempt, or `null` if no further attempts
+ *         should be made
+ * @param block the suspending block to be executed
+ * @return the first non-null result, or `null` if all attempts fail
+ */
+@UseExperimental(ExperimentalCoroutinesApi::class)
+suspend fun <R> CoroutineScope.retry(
+  retryDelay: (Int) -> Long?,
+  block: suspend (Int) -> R?
+): R? {
+  val jobs = mutableSetOf<Job>()
+  val result = CompletableDeferred<R?>()
+  result.invokeOnCompletion { jobs.forEach { job -> job.cancel() } }
+
+  var stopped = false
+  var i = 1
+  while (true) {
+    val attempt = i
+    val delayTime = retryDelay(attempt) ?: break
+    val deferred = async { block(attempt) }
+    deferred.invokeOnCompletion { e ->
+      try {
+        jobs.remove(deferred)
+        if (e is CancellationException) {
+          // ignore
+          return@invokeOnCompletion
+        }
+        if (e != null) {
+          result.completeExceptionally(e)
+        } else {
+          deferred.getCompleted()?.let { r -> result.complete(r) }
+          if (stopped && jobs.isEmpty()) {
+            result.complete(null)
+          }
+        }
+      } catch (e: Throwable) {
+        result.completeExceptionally(e)
+      }
+    }
+    jobs.add(deferred)
+
+    val r = withTimeoutOrNull(delayTime) { result.await() }
+    if (r != null) {
+      return r
+    }
+    ++i
+  }
+  stopped = true
+  if (jobs.isEmpty()) {
+    return null
+  }
+  return result.await()
+}
+
+/**
+ * Cancel and retry a suspending block until a non-null result is obtained.
+ *
+ * @param timeout the delay before re-attempting
+ * @param block the suspending block to be executed
+ * @return the first non-null result
+ */
+suspend fun <R> timeoutAndRetry(
+  timeout: Long,
+  block: suspend (Int) -> R?
+): R = timeoutAndRetry({ timeout }, block)!!
+
+/**
+ * Cancel and retry a suspending block until a non-null result is obtained.
+ *
+ * @param timeout the delay before re-attempting
+ * @param maxRetries the maximum number of attempts
+ * @param block the suspending block to be executed
+ * @return the first non-null result, or `null` if all attempts fail
+ */
+suspend fun <R> timeoutAndRetry(
+  timeout: Long,
+  maxRetries: Int,
+  block: suspend (Int) -> R?
+): R? = timeoutAndRetry({ i -> if (i >= maxRetries) null else timeout }, block)
+
+/**
+ * Cancel and retry a suspending block until a non-null result is obtained.
+ *
+ * @param timeout a function returning the delay that should follow each attempt, or `null` if no further attempts
+ *         should be made
+ * @param block the suspending block to be executed
+ * @return the first non-null result, or `null` if all attempts fail
+ */
+suspend fun <R> timeoutAndRetry(
+  timeout: (Int) -> Long?,
+  block: suspend (Int) -> R?
+): R? {
+  var i = 1
+  while (true) {
+    val attempt = i
+    val time = timeout(attempt) ?: return null
+    val r = withTimeoutOrNull(time) { block(attempt) }
+    if (r != null) {
+      return r
+    }
+    ++i
+  }
+}
diff --git a/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatchTest.kt b/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatchTest.kt
new file mode 100644
index 0000000..a61094c
--- /dev/null
+++ b/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/CoroutineLatchTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+internal class CoroutineLatchTest {
+
+  @Test
+  fun shouldntSuspendWhenLatchIsOpen() = runBlocking {
+    withTimeout(1) {
+      CoroutineLatch(0).await()
+    }
+    withTimeout(1) {
+      CoroutineLatch(-1).await()
+    }
+  }
+
+  @Test
+  fun shouldUnsuspendWhenLatchOpens() = runBlocking {
+    val latch = CoroutineLatch(2)
+    assertFalse(latch.isOpen)
+    assertEquals(2, latch.count)
+
+    var ok = false
+    var done = false
+    val job = async {
+      latch.await()
+      assertTrue(ok, "failed to suspend")
+      done = true
+    }
+
+    Thread.sleep(100)
+    assertFalse(latch.countDown())
+    assertFalse(latch.isOpen)
+    assertEquals(1, latch.count)
+
+    Thread.sleep(100)
+    assertFalse(done, "woke up too early")
+
+    ok = true
+    assertTrue(latch.countDown())
+    assertTrue(latch.isOpen)
+    assertEquals(0, latch.count)
+    job.await()
+    assertTrue(done, "failed to wakeup")
+  }
+
+  @Test
+  fun shouldSuspendWhenLatchCloses() = runBlocking {
+    val latch = CoroutineLatch(-1)
+    assertTrue(latch.isOpen)
+    assertEquals(-1, latch.count)
+
+    withTimeout(1) {
+      latch.await()
+    }
+
+    assertFalse(latch.countUp())
+    assertTrue(latch.isOpen)
+    assertEquals(0, latch.count)
+
+    withTimeout(1) {
+      latch.await()
+    }
+
+    assertTrue(latch.countUp())
+    assertFalse(latch.isOpen)
+    assertEquals(1, latch.count)
+
+    var ok = false
+    var done = false
+    val job = async {
+      latch.await()
+      assertTrue(ok, "failed to suspend")
+      done = true
+    }
+
+    ok = true
+    assertTrue(latch.countDown())
+    assertTrue(latch.isOpen)
+    job.await()
+    assertTrue(done, "failed to wakeup")
+  }
+
+  @Test
+  fun shouldTimeoutWhenBlocked() {
+    assertThrows<TimeoutCancellationException> {
+      runBlocking {
+        withTimeout(1) {
+          CoroutineLatch(1).await()
+        }
+      }
+    }
+  }
+}
diff --git a/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/RetryableTest.kt b/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/RetryableTest.kt
new file mode 100644
index 0000000..f29cb96
--- /dev/null
+++ b/concurrent-coroutines/src/test/kotlin/net/consensys/cava/concurrent/coroutines/RetryableTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019 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 net.consensys.cava.concurrent.coroutines
+
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.lang.RuntimeException
+
+private val NOOP_EXCEPTION_HANDLER = CoroutineExceptionHandler { _, _ -> }
+
+internal class RetryableTest {
+
+  @Test
+  fun shouldNotRetryIfFirstAttemptReturns() = runBlocking {
+    var attempts = 0
+    val result = retry(500) {
+      attempts++
+      "done"
+    }
+    assertEquals("done", result)
+    assertEquals(1, attempts)
+  }
+
+  @Test
+  fun shouldRetryUntilSuccess() = runBlocking {
+    var attempts = 0
+    val result = retry(100) { i ->
+      attempts++
+      delay(470)
+      "done $i"
+    }
+    assertEquals("done 1", result)
+    assertEquals(5, attempts)
+  }
+
+  @Test
+  fun shouldReturnAnySuccess() = runBlocking {
+    var attempts = 0
+    val result = retry(25) { i ->
+      attempts++
+      delay(if (i == 4) 60 else 1000)
+      "done $i"
+    }
+    assertEquals("done 4", result)
+    assertEquals(6, attempts)
+  }
+
+  @Test
+  fun shouldStopRetryingAfterMaxAttempts() = runBlocking {
+    var attempts = 0
+    val result = retry(50, 3) { i ->
+      attempts++
+      delay(250)
+      "done $i"
+    }
+    assertEquals("done 1", result)
+    assertEquals(3, attempts)
+  }
+
+  @Test
+  fun shouldReturnNullIfAllAttemptsFail() = runBlocking {
+    var attempts = 0
+    val result = retry(50, 3) {
+      attempts++
+      delay(250)
+      null
+    }
+    assertNull(result)
+    assertEquals(3, attempts)
+  }
+
+  @Test
+  fun shouldThrowIfAttemptThrows() {
+    var attempts = 0
+    val e = assertThrows<RuntimeException> {
+      runBlocking(NOOP_EXCEPTION_HANDLER) {
+        retry(25) { i ->
+          attempts++
+          if (i == 4) {
+            throw RuntimeException("catch me")
+          }
+          delay(1000)
+          "done $i"
+        }
+      }
+    }
+    assertEquals("catch me", e.message)
+    assertEquals(4, attempts)
+  }
+}
diff --git a/concurrent/build.gradle b/concurrent/build.gradle
new file mode 100644
index 0000000..51c7776
--- /dev/null
+++ b/concurrent/build.gradle
@@ -0,0 +1,12 @@
+description = 'Classes and utilities for working with concurrency.'
+
+dependencies {
+  compile 'com.google.guava:guava'
+  compileOnly 'io.vertx:vertx-core'
+
+  testCompile 'org.junit.jupiter:junit-jupiter-api'
+  testCompile 'org.junit.jupiter:junit-jupiter-params'
+  testCompile 'org.assertj:assertj-core'
+
+  testRuntime 'org.junit.jupiter:junit-jupiter-engine'
+}
diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncCompletion.java b/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncCompletion.java
new file mode 100644
index 0000000..cf97b6c
--- /dev/null
+++ b/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncCompletion.java
@@ -0,0 +1,478 @@
+/*
+ * 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 net.consensys.cava.concurrent;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.WorkerExecutor;
+
+/**
+ * A completion that will be complete at a future time.
+ */
+public interface AsyncCompletion {
+
+  AsyncCompletion COMPLETED = new DefaultCompletableAsyncCompletion(CompletableFuture.completedFuture(null));
+
+  /**
+   * Return an already completed completion.
+   *
+   * @return A completed completion.
+   */
+  static AsyncCompletion completed() {
+    return COMPLETED;
+  }
+
+  /**
+   * Return an already failed completion, caused by the given exception.
+   *
+   * @param ex The exception.
+   * @return A failed result.
+   */
+  static AsyncCompletion exceptional(Throwable ex) {
+    requireNonNull(ex);
+    CompletableAsyncCompletion completion = new DefaultCompletableAsyncCompletion();
+    completion.completeExceptionally(ex);
+    return completion;
+  }
+
+  /**
+   * Return an incomplete completion, that can be later completed or failed.
+   *
+   * @return An incomplete completion.
+   */
+  static CompletableAsyncCompletion incomplete() {
+    return new DefaultCompletableAsyncCompletion();
+  }
+
+  /**
+   * Returns an {@link AsyncCompletion} that completes when all of the given completions complete. If any completions
+   * complete exceptionally, then the resulting completion also completes exceptionally.
+   *
+   * @param cs The completions to combine.
+   * @return A completion.
+   */
+  static AsyncCompletion allOf(AsyncCompletion... cs) {
+    return allOf(Arrays.stream(cs));
+  }
+
+  /**
+   * Returns an {@link AsyncCompletion} that completes when all of the given completions complete. If any completions
+   * complete exceptionally, then the resulting completion also completes exceptionally.
+   *
+   * @param cs The completions to combine.
+   * @return A completion.
+   */
+  static AsyncCompletion allOf(Collection<AsyncCompletion> cs) {
+    return allOf(cs.stream());
+  }
+
+  /**
+   * Returns an {@link AsyncCompletion} that completes when all of the given completions complete. If any completions
+   * complete exceptionally, then the resulting completion also completes exceptionally.
+   *
+   * @param cs The completions to combine.
+   * @return A completion.
+   */
+  static AsyncCompletion allOf(Stream<AsyncCompletion> cs) {
+    @SuppressWarnings("rawtypes")
+    java.util.concurrent.CompletableFuture[] completableFutures = cs.map(completion -> {
+      java.util.concurrent.CompletableFuture<Void> javaFuture = new java.util.concurrent.CompletableFuture<>();
+      completion.whenComplete(ex -> {
+        if (ex == null) {
+          javaFuture.complete(null);
+        } else {
+          javaFuture.completeExceptionally(ex);
+        }
+      });
+      return javaFuture;
+    }).toArray(java.util.concurrent.CompletableFuture[]::new);
+    return new DefaultCompletableAsyncCompletion(java.util.concurrent.CompletableFuture.allOf(completableFutures));
+  }
+
+  /**
+   * Returns a completion that, after the given function executes on a vertx context and returns a completion, completes
+   * when the completion from the function does.
+   *
+   * @param vertx The vertx context.
+   * @param fn The function returning a completion.
+   * @return A completion.
+   */
+  static AsyncCompletion runOnContext(Vertx vertx, Supplier<? extends AsyncCompletion> fn) {
+    requireNonNull(fn);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    vertx.runOnContext(ev -> {
+      try {
+        fn.get().whenComplete(ex2 -> {
+          if (ex2 == null) {
+            try {
+              completion.complete();
+            } catch (Throwable ex3) {
+              completion.completeExceptionally(ex3);
+            }
+          } else {
+            completion.completeExceptionally(ex2);
+          }
+        });
+      } catch (Throwable ex1) {
+        completion.completeExceptionally(ex1);
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns a completion that completes after the given action executes on a vertx context.
+   *
+   * <p>
+   * Note that the given function is run directly on the context and should not block.
+   *
+   * @param vertx The vertx context.
+   * @param action The action to execute.
+   * @return A completion.
+   */
+  static AsyncCompletion runOnContext(Vertx vertx, Runnable action) {
+    requireNonNull(action);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    vertx.runOnContext(ev -> {
+      try {
+        action.run();
+        completion.complete();
+      } catch (Throwable ex) {
+        completion.completeExceptionally(ex);
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns a completion that completes after the given blocking action executes asynchronously on
+   * {@link ForkJoinPool#commonPool()}.
+   *
+   * @param action The blocking action to execute.
+   * @return A completion.
+   */
+  static AsyncCompletion executeBlocking(Runnable action) {
+    requireNonNull(action);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    ForkJoinPool.commonPool().execute(() -> {
+      try {
+        action.run();
+        completion.complete();
+      } catch (Throwable ex) {
+        completion.completeExceptionally(ex);
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns a completion that completes after the given blocking action executes asynchronously on an {@link Executor}.
+   *
+   * @param executor The executor.
+   * @param action The blocking action to execute.
+   * @return A completion.
+   */
+  static AsyncCompletion executeBlocking(Executor executor, Runnable action) {
+    requireNonNull(action);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    executor.execute(() -> {
+      try {
+        action.run();
+        completion.complete();
+      } catch (Throwable ex) {
+        completion.completeExceptionally(ex);
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns a completion that completes after the given blocking action executes asynchronously on a vertx context.
+   *
+   * @param vertx The vertx context.
+   * @param action The blocking action to execute.
+   * @return A completion.
+   */
+  static AsyncCompletion executeBlocking(Vertx vertx, Runnable action) {
+    requireNonNull(action);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    vertx.executeBlocking(future -> {
+      action.run();
+      future.complete();
+    }, false, res -> {
+      if (res.succeeded()) {
+        completion.complete();
+      } else {
+        completion.completeExceptionally(res.cause());
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns a completion that completes after the given blocking action executes asynchronously on a vertx executor.
+   *
+   * @param executor A vertx executor.
+   * @param action The blocking action to execute.
+   * @return A completion.
+   */
+  static AsyncCompletion executeBlocking(WorkerExecutor executor, Runnable action) {
+    requireNonNull(action);
+    CompletableAsyncCompletion completion = AsyncCompletion.incomplete();
+    executor.executeBlocking(future -> {
+      action.run();
+      future.complete();
+    }, false, res -> {
+      if (res.succeeded()) {
+        completion.complete();
+      } else {
+        completion.completeExceptionally(res.cause());
+      }
+    });
+    return completion;
+  }
+
+  /**
+   * Returns {@code true} if completed normally, completed exceptionally or cancelled.
+   *
+   * @return {@code true} if completed.
+   */
+  boolean isDone();
+
+  /**
+   * Returns {@code true} if completed exceptionally or cancelled.
+   *
+   * @return {@code true} if completed exceptionally or cancelled.
+   */
+  boolean isCompletedExceptionally();
+
+  /**
+   * Attempt to cancel execution of this task.
+   *
+   * <p>
+   * This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for
+   * some other reason. If successful, and this task has not started when {@code cancel} is called, this task should
+   * never run.
+   *
+   * <p>
+   * After this method returns, subsequent calls to {@link #isDone()} will always return {@code true}. Subsequent calls
+   * to {@link #isCancelled()} will always return {@code true} if this method returned {@code true}.
+   *
+   * @return {@code true} if this completion transitioned to a cancelled state.
+   */
+  boolean cancel();
+
+  /**
+   * Returns {@code true} if this task was cancelled before it completed normally.
+   *
+   * @return {@code true} if completed.
+   */
+  boolean isCancelled();
+
+  /**
+   * Waits if necessary for the computation to complete.
+   *
+   * @throws CompletionException If the computation threw an exception.
+   * @throws InterruptedException If the current thread was interrupted while waiting.
+   */
+  void join() throws CompletionException, InterruptedException;
+
+  /**
+   * Waits if necessary for at most the given time for the computation to complete.
+   *
+   * @param timeout The maximum time to wait.
+   * @param unit The time unit of the timeout argument.
+   * @throws CompletionException If the computation threw an exception.
+   * @throws TimeoutException If the wait timed out.
+   * @throws InterruptedException If the current thread was interrupted while waiting.
+   */
+  void join(long timeout, TimeUnit unit) throws CompletionException, TimeoutException, InterruptedException;
+
+  /**
+   * Returns a new completion that, when this completion completes normally, completes with the same value or exception
+   * as the result returned after executing the given function.
+   *
+   * @param fn The function returning a new result.
+   * @param <U> The type of the returned result's value.
+   * @return A new result.
+   */
+  <U> AsyncResult<U> then(Supplier<? extends AsyncResult<U>> fn);
+
+  /**
+   * Returns a new result that, when this completion completes normally, completes with the same value or exception as
+   * the completion returned after executing the given function on the vertx context.
+   *
+   * @param vertx The vertx context.
+   * @param fn The function returning a new result.
+   * @param <U> The type of the returned result's value.
+   * @return A new result.
+   */
+  <U> AsyncResult<U> thenSchedule(Vertx vertx, Supplier<? extends AsyncResult<U>> fn);
+
+  /**
+   * Returns a new completion that, when this completion completes normally, completes after given action is executed.
+   *
+   * @param runnable Te action to perform before completing the returned {@link AsyncCompletion}.
+   * @return A completion.
+   */
+  AsyncCompletion thenRun(Runnable runnable);
+
+  /**
+   * Returns a new completion that, when this completion completes normally, completes after the given action is
+   * executed on the vertx context.
+   *
+   * @param vertx The vertx context.
+   * @param runnable The action to execute on the vertx context before completing the returned completion.
+   * @return A completion.
+   */
+  AsyncCompletion thenScheduleRun(Vertx vertx, Runnable runnable);
+
+  /**
+   * Returns a new completion that, when this completion completes normally, completes after the given blocking action
+   * is executed on the vertx context.
+   *
+   * @param vertx The vertx context.
+   * @param runnable The action to execute on the vertx context before completing the returned completion.
+   * @return A completion.
+   */
+  AsyncCompletion thenScheduleBlockingRun(Vertx vertx, Runnable runnable);
+
+  /**
+   * Returns a new completion that, when this completion completes normally, completes after the given blocking action
+   * is executed on the vertx executor.
+   *
+   * @param executor The vertx executor.
+   * @param runnable The action to execute on the vertx context before completing the returned completion.
+   * @return A completion.
+   */
+  AsyncCompletion thenScheduleBlockingRun(WorkerExecutor executor, Runnable runnable);
+
+  /**
+   * When this result completes normally, invokes the given function with the resulting value and obtain a new
+   * {@link AsyncCompletion}.
+   *
+   * @param fn The function returning a new completion.
+   * @return A completion.
+   */
+  AsyncCompletion thenCompose(Supplier<? extends AsyncCompletion> fn);
+
+  /**
+   * Returns a completion that, when this result completes normally, completes with the value obtained after executing
+   * the supplied function.
+   *
+   * @param supplier The function to use to compute the value of the returned result.
+   * @param <U> The function's return type.
+   * @return A new result.
+   */
+  <U> AsyncResult<U> thenSupply(Supplier<? extends U> supplier);
+
+  /**
+   * Returns a completion that, when this result completes normally, completes with the value obtained after executing
+   * the supplied function on the vertx context.
+   *
+   * @param vertx The vertx context.
+   * @param supplier The function to use to compute the value of the returned result.
+   * @param <U> The function's return type.
+   * @return A new result.
+   */
+  <U> AsyncResult<U> thenSupply(Vertx vertx, Supplier<? extends U> supplier);
+
+  /**
+   * Returns a completion that, when this completion and the supplied result both complete normally, completes after
+   * executing the supplied function with the value from the supplied result as an argument.
+   *
+   * @param other The other result.
+   * @param consumer The function to execute.
+   * @param <U> The type of the other's value.
+   * @return A new result.
+   */
+  <U> AsyncCompletion thenConsume(AsyncResult<? extends U> other, Consumer<? super U> consumer);
+
+  /**
+   * Returns a result that, when this completion and the other result both complete normally, completes with the value
+   * obtained from executing the supplied function with the value from the other result as an argument.
+   *
+   * @param other The other result.
+   * @param fn The function to execute.
+   * @param <U> The type of the other's value.
+   * @param <V> The type of the value returned by the function.
+   * @return A new result.
+   */
+  <U, V> AsyncResult<V> thenApply(AsyncResult<? extends U> other, Function<? super U, ? extends V> fn);
+
+  /**
+   * Returns a completion that completes when both this completion and the other complete normally.
+   *
+   * @param other The other completion.
+   * @return A completion.
+   */
+  AsyncCompletion thenCombine(AsyncCompletion other);
+
+  /**
+   * Returns a new completion that, when this result completes exceptionally, completes after executing the supplied
+   * function. Otherwise, if this result completes normally, then the returned result also completes normally with the
+   * same value.
+   *
+   * @param consumer The function to execute.
+   * @return A new result.
+   */
+  AsyncCompletion exceptionally(Consumer<? super Throwable> consumer);
+
+  /**
+   * Returns a new completion that completes in the same manner as this completion, after executing the given function
+   * with this completion's exception (if any).
+   * <p>
+   * The exception supplied to the function will be {@code null} if this completion completes successfully.
+   *
+   * @param consumer The action to execute.
+   * @return A new result.
+   */
+  AsyncCompletion whenComplete(Consumer<? super Throwable> consumer);
+
+  /**
+   * Returns a new result that, when this result completes either normally or exceptionally, completes with the value
+   * obtained from executing the supplied function with this result's exception (if any) as an argument.
+   * <p>
+   * The exception supplied to the function will be {@code null} if this completion completes successfully.
+   *
+   * @param fn The function to execute.
+   * @param <U> The type of the value returned from the function.
+   * @return A new result.
+   */
+  <U> AsyncResult<U> handle(Function<? super Throwable, ? extends U> fn);
+
+  /**
+   * Returns a new completion that completes successfully, after executing the given function with this completion's
+   * exception (if any).
+   * <p>
+   * The exception supplied to the function will be {@code null} if this completion completes successfully.
+   *
+   * @param consumer The action to execute.
+   * @return A new result.
+   */
+  AsyncCompletion accept(Consumer<? super Throwable> consumer);
+}
diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncResult.java b/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncResult.java
new file mode 100644
index 0000000..b039e77
... 86281 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org