diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..7d4a55cc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,145 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/openjdk:8u171-jdk-stretch + + working_directory: ~/repo + + environment: + TERM: dumb + JAVA_TOOL_OPTIONS: -Xmx2048m + 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 (master branch only) + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; then + echo "Start deployment" + ./gradlew deploy + else + echo "Start dry run deployment" + export BINTRAY_DRYRUN=true + ./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 00000000..470eda26 --- /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 00000000..e9de15c4 --- /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 00000000..2ce8720d --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*.bak +*.swp +*.tmp +*~.nib +*.iml +*.launch +*.swp +*.tokens +.classpath +.externalToolBuilders/ +.gradle/ +.idea/ +.loadpath +.metadata +.prefs +.project +.recommenders/ +.settings +.springBeans +.vertx +bin/ +local.properties +target/ +tmp/ +build/ +out/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..184f98fb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "eth-reference-tests/src/test/resources/tests"] + path = eth-reference-tests/src/test/resources/tests + url = https://github.com/ethereum/tests.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..723abef8 --- /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, and code that does not comply stylistically will fail to build. + +#### 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.beams.io/posts/git-commit/). + +#### 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 00000000..8dada3ed --- /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 00000000..078ef1bc --- /dev/null +++ b/PACKAGES.md @@ -0,0 +1,119 @@ +# 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 standard 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 standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-concurrent` (`cava-concurrent.jar`). + +# Package net.consensys.cava.config + +A general-purpose library for managing configuration data. + +These classes are included in the standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-config` (`cava-config.jar`). + +# Package net.consensys.cava.concurrent.coroutines.experimental + +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.crypto + +Classes and utilities for working with cryptography. + +These classes are included in the standard 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 also depend upon the JNR-FFI library being available on the classpath, along with its dependencies. 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.eth.domain + +Classes and utilities for working with Ethereum domain objects. + +These classes are included in the standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-eth-domain` (`cava-eth-domain.jar`). + +# Package net.consensys.cava.io + +Classes and utilities for handling file and network IO. + +These classes are included in the standard 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 standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-junit` (`cava-junit.jar`). + +# Package net.consensys.cava.kv + +Classes and utilities for working with key/value stores. + +These classes are included in the standard 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 standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-net` (`cava-net.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 standard 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 standard 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 standard Cava distribution, or separately when using the gradle dependency `net.consensys.cava:cava-merkle-trie` (`cava-merkle-trie.jar`). + +# Package net.consensys.cava.trie.experimental + +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 standard 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 00000000..f12c4cbf --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# 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.1.0) ](https://bintray.com/consensys/consensys/cava/0.1.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/) and linked to JCenter. + +You can import all modules using the cava jar. + +With Maven: +``` + + net.consensys.cava + cava + 0.1.0 + +``` + +With Gradle: `compile 'net.consensys.cava:cava:0.1.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: + +``` +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) +- [Issue tracker: Report a defect or feature request](https://github.com/google/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 00000000..32ab1cbe --- /dev/null +++ b/build.gradle @@ -0,0 +1,394 @@ +import java.util.regex.Pattern + +buildscript { + repositories { + maven { url 'https://consensys.bintray.com/consensys/' } + jcenter() + } + dependencies { classpath 'com.yubico:gradle-gpg-signing-plugin:0.1.0-alpha1' } +} +plugins { + id 'com.diffplug.gradle.spotless' version '3.12.0' + id 'net.ltgt.errorprone' version '0.0.14' + id 'io.spring.dependency-management' version '1.0.4.RELEASE' + id 'com.github.hierynomus.license' version '0.14.0' + id 'com.jfrog.bintray' version '1.8.1' + id 'org.jetbrains.kotlin.jvm' version '1.2.41' + id 'org.jetbrains.dokka' version '0.9.17' +} + + +////// +// Version numbering + +def versionNumber = '0.1.0' +def buildVersion = versionNumber + buildTag() + +static String buildTag() { + if (System.getenv('BUILD_TAG_MODE') == 'release') { + return '' + } + if (!System.getenv('CIRCLECI')) { + return '-dev' + } + return '-' + System.getenv('CIRCLE_SHA1').take(4).toUpperCase() + + String.format('%02X', System.getenv('CIRCLE_BUILD_NUM').toInteger() % 256, 16) + + (System.getenv('BUILD_ENVIRONMENT_TAG') ?: '-snapshot') +} + + +////// +// 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() + +task deploy() {} + +allprojects { + apply plugin: 'java-library' + apply plugin: 'kotlin' + apply plugin: 'com.diffplug.gradle.spotless' + 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 plugin: 'com.yubico.signing.gpg' + apply from: "${rootDir}/dependency-versions.gradle" + apply from: "${rootDir}/gradle/check-licenses.gradle" + + version = buildVersion + + repositories { jcenter() } + + ////// + // Use JUnit5 for testing + + test { useJUnitPlatform() { includeEngines 'spek', 'junit-jupiter' } } + + + ////// + // Source formatting + + spotless { + java { + target project.fileTree(project.rootDir) { + include '**/*.java' + exclude '**/generated-src/**/*.*' + } + removeUnusedImports() + licenseHeaderFile rootProject.file('gradle/license-header.txt') + eclipse().configFile(rootProject.file('gradle/eclipse-java-consensys-style.xml')) + importOrder 'net.consensys', 'java', '' + endWithNewline() + } + groovyGradle { + target '**/*.gradle' + greclipse().configFile(rootProject.file('gradle/greclipse-gradle-consensys-style.properties')) + endWithNewline() + } + kotlin { + licenseHeaderFile rootProject.file('gradle/license-header.txt') + ktlint().userData(['indent_size': '2', 'continuation_indent_size' : '2', 'max_line_length': '120']) + endWithNewline() + } + } + + + ////// + // Compiler arguments + + dependencies { errorprone 'com.google.errorprone:error_prone_core' } + + tasks.withType(JavaCompile) { + options.compilerArgs += [ + '-proc:none', + '-Xlint:unchecked', + '-Xlint:cast', + '-Xlint:rawtypes', + '-Xlint:overloads', + '-Xlint:divzero', + '-Xlint:finally', + '-Xlint:static', + '-Werror', + '-Xep:FutureReturnValueIgnored:OFF' + ] + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + allWarningsAsErrors = true + freeCompilerArgs = ['-Xjsr305=strict'] + } + } + + kotlin.experimental.coroutines = 'enable' + + + ////// + // 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" + } + } + + + ////// + // 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) + } + } + + jar { + destinationDir = file("${rootProject.buildDir}/libs") + } + + task sourcesJar(type: Jar, dependsOn: classes) { + destinationDir = file("${rootProject.buildDir}/src") + classifier = 'sources' + from sourceSets.main.allSource + } + + task dokkaJar(type: Jar, dependsOn: dokka) { + destinationDir = file("${rootProject.buildDir}/docs") + classifier = 'dokka' + from dokka.outputDirectory + } + + if (project.name != 'eth-reference-tests') { + signing { + useGpgCmd() + sign configurations.archives + } + + tasks.withType(Sign) { + onlyIf { System.getenv('ENABLE_SIGNING') == 'true' } + } + + artifacts { + archives jar + archives sourcesJar + archives dokkaJar + } + + publishing { + publications { + MavenDeployment(MavenPublication) { publication -> + from components.java + artifact sourcesJar { classifier 'sources' } + artifact dokkaJar { classifier 'javadoc' } + groupId 'net.consensys.cava' + artifactId project.jar.baseName + version project.version + + pom.withXml { + def root = asNode() + root.appendNode('description', project.description) + root.appendNode('name', project.jar.baseName) + root.appendNode('url', 'https://github.com/ConsenSys/cava') + def license = root.appendNode('licenses').appendNode('license') + license.appendNode('name', 'The Apache Software License, Version 2.0') + license.appendNode('url', 'http://www.apache.org/licenses/LICENSE-2.0.txt') + license.appendNode('distribution', 'repo') + def scm = root.appendNode('scm') + scm.appendNode('url', 'https://github.com/ConsenSys/cava') + scm.appendNode('connection', 'scm:https://github.com/ConsenSys/cava.git') + scm.appendNode('developerConnection', 'scm:git@github.com:ConsenSys/cava.git') + } + } + } + } + + 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_DRYRUN') == '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 + } +} + + +subprojects { + + ////// + // 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 + } + + task allDependencies(type: DependencyReportTask) {} +} + +////// +// Deploy the site + +task publishSite(type: Exec) { commandLine "${rootProject.projectDir}/site/deploy.sh", "${versionNumber}" } + + +////// +// Bundle all subprojects + +subprojects.each { evaluationDependsOn(it.path) } + +jar { + baseName = rootProject.name + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } + subprojects.each { + from it.sourceSets.main.output + dependsOn it.classes + } +} + +dependencies { + subprojects.each { + it.configurations.compile.allDependencies.each { d -> + if (d instanceof ExternalModuleDependency) { + add('compile', d) + } + } + it.configurations.runtime.allDependencies.each { d -> + if (d instanceof ExternalModuleDependency) { + add('runtime', d) + } + } + } +} + +sourcesJar { + baseName = rootProject.name + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } + subprojects.each { + from it.sourceSets.main.allSource + dependsOn it.classes + } +} + +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://atoulme.github.io/kotlinx.coroutines/kotlinx-coroutines-core/') + } +} + +dokkaJar { + baseName = rootProject.name + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +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)") +} diff --git a/bytes/build.gradle b/bytes/build.gradle new file mode 100644 index 00000000..aefc8679 --- /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 00000000..1ad2849b --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/AbstractBytes.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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. + * + *

+ * Two {@link Bytes} values are equal is they have contain the exact same bytes. + * + * @param obj The object to test for equality with. + * @return 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 00000000..2f365630 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes.java @@ -0,0 +1,188 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.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(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 00000000..8859e437 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/ArrayWrappingBytes32.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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/BufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/BufferWrappingBytes.java new file mode 100644 index 00000000..46beb633 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/BufferWrappingBytes.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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)); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof BufferWrappingBytes)) { + return super.equals(obj); + } + BufferWrappingBytes other = (BufferWrappingBytes) obj; + return buffer.equals(other.buffer); + } + + @Override + public int hashCode() { + return buffer.hashCode(); + } + + // 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 00000000..ec0929d9 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufWrappingBytes.java @@ -0,0 +1,130 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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)); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ByteBufWrappingBytes)) { + return super.equals(obj); + } + ByteBufWrappingBytes other = (ByteBufWrappingBytes) obj; + return byteBuf.equals(other.byteBuf); + } + + @Override + public int hashCode() { + return byteBuf.hashCode(); + } + + // 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 00000000..83d910d2 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/ByteBufferWrappingBytes.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 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 00000000..02d68f31 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/Bytes.java @@ -0,0 +1,1144 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; + +import io.netty.buffer.ByteBuf; +import io.vertx.core.buffer.Buffer; + +/** + * A value made of bytes. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (buffer.length() > 0 && offset >= + * buffer.length())}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (byteBuf.capacity() > 0 && offset >= + * byteBuf.capacity())}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (byteBuffer.limit() > 0 && offset >= + * byteBuf.limit())}. + * @throws IllegalArgumentException if {@code length < 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 bytes value corresponding to the provided value interpreted as an unsigned short value. + * + * @param value The value, which must fit an unsigned short. + * @return A 2 bytes value corresponding to {@code v}. + * @throws IllegalArgumentException if {@code v < 0} or {@code v} is too big to fit an unsigned 2-bytes short (that + * is, if {@code v >= (1 << 16)}). + */ + static Bytes ofUnsignedShort(int value) { + 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]; + res[0] = (byte) ((value >> 8) & 0xFF); + res[1] = (byte) (value & 0xFF); + return Bytes.wrap(res); + } + + /** + * Return a 4 bytes value corresponding to the provided value interpreted as an unsigned int value. + * + * @param value The value, which must fit an unsigned int. + * @return A 4 bytes value corresponding to {@code v}. + * @throws IllegalArgumentException if {@code v < 0} or {@code v} is too big to fit an unsigned 4-bytes int (that is, + * if {@code v >= (1L << 32)}). + */ + static Bytes ofUnsignedInt(long value) { + 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]; + res[0] = (byte) ((value >> 24) & 0xFF); + res[1] = (byte) ((value >> 16) & 0xFF); + res[2] = (byte) ((value >> 8) & 0xFF); + res[3] = (byte) ((value) & 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. + * + *

+ * 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 valid hexadecimal representation. + */ + static Bytes fromHexStringLenient(String str) { + checkNotNull(str); + return BytesValues.fromHexString(str, -1, true); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value of the provided size. + * + *

+ * 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 valid hexadecimal representation, represents + * more bytes than {@code destinationSize} or {@code destinationSize < 0}. + */ + static Bytes fromHexStringLenient(String 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. + * + *

+ * 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 valid hexadecimal representation, or is of + * an odd length. + */ + static Bytes fromHexString(String str) { + checkNotNull(str); + return BytesValues.fromHexString(str, -1, false); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value. + * + *

+ * 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 valid hexadecimal representation, or is of an + * odd length. + * @throws IllegalArgumentException if {@code str} does not correspond to valid hexadecimal representation, or is of + * an odd length, or represents more bytes than {@code destinationSize} or {@code destinationSize < 0}. + */ + static Bytes fromHexString(String str, int destinationSize) { + checkNotNull(str); + checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize); + return BytesValues.fromHexString(str, destinationSize, false); + } + + /** @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 < 0} or {i >= 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 < 0} or {@code i > size() - 4}. + */ + default int getInt(int i) { + 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; + 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); + 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() > 4}. + */ + default int intValue() { + int i = size(); + checkArgument(i <= 4, "Value of size %s has more than 4 bytes", size()); + if (i == 0) { + return 0; + } + 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; + } + + /** + * 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 < 0} or {@code i > size() - 8}. + */ + default long getLong(int i) { + 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; + 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); + 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() > 8}. + */ + default long longValue() { + int i = size(); + checkArgument(i <= 8, "Value of size %s has more than 8 bytes", size()); + if (i == 0) { + return 0; + } + 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; + } + + /** + * 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 bigIntegerValue() { + if (size() == 0) { + return BigInteger.ZERO; + } + return new BigInteger(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 unsignedBigIntegerValue() { + return new BigInteger(1, toArrayUnsafe()); + } + + /** + * Whether this value has only zero bytes. + * + * @return 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 - 1; + } + } + 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. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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 or(other, MutableBytes.create(Math.max(size(), other.size()))); + } + + /** + * Calculate a bit-wise XOR of these bytes and the supplied bytes. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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 The {@link MutableBytes} value type. + * @return The {@code result} output vector. + */ + default 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. + * + *

+ * 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 < 0}. + */ + default Bytes slice(int i) { + 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. + * + *

+ * 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 < 0}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()} or {i + length > 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 not 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. + * + *

+ * 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() > (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 provided Vert.x {@link Buffer}. + * + *

+ * 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)); + } + } + + /** + * 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); + for (int i = 0; i < size(); i++) { + digest.update(get(i)); + } + } + + /** + * 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. + * + *

+ * 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() { + int size = size(); + StringBuilder r = new StringBuilder(2 + size * 2); + r.append("0x"); + + for (int i = 0; i < size; i++) { + byte b = get(i); + r.append(AbstractBytes.HEX_CODE[b >> 4 & 15]); + r.append(AbstractBytes.HEX_CODE[b & 15]); + } + + return r.toString(); + } + + /** @return This value represented as a minimal hexadecimal string (without any leading zero). */ + default String toShortHexString() { + String hex = toString(); + // Skipping '0x' + if (hex.charAt(2) != '0') + return hex; + + int i = 3; + while (i < hex.length() && hex.charAt(i) == '0') { + i++; + } + return "0x" + hex.substring(i); + } +} 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 00000000..31d1f4f6 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/Bytes32.java @@ -0,0 +1,240 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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; + +/** + * 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}. + * + *

+ * 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}. + * + *

+ * 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 < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > 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}. + * + *

+ * 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}. + * + *

+ * 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 < 0 || (value.size() > 0 && offset >= + * value.size())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > 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() > 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; + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

+ * 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 valid hexadecimal representation or contains + * more than 32 bytes. + */ + static Bytes32 fromHexStringLenient(String str) { + checkNotNull(str); + return wrap(BytesValues.fromRawHexString(str, SIZE, true)); + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

+ * 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 valid hexadecimal representation, is of an + * odd length, or contains more than 32 bytes. + */ + static Bytes32 fromHexString(String str) { + checkNotNull(str); + return wrap(BytesValues.fromRawHexString(str, SIZE, false)); + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

+ * 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 valid hexadecimal representation, is of an + * odd length or does not contain exactly 32 bytes. + */ + static Bytes32 fromHexStringStrict(String 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/BytesValues.java b/bytes/src/main/java/net/consensys/cava/bytes/BytesValues.java new file mode 100644 index 00000000..d81e6f22 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/BytesValues.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 Bytes fromHexString(String str, int destSize, boolean lenient) { + return Bytes.wrap(fromRawHexString(str, destSize, lenient)); + } + + static byte[] fromRawHexString(String str, int destSize, boolean lenient) { + String hex = str; + if (str.startsWith("0x")) { + hex = str.substring(2); + } + + int len = hex.length(); + int idxShift = 0; + if (len % 2 != 0) { + if (!lenient) { + throw new IllegalArgumentException("Invalid odd-length hex binary representation '" + str + "'"); + } + + hex = "0" + hex; + len += 1; + idxShift = 1; + } + + int size = len / 2; + if (destSize < 0) { + destSize = size; + } else { + checkArgument( + size <= destSize, + "Hex value %s is too big: expected at most %s bytes but got %s", + str, + 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 '%s'", + hex.charAt(i), + i - idxShift, + str)); + } + if (l == -1) { + throw new IllegalArgumentException( + String.format( + "Illegal character '%c' found at index %d in hex binary representation '%s'", + hex.charAt(i + 1), + i + 1 - idxShift, + str)); + } + + 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 00000000..310be19f --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/ConcatenatedBytes.java @@ -0,0 +1,213 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..652a34c8 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingBytes32.java @@ -0,0 +1,234 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.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 intValue() { + return delegate.intValue(); + } + + @Override + public long getLong(int i) { + return delegate.getLong(i); + } + + @Override + public long longValue() { + return delegate.longValue(); + } + + @Override + public BigInteger bigIntegerValue() { + return delegate.bigIntegerValue(); + } + + @Override + public BigInteger unsignedBigIntegerValue() { + return delegate.unsignedBigIntegerValue(); + } + + @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 and(Bytes other, T result) { + return delegate.and(other, result); + } + + @Override + public Bytes or(Bytes other) { + return delegate.or(other); + } + + @Override + public T or(Bytes other, T result) { + return delegate.or(other, result); + } + + @Override + public Bytes xor(Bytes other) { + return delegate.xor(other); + } + + @Override + public T xor(Bytes other, T result) { + return delegate.xor(other, result); + } + + @Override + public T not(T result) { + return delegate.not(result); + } + + @Override + public T shiftRight(int distance, T result) { + return delegate.shiftRight(distance, result); + } + + @Override + public 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(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 00000000..aea97788 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/DelegatingMutableBytes32.java @@ -0,0 +1,264 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.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 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 intValue() { + return delegate.intValue(); + } + + @Override + public long getLong(int i) { + return delegate.getLong(i); + } + + @Override + public long longValue() { + return delegate.longValue(); + } + + @Override + public BigInteger bigIntegerValue() { + return delegate.bigIntegerValue(); + } + + @Override + public BigInteger unsignedBigIntegerValue() { + return delegate.unsignedBigIntegerValue(); + } + + @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 and(Bytes other, T result) { + return delegate.and(other, result); + } + + @Override + public Bytes or(Bytes other) { + return delegate.or(other); + } + + @Override + public T or(Bytes other, T result) { + return delegate.or(other, result); + } + + @Override + public Bytes xor(Bytes other) { + return delegate.xor(other); + } + + @Override + public T xor(Bytes other, T result) { + return delegate.xor(other, result); + } + + @Override + public T not(T result) { + return delegate.not(result); + } + + @Override + public T shiftRight(int distance, T result) { + return delegate.shiftRight(distance, result); + } + + @Override + public 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(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 00000000..441e4e73 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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, size()); + this.bytes[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, + "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 00000000..891fab3d --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableArrayWrappingBytes32.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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/MutableBufferWrappingBytes.java b/bytes/src/main/java/net/consensys/cava/bytes/MutableBufferWrappingBytes.java new file mode 100644 index 00000000..5fcd727a --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBufferWrappingBytes.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..ff56eca8 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufWrappingBytes.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..d3c47021 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableByteBufferWrappingBytes.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..0f71d383 --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes.java @@ -0,0 +1,295 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.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. + * + *

+ * 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 < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (buffer.length() > 0 && offset >= + * buffer.length())}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (byteBuf.capacity() > 0 && offset >= + * byteBuf.capacity())}. + * @throws IllegalArgumentException if {@code length < 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. + * + *

+ * 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. + * + *

+ * 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 < 0 || (byteBuffer.limit() > 0 && offset >= + * byteBuffer.limit())}. + * @throws IllegalArgumentException if {@code length < 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); + } + + /** + * 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 >= 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 < 0} or {@code i > 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 < 0} or {@code i > 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)); + } + + /** + * Create a mutable slice of the bytes of this value. + * + *

+ * 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 < 0}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()} or {i + length > 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 00000000..5aa1a92e --- /dev/null +++ b/bytes/src/main/java/net/consensys/cava/bytes/MutableBytes32.java @@ -0,0 +1,110 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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}. + * + *

+ * 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 < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > 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}. + * + *

+ * 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}. + * + *

+ * 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 < 0 || (value.size() > 0 && offset >= + * value.size())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > 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/package-info.java b/bytes/src/main/java/net/consensys/cava/bytes/package-info.java new file mode 100644 index 00000000..3707b77a --- /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. + * + *

+ * 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 00000000..1fb885be --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/BufferBytesTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..3aa4e1d8 --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufBytesTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..5aac46b9 --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/ByteBufferBytesTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 00000000..4859d027 --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/Bytes32Test.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 failsWhenLeftPaddingValueLargerThan32() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes32.leftPad(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/BytesTest.java b/bytes/src/test/java/net/consensys/cava/bytes/BytesTest.java new file mode 100644 index 00000000..1ad04ece --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/BytesTest.java @@ -0,0 +1,370 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.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 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 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.unsignedBigIntegerValue()); + assertEquals(bi("1"), Bytes.of(1).unsignedBigIntegerValue()); + } + + @Test + void asSignedBigIntegerConstants() { + assertEquals(bi("0"), Bytes.EMPTY.bigIntegerValue()); + assertEquals(bi("1"), Bytes.of(1).bigIntegerValue()); + } + + @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 'foo'", 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 'foo'", exception.getMessage()); + } + + @Test + void fromHexStringLenientLeftPaddingInvalidSize() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2)); + assertEquals("Hex value 0x001F34 is too big: 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 'fooo'", exception.getMessage()); + } + + @Test + void fromHexStringNotLenient() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100")); + assertEquals("Invalid odd-length hex binary representation '0x100'", 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 'fooo'", exception.getMessage()); + } + + @Test + void fromHexStringLeftPaddingNotLenient() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100", 4)); + assertEquals("Invalid odd-length hex binary representation '0x100'", exception.getMessage()); + } + + @Test + void fromHexStringLeftPaddingInvalidSize() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2)); + assertEquals("Hex value 0x001F34 is too big: expected at most 2 bytes but got 3", exception.getMessage()); + } +} 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 00000000..c90d049b --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/CommonBytesTests.java @@ -0,0 +1,575 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.function.Function; + +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").unsignedBigIntegerValue()); + + // 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.unsignedBigIntegerValue()); + } + + @Test + void testAsSignedBigInteger() { + // Make sure things are interpreted signed. + assertEquals(bi("-1"), h("0xFF").bigIntegerValue()); + + // 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.bigIntegerValue()); + + // 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.bigIntegerValue()); + } + + @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.intValue()); + Bytes value1 = w(new byte[] {0, 0, 1, 0}); + // 0x00000100 = 256 + assertEquals(256, value1.intValue()); + assertEquals(256, value1.slice(2).intValue()); + + Bytes value2 = w(new byte[] {0, 1, 0, -1}); + // 0x000100FF = 65536 + 255 = 65791 + assertEquals(65791, value2.intValue()); + assertEquals(65791, value2.slice(1).intValue()); + + Bytes value3 = w(new byte[] {1, 0, -1, -1}); + // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751 + assertEquals(16842751, value3.intValue()); + + Bytes value4 = w(new byte[] {-1, -1, -1, -1}); + // 0xFFFFFFFF = -1 + assertEquals(-1, value4.intValue()); + } + + @Test + void testAsIntTooManyBytes() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> w(new byte[] {1, 2, 3, 4, 5}).intValue()); + 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.longValue()); + Bytes value1 = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1}); + // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071 + assertEquals(1103806595071L, value1.longValue()); + assertEquals(1103806595071L, value1.slice(2).longValue()); + Bytes value2 = w(new byte[] {0, 1, 0, -1, -1, -1, -1, 0}); + // 0x000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176 + assertEquals(282574488338176L, value2.longValue()); + assertEquals(282574488338176L, value2.slice(1).longValue()); + + Bytes value3 = w(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1}); + assertEquals(-1L, value3.longValue()); + } + + @Test + void testAsLongTooManyBytes() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}).longValue()); + 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 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 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()); + } +} 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 00000000..bf03bb3b --- /dev/null +++ b/bytes/src/test/java/net/consensys/cava/bytes/ConcatenatedBytesTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 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/build.gradle b/concurrent/build.gradle new file mode 100644 index 00000000..58dd6be5 --- /dev/null +++ b/concurrent/build.gradle @@ -0,0 +1,13 @@ +description = 'Classes and utilities for working with concurrency.' + +dependencies { + compile 'com.google.guava:guava' + compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + 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 00000000..749b531a --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncCompletion.java @@ -0,0 +1,478 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 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 cs) { + @SuppressWarnings("rawtypes") + java.util.concurrent.CompletableFuture[] completableFutures = cs.map(completion -> { + java.util.concurrent.CompletableFuture 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 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. + * + *

+ * 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 true if completed normally, completed exceptionally or cancelled. + * + * @return true if completed. + */ + boolean isDone(); + + /** + * Returns true if completed exceptionally or cancelled. + * + * @return true if completed exceptionally or cancelled. + */ + boolean isCompletedExceptionally(); + + /** + * Attempt to cancel execution of this task. + * + *

+ * 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. + * + *

+ * After this method returns, subsequent calls to {@link #isDone()} will always return true. Subsequent calls + * to {@link #isCancelled()} will always return true if this method returned true. + * + * @return true if this completion transitioned to a cancelled state. + */ + boolean cancel(); + + /** + * Returns true if this task was cancelled before it completed normally. + * + * @return 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 The type of the returned result's value. + * @return A new result. + */ + AsyncResult then(Supplier> 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 The type of the returned result's value. + * @return A new result. + */ + AsyncResult thenSchedule(Vertx vertx, Supplier> 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 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 The function's return type. + * @return A new result. + */ + AsyncResult thenSupply(Supplier 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 The function's return type. + * @return A new result. + */ + AsyncResult thenSupply(Vertx vertx, Supplier 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 The type of the other's value. + * @return A new result. + */ + AsyncCompletion thenConsume(AsyncResult other, Consumer 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 The type of the other's value. + * @param The type of the value returned by the function. + * @return A new result. + */ + AsyncResult thenApply(AsyncResult other, Function 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 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). + *

+ * 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 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. + *

+ * The exception supplied to the function will be {@code null} if this completion completes successfully. + * + * @param fn The function to execute. + * @param The type of the value returned from the function. + * @return A new result. + */ + AsyncResult handle(Function fn); + + /** + * Returns a new completion that completes successfully, after executing the given function with this completion's + * exception (if any). + *

+ * 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 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 00000000..6a99ac2d --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/AsyncResult.java @@ -0,0 +1,515 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.*; +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.*; +import java.util.stream.Stream; + +import io.vertx.core.Vertx; +import io.vertx.core.WorkerExecutor; + +/** + * A result that will be available at a future time. + * + * @param The type of the result object. + */ +public interface AsyncResult { + + /** + * Return an already completed result containing the given value. + * + * @param value The value. + * @param The type of the value. + * @return A completed result. + */ + static AsyncResult completed(T value) { + requireNonNull(value); + CompletableAsyncResult result = new DefaultCompletableAsyncResult<>(); + result.complete(value); + return result; + } + + /** + * Return an already failed result, caused by the given exception. + * + * @param ex The exception. + * @param The type of the value that would be available if this result hadn't completed exceptionally. + * @return A failed result. + */ + static AsyncResult exceptional(Throwable ex) { + requireNonNull(ex); + CompletableAsyncResult result = new DefaultCompletableAsyncResult<>(); + result.completeExceptionally(ex); + return result; + } + + /** + * Return an incomplete result, that can be later completed or failed. + * + * @param The type of the value that this result will complete with. + * @return An incomplete result. + */ + static CompletableAsyncResult incomplete() { + return new DefaultCompletableAsyncResult<>(); + } + + /** + * Returns an {@link AsyncCompletion} that completes when all of the given results complete. If any results complete + * exceptionally, then the resulting completion also completes exceptionally. + * + * @param rs The results to combine. + * @return A completion. + */ + static AsyncCompletion allOf(AsyncResult... rs) { + return allOf(Arrays.stream(rs)); + } + + /** + * Returns an {@link AsyncCompletion} that completes when all of the given results complete. If any results complete + * exceptionally, then the resulting completion also completes exceptionally. + * + * @param rs The results to combine. + * @return A completion. + */ + static AsyncCompletion allOf(Collection> rs) { + return allOf(rs.stream()); + } + + /** + * Returns an {@link AsyncCompletion} that completes when all of the given results complete. If any results complete + * exceptionally, then the resulting completion also completes exceptionally. + * + * @param rs The results to combine. + * @return A completion. + */ + static AsyncCompletion allOf(Stream> rs) { + @SuppressWarnings("rawtypes") + java.util.concurrent.CompletableFuture[] completableFutures = rs.map(result -> { + java.util.concurrent.CompletableFuture javaFuture = new java.util.concurrent.CompletableFuture<>(); + result.whenComplete((v, 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 result that completes when all of the given results complete. If any results complete exceptionally, then + * the resulting completion also completes exceptionally. + * + * @param The type of the values that this result will complete with. + * @param rs The results to combine. + * @return A new result. + */ + static AsyncResult> combine(Collection> rs) { + return combine(rs.stream()); + } + + /** + * Returns a result that completes when all of the given results complete. If any results complete exceptionally, then + * the resulting completion also completes exceptionally. + * + * @param The type of the values that this result will complete with. + * @param rs The results to combine. + * @return A new result. + */ + static AsyncResult> combine(Stream> rs) { + Stream>> ls = rs.map(r -> r.thenApply(Collections::singletonList)); + return ls.reduce(AsyncResult.completed(new ArrayList<>()), (r1, r2) -> r1.thenCombine(r2, (l1, l2) -> { + l1.addAll(l2); + return l1; + })); + } + + /** + * Returns a result that, after the given function executes on a vertx context and returns a result, completes when + * the returned result completes, with the same value or exception. + * + *

+ * Note that the given function is run directly on the context and should not block. + * + * @param vertx The vertx context. + * @param fn The function returning a result. + * @param The type of the returned result's value. + * @return A new result. + */ + static AsyncResult runOnContext(Vertx vertx, Supplier> fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + vertx.runOnContext(ev -> { + try { + fn.get().whenComplete((u, ex2) -> { + if (ex2 == null) { + try { + asyncResult.complete(u); + } catch (Throwable ex3) { + asyncResult.completeExceptionally(ex3); + } + } else { + asyncResult.completeExceptionally(ex2); + } + }); + } catch (Throwable ex1) { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + /** + * Returns a result that, after the given blocking function executes asynchronously on + * {@link ForkJoinPool#commonPool()} and returns a result, completes when the returned result completes, with the same + * value or exception. + * + * @param fn The function returning a result. + * @param The type of the returned result's value. + * @return A new result. + */ + static AsyncResult executeBlocking(Supplier fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + ForkJoinPool.commonPool().execute(() -> { + try { + asyncResult.complete(fn.get()); + } catch (Throwable ex) { + asyncResult.completeExceptionally(ex); + } + }); + return asyncResult; + } + + /** + * Returns a result that, after the given blocking function executes asynchronously on an {@link Executor} and returns + * a result, completes when the returned result completes, with the same value or exception. + * + * @param executor The executor. + * @param fn The function returning a result. + * @param The type of the returned result's value. + * @return A new result. + */ + static AsyncResult executeBlocking(Executor executor, Supplier fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + executor.execute(() -> { + try { + asyncResult.complete(fn.get()); + } catch (Throwable ex) { + asyncResult.completeExceptionally(ex); + } + }); + return asyncResult; + } + + /** + * Returns a result that, after the given blocking function executes asynchronously on a vertx context and returns a + * result, completes when the returned result completes, with the same value or exception. + * + * @param vertx The vertx context. + * @param fn The function returning a result. + * @param The type of the returned result's value. + * @return A new result. + */ + static AsyncResult executeBlocking(Vertx vertx, Supplier fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + vertx.executeBlocking(future -> future.complete(fn.get()), false, res -> { + if (res.succeeded()) { + asyncResult.complete(res.result()); + } else { + asyncResult.completeExceptionally(res.cause()); + } + }); + return asyncResult; + } + + /** + * Returns a result that, after the given blocking function executes asynchronously on a vertx executor and returns a + * result, completes when the returned result completes, with the same value or exception. + * + * @param executor A vertx executor. + * @param fn The function returning a result. + * @param The type of the returned result's value. + * @return A new result. + */ + static AsyncResult executeBlocking(WorkerExecutor executor, Supplier fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + executor.executeBlocking(future -> future.complete(fn.get()), false, res -> { + if (res.succeeded()) { + asyncResult.complete(res.result()); + } else { + asyncResult.completeExceptionally(res.cause()); + } + }); + return asyncResult; + } + + /** + * Returns true if completed normally, completed exceptionally or cancelled. + * + * @return true if completed. + */ + boolean isDone(); + + /** + * Returns true if completed exceptionally or cancelled. + * + * @return true if completed exceptionally or cancelled. + */ + boolean isCompletedExceptionally(); + + /** + * Attempt to cancel execution of this task. + * + *

+ * 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. + * + *

+ * After this method returns, subsequent calls to {@link #isDone()} will always return true. Subsequent calls + * to {@link #isCancelled()} will always return true if this method returned true. + * + * @return true if this result transitioned to a cancelled state. + */ + boolean cancel(); + + /** + * Returns true if this task was cancelled before it completed normally. + * + * @return true if completed. + */ + boolean isCancelled(); + + /** + * Waits if necessary for the computation to complete, and then retrieves its result. + * + * @return The computed result. + * @throws CompletionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted while waiting. + */ + T get() throws CompletionException, InterruptedException; + + /** + * Waits if necessary for at most the given time for the computation to complete, and then retrieves its result. + * + * @param timeout The maximum time to wait. + * @param unit The time unit of the timeout argument. + * @return The computed result. + * @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. + */ + T get(long timeout, TimeUnit unit) throws CompletionException, TimeoutException, InterruptedException; + + /** + * Returns a new result that, when this result completes normally, completes with the same value or exception as the + * result returned after executing the given function with this results value as an argument. + * + * @param fn The function returning a new result. + * @param The type of the returned result's value. + * @return A new result. + */ + AsyncResult then(Function> fn); + + /** + * Returns a new result that, when this result completes normally, completes with the same value or exception as the + * completion returned after executing the given function on the vertx context with this results value as an argument. + * + * @param vertx The vertx context. + * @param fn The function returning a new result. + * @param The type of the returned result's value. + * @return A new result. + */ + AsyncResult thenSchedule(Vertx vertx, Function> fn); + + /** + * When this result completes normally, invokes the given function with the resulting value and obtains a new + * {@link AsyncCompletion}. + * + * @param fn The function returning a new completion. + * @return A completion. + */ + AsyncCompletion thenCompose(Function fn); + + /** + * Returns a new completion that, when this result completes normally, completes after given action is executed. + * + * @param runnable The action to execute before completing the returned completion. + * @return A completion. + */ + AsyncCompletion thenRun(Runnable runnable); + + /** + * Returns a new completion that, when this result 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 result 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 result 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); + + /** + * Returns a result that, when this result completes normally, completes with the value obtained from executing the + * supplied function with this result's value as an argument. + * + * @param fn The function to use to compute the value of the returned result. + * @param The function's return type. + * @return A new result. + */ + AsyncResult thenApply(Function fn); + + /** + * Returns a result that, when this result completes normally, completes with the value obtained from executing the + * supplied function on the vertx context with this result's value as an argument. + * + * @param vertx The vertx context. + * @param fn The function to use to compute the value of the returned result. + * @param The function's return type. + * @return A new result. + */ + AsyncResult thenScheduleApply(Vertx vertx, Function fn); + + /** + * Returns a result that, when this result completes normally, completes with the value obtained from executing the + * supplied blocking function on the vertx context with this result's value as an argument. + * + * @param vertx The vertx context. + * @param fn The function to use to compute the value of the returned result. + * @param The function's return type. + * @return A new result. + */ + AsyncResult thenScheduleBlockingApply(Vertx vertx, Function fn); + + /** + * Returns a result that, when this result completes normally, completes with the value obtained from executing the + * supplied blocking function on the vertx executor with this result's value as an argument. + * + * @param executor The vertx executor. + * @param fn The function to use to compute the value of the returned result. + * @param The function's return type. + * @return A new result. + */ + AsyncResult thenScheduleBlockingApply(WorkerExecutor executor, Function fn); + + /** + * Returns a completion that, when this result completes normally, completes after executing the supplied consumer + * with this result's value as an argument. + * + * @param consumer The consumer for the value of this result. + * @return A completion. + */ + AsyncCompletion thenAccept(Consumer consumer); + + /** + * Returns a completion that, when this result and the other result both complete normally, completes after executing + * the supplied consumer with both this result's value and the value from the other result as arguments. + * + * @param other The other result. + * @param consumer The consumer for both values. + * @param The type of the other's value. + * @return A completion. + */ + AsyncCompletion thenAcceptBoth(AsyncResult other, BiConsumer consumer); + + /** + * Returns a result that, when this result and the other result both complete normally, completes with the value + * obtained from executing the supplied function with both this result's value and the value from the other result as + * arguments. + * + * @param other The other result. + * @param fn The function to execute. + * @param The type of the other's value. + * @param The type of the value returned by the function. + * @return A new result. + */ + AsyncResult thenCombine(AsyncResult other, BiFunction fn); + + /** + * Returns a new result that, when this result completes exceptionally, completes with the value obtained from + * executing the supplied function with this result's exception as an argument. Otherwise, if this result completes + * normally, then the returned result also completes normally with the same value. + * + * @param fn The function to execute. + * @return A new result. + */ + AsyncResult exceptionally(Function fn); + + /** + * Returns a new result that completes with the same value or exception as this result, after executing the given + * action with this result's value or exception. + *

+ * Either the value or the exception supplied to the action will be {@code null}. + * + * @param action The action to execute. + * @return A new result. + */ + AsyncResult whenComplete(BiConsumer action); + + /** + * 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 value and exception as arguments. + *

+ * Either the value or the exception supplied to the function will be {@code null}. + * + * @param fn The function to execute. + * @param The type of the value returned from the function. + * @return A new result. + */ + AsyncResult handle(BiFunction fn); + + /** + * Returns a new completion that, when this result completes either normally or exceptionally, completes after + * executing the supplied function with this result's value and exception as arguments. + *

+ * Either the value or the exception supplied to the function will be {@code null}. + * + * @param consumer The consumer to execute. + * @return A completion. + */ + AsyncCompletion accept(BiConsumer consumer); +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/AtomicSlotMap.java b/concurrent/src/main/java/net/consensys/cava/concurrent/AtomicSlotMap.java new file mode 100644 index 00000000..53f97b3f --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/AtomicSlotMap.java @@ -0,0 +1,249 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Stream; + +import com.google.common.collect.DiscreteDomain; + +/** + * An atomic map that locates available keys within a {@link DiscreteDomain}. + * + *

+ * This is an atomic map that will allocate key slots based on availability. It will attempt to keep the range compact + * by filling slots as they become available. + *

+ * This implementation should be used with small sets, as addition is an O(N) operation. + * + * @param The type of the map keys. + * @param The type of values to store in the map. + */ +@SuppressWarnings("rawtypes") // allow ungenerified Comparable types +public final class AtomicSlotMap { + private final DiscreteDomain domain; + private final ConcurrentHashMap> slots = new ConcurrentHashMap<>(); + private final AtomicInteger size = new AtomicInteger(0); + + /** + * Create a slot map over the range of integers > 0. + * + * @param The type of values to store in the map. + * @return A new slot map. + */ + public static AtomicSlotMap positiveIntegerSlots() { + return new AtomicSlotMap<>(PositiveIntegerDomain.INSTANCE); + } + + /** + * Create a slot map over the provided domain. + * + * @param domain The {@link DiscreteDomain} that defines the slots to be used. + */ + public AtomicSlotMap(DiscreteDomain domain) { + requireNonNull(domain); + this.domain = domain; + } + + /** + * Add a value to the slot map, using the first available slot. + * + * @param value The value. + * @return The slot that was used to store the value. + */ + public K add(V value) { + requireNonNull(value); + K slot = domain.minValue(); + Optional storedValue = Optional.of(value); + while (slots.containsKey(slot) || slots.putIfAbsent(slot, storedValue) != null) { + slot = domain.next(slot); + } + size.incrementAndGet(); + return slot; + } + + /** + * Put a value into a specific slot. + * + * @param slot The slot to put the value in. + * @param value The value. + * @return The previous value in the slot, if present. + */ + public Optional put(K slot, V value) { + requireNonNull(slot); + requireNonNull(value); + Optional previous = slots.put(slot, Optional.of(value)); + if (previous == null || !previous.isPresent()) { + size.incrementAndGet(); + return Optional.empty(); + } + return previous; + } + + /** + * Find a slot and compute a value for it. + * + * @param fn A function to compute the value for a slot. + * @return The slot for which the value was computed. + */ + public K compute(Function fn) { + requireNonNull(fn); + K slot = domain.minValue(); + // store an empty optional to prevent contention on the slot, then replace with computed value. + Optional placeholder = Optional.empty(); + while (slots.containsKey(slot) || slots.putIfAbsent(slot, placeholder) != null) { + slot = domain.next(slot); + } + try { + if (slots.replace(slot, placeholder, Optional.of(fn.apply(slot)))) { + size.incrementAndGet(); + } + return slot; + } catch (Throwable ex) { + slots.remove(slot, placeholder); + throw ex; + } + } + + /** + * Find a slot and compute a value for it. + * + * @param fn A function to compute the value for a slot. + * @return A result that will complete with the slot for which the value was computed. + */ + public AsyncResult computeAsync(Function> fn) { + requireNonNull(fn); + K slot = domain.minValue(); + // store an empty optional to prevent contention on the slot, then replace with computed value. + Optional placeholder = Optional.empty(); + while (slots.containsKey(slot) || slots.putIfAbsent(slot, placeholder) != null) { + slot = domain.next(slot); + } + K finalSlot = slot; + try { + return fn.apply(finalSlot).thenApply(value -> { + if (slots.replace(finalSlot, placeholder, Optional.of(value))) { + size.incrementAndGet(); + } + return finalSlot; + }); + } catch (Throwable ex) { + slots.remove(finalSlot, placeholder); + throw ex; + } + } + + /** + * Get the value in a slot. + * + * @param slot The slot. + * @return The value, if present. + */ + public Optional get(K slot) { + requireNonNull(slot); + Optional value = slots.get(slot); + if (value == null) { + return Optional.empty(); + } + return value; + } + + /** + * Remove a value from a slot, making the slot available again. + * + * @param slot The slot. + * @return The value that was in the slot, if any. + */ + public Optional remove(K slot) { + requireNonNull(slot); + Optional previous = slots.remove(slot); + if (previous == null) { + return Optional.empty(); + } + size.decrementAndGet(); + return previous; + } + + /** + * @return The number of slots filled. + */ + public int size() { + return size.get(); + } + + /** + * @return A stream over the entries in the slot map. + */ + public Stream> entries() { + return slots.entrySet().stream().filter(e -> e.getValue().isPresent()).map(e -> new Map.Entry() { + @Override + public K getKey() { + return e.getKey(); + } + + @Override + public V getValue() { + return e.getValue().get(); + } + + @Override + public V setValue(Object value) { + throw new UnsupportedOperationException(); + } + }); + } + + /** + * @return A stream over the values stored in the slot map. + */ + public Stream values() { + return slots.values().stream().filter(Optional::isPresent).map(Optional::get); + } + + private static final class PositiveIntegerDomain extends DiscreteDomain { + private static final PositiveIntegerDomain INSTANCE = new PositiveIntegerDomain(); + + @Override + public Integer next(Integer value) { + int i = value; + return (i == Integer.MAX_VALUE) ? null : i + 1; + } + + @Override + public Integer previous(Integer value) { + int i = value; + return (i == 1) ? null : i - 1; + } + + @Override + public long distance(Integer start, Integer end) { + return (long) end - start; + } + + @Override + public Integer minValue() { + return 1; + } + + @Override + public Integer maxValue() { + return Integer.MAX_VALUE; + } + } +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncCompletion.java b/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncCompletion.java new file mode 100644 index 00000000..06dff5bd --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncCompletion.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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; + +/** + * An {@link AsyncCompletion} that can later be completed successfully or with a provided exception. + */ +public interface CompletableAsyncCompletion extends AsyncCompletion { + + /** + * Complete this completion. + * + * @return true if this invocation caused this completion to transition to a completed state, else + * false. + */ + boolean complete(); + + /** + * Complete this completion with the given exception. + * + * @param ex The exception to complete this result with. + * @return true if this invocation caused this completion to transition to a completed state, else + * false. + */ + boolean completeExceptionally(Throwable ex); +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncResult.java b/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncResult.java new file mode 100644 index 00000000..187fe341 --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/CompletableAsyncResult.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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; + +/** + * An {@link AsyncResult} that can be later completed successfully with a provided value, or completed with an + * exception. + * + * @param The type of the value returned by this result. + */ +public interface CompletableAsyncResult extends AsyncResult { + + /** + * Complete this result with the given value. + * + * @param value The value to complete this result with. + * @return true if this invocation caused this result to transition to a completed state, else + * false. + */ + boolean complete(T value); + + /** + * Complete this result with the given exception. + * + * @param ex The exception to complete this result with. + * @return true if this invocation caused this result to transition to a completed state, else + * false. + */ + boolean completeExceptionally(Throwable ex); +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletion.java b/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletion.java new file mode 100644 index 00000000..e7da74a0 --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletion.java @@ -0,0 +1,374 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +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 io.vertx.core.Vertx; +import io.vertx.core.WorkerExecutor; + +final class DefaultCompletableAsyncCompletion implements CompletableAsyncCompletion { + + private final CompletableFuture future; + + DefaultCompletableAsyncCompletion() { + this(new CompletableFuture<>()); + } + + DefaultCompletableAsyncCompletion(CompletableFuture future) { + this.future = future; + } + + @Override + public boolean complete() { + return future.complete(null); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + return future.completeExceptionally(ex); + } + + @Override + public boolean cancel() { + return future.cancel(false); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public boolean isCompletedExceptionally() { + return future.isCompletedExceptionally(); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public void join() throws CompletionException, InterruptedException { + try { + join(10, TimeUnit.SECONDS); + } catch (TimeoutException ex) { + throw new RuntimeException("Default timeout triggered for blocking call to AsyncCompletion::join()", ex); + } + } + + @Override + public void join(long timeout, TimeUnit unit) throws CompletionException, TimeoutException, InterruptedException { + requireNonNull(unit); + try { + future.get(timeout, unit); + } catch (ExecutionException ex) { + throw new CompletionException(ex.getMessage(), ex.getCause()); + } + } + + @Override + public AsyncResult then(Supplier> fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((v, ex1) -> { + if (ex1 == null) { + try { + fn.get().whenComplete((u, ex3) -> { + if (ex3 == null) { + asyncResult.complete(u); + } else { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncResult thenSchedule(Vertx vertx, Supplier> fn) { + requireNonNull(vertx); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((v, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + fn.get().whenComplete((u, ex4) -> { + if (ex4 == null) { + asyncResult.complete(u); + } else { + asyncResult.completeExceptionally(ex4); + } + }); + } catch (Throwable ex3) { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncCompletion thenCompose(Supplier fn) { + requireNonNull(fn); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((v, ex1) -> { + if (ex1 == null) { + try { + fn.get().whenComplete(ex3 -> { + if (ex3 == null) { + completion.complete(); + } else { + completion.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenRun(Runnable action) { + requireNonNull(action); + return new DefaultCompletableAsyncCompletion(future.thenRun(action)); + } + + @Override + public AsyncCompletion thenScheduleRun(Vertx vertx, Runnable runnable) { + requireNonNull(vertx); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + runnable.run(); + } catch (Throwable ex3) { + completion.completeExceptionally(ex3); + return; + } + completion.complete(); + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenScheduleBlockingRun(Vertx vertx, Runnable runnable) { + requireNonNull(vertx); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.executeBlocking(vertxFuture -> { + runnable.run(); + vertxFuture.complete(null); + }, false, res -> { + if (res.succeeded()) { + completion.complete(); + } else { + completion.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenScheduleBlockingRun(WorkerExecutor executor, Runnable runnable) { + requireNonNull(executor); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + executor.executeBlocking(vertxFuture -> { + runnable.run(); + vertxFuture.complete(null); + }, false, res -> { + if (res.succeeded()) { + completion.complete(); + } else { + completion.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncResult thenSupply(Supplier supplier) { + requireNonNull(supplier); + return new DefaultCompletableAsyncResult<>(future.thenApply(v -> supplier.get())); + } + + @Override + public AsyncResult thenSupply(Vertx vertx, Supplier supplier) { + requireNonNull(vertx); + requireNonNull(supplier); + CompletableAsyncResult completion = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + completion.complete(supplier.get()); + } catch (Throwable ex3) { + completion.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenConsume(AsyncResult other, Consumer consumer) { + requireNonNull(other); + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.thenAccept(v -> other.thenAccept(consumer::accept))); + } + + @Override + public AsyncResult thenApply(AsyncResult other, Function fn) { + requireNonNull(other); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((v, ex1) -> { + if (ex1 == null) { + try { + other.whenComplete((u, ex3) -> { + if (ex3 == null) { + asyncResult.complete(fn.apply(u)); + } else { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncCompletion thenCombine(AsyncCompletion other) { + requireNonNull(other); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((v, ex1) -> { + if (ex1 == null) { + try { + other.whenComplete(ex3 -> { + if (ex3 == null) { + completion.complete(); + } else { + completion.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion exceptionally(Consumer consumer) { + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.exceptionally(ex -> { + consumer.accept(ex); + return null; + })); + } + + @Override + public AsyncCompletion whenComplete(Consumer consumer) { + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.whenComplete((v, ex) -> consumer.accept(ex))); + } + + @Override + public AsyncResult handle(Function fn) { + requireNonNull(fn); + return new DefaultCompletableAsyncResult<>(future.handle((v, ex) -> fn.apply(ex))); + } + + @Override + public AsyncCompletion accept(Consumer consumer) { + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.handle((v, ex) -> { + consumer.accept(ex); + return null; + })); + } +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResult.java b/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResult.java new file mode 100644 index 00000000..9b431a2c --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResult.java @@ -0,0 +1,406 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import io.vertx.core.Vertx; +import io.vertx.core.WorkerExecutor; + +final class DefaultCompletableAsyncResult implements CompletableAsyncResult { + + private final CompletableFuture future; + + DefaultCompletableAsyncResult() { + this(new CompletableFuture<>()); + } + + DefaultCompletableAsyncResult(CompletableFuture future) { + this.future = future; + } + + @Override + public boolean complete(T value) { + return future.complete(value); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + return future.completeExceptionally(ex); + } + + @Override + public boolean cancel() { + return future.cancel(false); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public boolean isCompletedExceptionally() { + return future.isCompletedExceptionally(); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public T get() throws CompletionException, InterruptedException { + try { + return get(10, TimeUnit.SECONDS); + } catch (TimeoutException ex) { + throw new RuntimeException("Default timeout triggered for blocking call to AsyncResult::get()", ex); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws CompletionException, TimeoutException, InterruptedException { + requireNonNull(unit); + try { + return future.get(timeout, unit); + } catch (ExecutionException ex) { + throw new CompletionException(ex.getMessage(), ex.getCause()); + } + } + + @Override + public AsyncResult then(Function> fn) { + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + fn.apply(t).whenComplete((u, ex3) -> { + if (ex3 == null) { + asyncResult.complete(u); + } else { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncResult thenSchedule(Vertx vertx, Function> fn) { + requireNonNull(vertx); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + fn.apply(t).whenComplete((u, ex4) -> { + if (ex4 == null) { + asyncResult.complete(u); + } else { + asyncResult.completeExceptionally(ex4); + } + }); + } catch (Throwable ex3) { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncCompletion thenCompose(Function fn) { + requireNonNull(fn); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + fn.apply(t).whenComplete(ex3 -> { + if (ex3 == null) { + completion.complete(); + } else { + completion.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenRun(Runnable action) { + requireNonNull(action); + return new DefaultCompletableAsyncCompletion(future.thenRun(action)); + } + + @Override + public AsyncCompletion thenScheduleRun(Vertx vertx, Runnable runnable) { + requireNonNull(vertx); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + runnable.run(); + } catch (Throwable ex3) { + completion.completeExceptionally(ex3); + return; + } + completion.complete(); + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenScheduleBlockingRun(Vertx vertx, Runnable runnable) { + requireNonNull(vertx); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.executeBlocking(vertxFuture -> { + runnable.run(); + vertxFuture.complete(null); + }, false, res -> { + if (res.succeeded()) { + completion.complete(); + } else { + completion.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncCompletion thenScheduleBlockingRun(WorkerExecutor executor, Runnable runnable) { + requireNonNull(executor); + requireNonNull(runnable); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + executor.executeBlocking(vertxFuture -> { + runnable.run(); + vertxFuture.complete(null); + }, false, res -> { + if (res.succeeded()) { + completion.complete(); + } else { + completion.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + completion.completeExceptionally(ex2); + } + } else { + completion.completeExceptionally(ex1); + } + }); + return completion; + } + + @Override + public AsyncResult thenApply(Function fn) { + requireNonNull(fn); + return new DefaultCompletableAsyncResult<>(future.thenApply(fn)); + } + + @Override + public AsyncResult thenScheduleApply(Vertx vertx, Function fn) { + requireNonNull(vertx); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.runOnContext(ev -> { + try { + asyncResult.complete(fn.apply(t)); + } catch (Throwable ex3) { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncResult thenScheduleBlockingApply(Vertx vertx, Function fn) { + requireNonNull(vertx); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + vertx.executeBlocking(vertxFuture -> vertxFuture.complete(fn.apply(t)), false, res -> { + if (res.succeeded()) { + asyncResult.complete(res.result()); + } else { + asyncResult.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncResult thenScheduleBlockingApply(WorkerExecutor executor, Function fn) { + requireNonNull(executor); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + executor.executeBlocking(vertxFuture -> vertxFuture.complete(fn.apply(t)), false, res -> { + if (res.succeeded()) { + asyncResult.complete(res.result()); + } else { + asyncResult.completeExceptionally(res.cause()); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncCompletion thenAccept(Consumer consumer) { + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.thenAccept(consumer)); + } + + @Override + public AsyncCompletion thenAcceptBoth(AsyncResult other, BiConsumer consumer) { + requireNonNull(other); + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.thenAccept(t -> other.thenAccept(u -> consumer.accept(t, u)))); + } + + @Override + public AsyncResult thenCombine( + AsyncResult other, + BiFunction fn) { + requireNonNull(other); + requireNonNull(fn); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + future.whenComplete((t, ex1) -> { + if (ex1 == null) { + try { + other.whenComplete((u, ex3) -> { + if (ex3 == null) { + asyncResult.complete(fn.apply(t, u)); + } else { + asyncResult.completeExceptionally(ex3); + } + }); + } catch (Throwable ex2) { + asyncResult.completeExceptionally(ex2); + } + } else { + asyncResult.completeExceptionally(ex1); + } + }); + return asyncResult; + } + + @Override + public AsyncResult exceptionally(Function fn) { + requireNonNull(fn); + return new DefaultCompletableAsyncResult<>(future.exceptionally(fn::apply)); + } + + @Override + public AsyncResult whenComplete(BiConsumer action) { + requireNonNull(action); + return new DefaultCompletableAsyncResult<>(future.whenComplete(action::accept)); + } + + @Override + public AsyncResult handle(BiFunction fn) { + requireNonNull(fn); + return new DefaultCompletableAsyncResult<>(future.handle(fn::apply)); + } + + @Override + public AsyncCompletion accept(BiConsumer consumer) { + requireNonNull(consumer); + return new DefaultCompletableAsyncCompletion(future.handle((t, ex) -> { + consumer.accept(t, ex); + return null; + })); + } +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/ExpiringMap.java b/concurrent/src/main/java/net/consensys/cava/concurrent/ExpiringMap.java new file mode 100644 index 00000000..5449cb10 --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/ExpiringMap.java @@ -0,0 +1,261 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.function.LongSupplier; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; + +/** + * A concurrent hash map that stores values along with an expiry. + * + * Values are stored in the map until their expiry is reached, after which they will no longer be available and will + * appear as if removed. The actual removal is done lazily whenever the map is accessed, or when the + * {@link #purgeExpired()} method is invoked. + * + * @param The key type. + * @param The value type. + */ +public final class ExpiringMap implements Map { + + private static final long MAX_EXPIRY = Long.MAX_VALUE; + + // Uses object equality, to ensure uniqueness as a value in the storage map + private static final class ExpiringEntry implements Comparable> { + private K key; + private V value; + private long expiry; + + ExpiringEntry(K key, V value, long expiry) { + this.key = key; + this.value = value; + this.expiry = expiry; + } + + @Override + public int compareTo(ExpiringEntry o) { + return Long.compare(expiry, o.expiry); + } + } + + private final ConcurrentHashMap> storage = new ConcurrentHashMap<>(); + private final PriorityBlockingQueue> expiryQueue = new PriorityBlockingQueue<>(); + private final LongSupplier currentTimeSupplier; + + /** + * Construct an empty map. + */ + public ExpiringMap() { + this(System::currentTimeMillis); + } + + @VisibleForTesting + ExpiringMap(LongSupplier currentTimeSupplier) { + this.currentTimeSupplier = currentTimeSupplier; + } + + @Override + public V get(Object key) { + requireNonNull(key); + purgeExpired(); + ExpiringEntry entry = storage.get(key); + return (entry == null) ? null : entry.value; + } + + @Override + public boolean containsKey(Object key) { + requireNonNull(key); + purgeExpired(); + return storage.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + requireNonNull(value); + purgeExpired(); + return storage.values().stream().anyMatch(e -> e.value.equals(value)); + } + + @Override + public int size() { + purgeExpired(); + return storage.size(); + } + + @Override + public boolean isEmpty() { + purgeExpired(); + return storage.isEmpty(); + } + + @Override + public V put(K key, V value) { + requireNonNull(key); + requireNonNull(value); + purgeExpired(); + ExpiringEntry oldEntry = storage.put(key, new ExpiringEntry<>(key, value, MAX_EXPIRY)); + return (oldEntry == null) ? null : oldEntry.value; + } + + /** + * Associates the specified value with the specified key in this map, and expires the entry when the specified expiry + * time is reached. If the map previously contained a mapping for the key, the old value is replaced by the specified + * value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * @param expiry The expiry time for the value, in milliseconds since the epoch. + * @return The previous value associated with {@code key}, or {@code null} if there was no mapping for {@code key}. + */ + public synchronized V put(K key, V value, long expiry) { + requireNonNull(key); + requireNonNull(value); + if (expiry >= MAX_EXPIRY) { + return put(key, value); + } + + long now = currentTimeSupplier.getAsLong(); + if (expiry <= now) { + return remove(key); + } + + purgeExpired(now); + ExpiringEntry newEntry = new ExpiringEntry<>(key, value, expiry); + ExpiringEntry oldEntry = storage.put(key, newEntry); + expiryQueue.offer(newEntry); + return (oldEntry == null) ? null : oldEntry.value; + } + + @Override + public void putAll(Map m) { + requireNonNull(m); + purgeExpired(); + for (Map.Entry e : m.entrySet()) { + storage.put(e.getKey(), new ExpiringEntry<>(e.getKey(), e.getValue(), MAX_EXPIRY)); + } + } + + @Override + public V remove(Object key) { + requireNonNull(key); + purgeExpired(); + ExpiringEntry entry = storage.remove(key); + if (entry == null) { + return null; + } + if (entry.expiry < MAX_EXPIRY) { + expiryQueue.remove(entry); + } + return entry.value; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + requireNonNull(key); + requireNonNull(value); + purgeExpired(); + ExpiringEntry entry = storage.get(key); + if (entry == null || !value.equals(entry.value)) { + return false; + } + storage.remove(key); + if (entry.expiry < MAX_EXPIRY) { + expiryQueue.remove(entry); + } + return true; + } + + @Override + public synchronized void clear() { + expiryQueue.clear(); + storage.clear(); + } + + @Override + public Set keySet() { + purgeExpired(); + return storage.keySet(); + } + + @Override + public Collection values() { + purgeExpired(); + return storage.values().stream().map(e -> e.value).collect(Collectors.toList()); + } + + @Override + public Set> entrySet() { + purgeExpired(); + return storage.entrySet().stream().map(e -> new Map.Entry() { + @Override + public K getKey() { + return e.getKey(); + } + + @Override + public V getValue() { + return e.getValue().value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + }).collect(Collectors.toSet()); + } + + /** + * Force immediate expiration of any key/value pairs that have reached their expiry. + */ + public void purgeExpired() { + purgeExpired(currentTimeSupplier.getAsLong()); + } + + private synchronized void purgeExpired(long oldest) { + ExpiringEntry head; + while ((head = expiryQueue.peek()) != null && head.expiry <= oldest) { + // only remove if it's still mapped to the same entry (object equality is used) + storage.remove(head.key, head); + expiryQueue.remove(); + } + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof ExpiringMap)) { + return false; + } + ExpiringMap other = (ExpiringMap) obj; + return storage.equals(other.storage); + } + + @Override + public int hashCode() { + return storage.hashCode(); + } +} diff --git a/concurrent/src/main/java/net/consensys/cava/concurrent/package-info.java b/concurrent/src/main/java/net/consensys/cava/concurrent/package-info.java new file mode 100644 index 00000000..136ab2bb --- /dev/null +++ b/concurrent/src/main/java/net/consensys/cava/concurrent/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes and utilities for working with concurrency. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-concurrent' (cava-concurrent.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.concurrent; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncCompletion.kt b/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncCompletion.kt new file mode 100644 index 00000000..c698e26f --- /dev/null +++ b/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncCompletion.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.experimental + +import kotlinx.coroutines.experimental.CancellableContinuation +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.experimental.CoroutineDispatcher +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.CoroutineStart +import kotlinx.coroutines.experimental.DefaultDispatcher +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.newCoroutineContext +import kotlinx.coroutines.experimental.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.experimental.Continuation +import kotlin.coroutines.experimental.ContinuationInterceptor +import kotlin.coroutines.experimental.CoroutineContext + +/** + * Starts new coroutine and returns its result as an [AsyncCompletion]. + * + * This coroutine builder uses [CommonPool] context by default and is conceptually similar to + * [AsyncCompletion.executeBlocking]. + * + * The running coroutine is cancelled when the [AsyncCompletion] is cancelled or otherwise completed. + * + * The [context] for the new coroutine can be explicitly specified. See [CoroutineDispatcher] for the standard context + * implementations that are provided by `kotlinx.coroutines`. The [context][CoroutineScope.coroutineContext] of the + * parent coroutine from its [scope][CoroutineScope] may be used, in which case the [Job] of the resulting coroutine is + * a child of the job of the parent coroutine. The parent job may be also explicitly specified using [parent] + * parameter. + * + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [DefaultDispatcher] is + * used. + * + * By default, the coroutine 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 + * [AsyncCompletion] does not provide the corresponding capability) and produces [IllegalArgumentException]. + * + * See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine. + * + * @param context context of the coroutine. The default value is [DefaultDispatcher]. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param parent explicitly specifies the parent job, overrides job from the [context] (if any). + * @param block the coroutine code. + */ +fun asyncCompletion( + context: CoroutineContext = DefaultDispatcher, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + block: suspend CoroutineScope.() -> Unit +): AsyncCompletion { + require(!start.isLazy) { "$start start is not supported" } + val newContext = newCoroutineContext(context, parent) + val job = Job(newContext[Job]) + val coroutine = AsyncCompletionCoroutine(newContext + job) + job.invokeOnCompletion { coroutine.completion.cancel() } + coroutine.completion.whenComplete { exception -> job.cancel(exception) } + start(block, receiver = coroutine, completion = coroutine) // use the specified start strategy + return coroutine.completion +} + +private class AsyncCompletionCoroutine( + override val context: CoroutineContext, + val completion: CompletableAsyncCompletion = AsyncCompletion.incomplete() +) : Continuation, CoroutineScope { + override val coroutineContext: CoroutineContext get() = context + override val isActive: Boolean get() = context[Job]!!.isActive + override fun resume(value: Unit) { + completion.complete() + } + + override fun resumeWithException(exception: Throwable) { + completion.completeExceptionally(exception) + } +} + +/** + * Converts this deferred value to a [AsyncCompletion]. + * The deferred value is cancelled when the returned [AsyncCompletion] is cancelled or otherwise completed. + */ +fun Deferred.asAsyncCompletion(): AsyncCompletion { + val asyncCompletion = AsyncCompletion.incomplete() + asyncCompletion.whenComplete { exception -> cancel(exception) } + 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. + */ +fun Job.asAsyncCompletion(): AsyncCompletion { + val asyncCompletion = AsyncCompletion.incomplete() + asyncCompletion.whenComplete { exception -> cancel(exception) } + 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. + */ +fun AsyncCompletion.asDeferred(): Deferred { + // 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().also { it.completeExceptionally(original) } + } + } + val result = CompletableDeferred() + 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 -> + val consumer = ContinuationConsumer(cont) + whenComplete(consumer) + cont.invokeOnCancellation { + consumer.cont = null // shall clear reference to continuation + } + } +} + +private class ContinuationConsumer( + @Volatile @JvmField var cont: Continuation? +) : Consumer { + 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 + cont.resumeWithException(exception) + } +} diff --git a/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncResult.kt b/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncResult.kt new file mode 100644 index 00000000..b3064ec8 --- /dev/null +++ b/concurrent/src/main/kotlin/net/consensys/cava/concurrent/coroutines/experimental/AsyncResult.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.experimental + +import kotlinx.coroutines.experimental.CancellableContinuation +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.experimental.CoroutineDispatcher +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.CoroutineStart +import kotlinx.coroutines.experimental.DefaultDispatcher +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.newCoroutineContext +import kotlinx.coroutines.experimental.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.coroutines.experimental.Continuation +import kotlin.coroutines.experimental.ContinuationInterceptor +import kotlin.coroutines.experimental.CoroutineContext + +/** + * Starts new coroutine and returns its result as an [AsyncResult]. + * + * This coroutine builder uses [CommonPool] context by default and is conceptually similar to + * [AsyncResult.executeBlocking]. + * + * The running coroutine is cancelled when the [AsyncResult] is cancelled or otherwise completed. + * + * The [context] for the new coroutine can be explicitly specified. See [CoroutineDispatcher] for the standard context + * implementations that are provided by `kotlinx.coroutines`. The [context][CoroutineScope.coroutineContext] of the + * parent coroutine from its [scope][CoroutineScope] may be used, in which case the [Job] of the resulting coroutine is + * a child of the job of the parent coroutine. The parent job may be also explicitly specified using [parent] + * parameter. + * + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [DefaultDispatcher] is + * used. + * + * By default, the coroutine 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] + * does not provide the corresponding capability) and produces [IllegalArgumentException]. + * + * See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine. + * + * @param context context of the coroutine. The default value is [DefaultDispatcher]. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param parent explicitly specifies the parent job, overrides job from the [context] (if any). + * @param block the coroutine code. + */ +fun asyncResult( + context: CoroutineContext = DefaultDispatcher, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + block: suspend CoroutineScope.() -> T +): AsyncResult { + require(!start.isLazy) { "$start start is not supported" } + val newContext = newCoroutineContext(context, parent) + val job = Job(newContext[Job]) + val coroutine = AsyncResultCoroutine(newContext + job) + job.invokeOnCompletion { coroutine.result.cancel() } + coroutine.result.whenComplete { _, exception -> job.cancel(exception) } + start(block, receiver = coroutine, completion = coroutine) // use the specified start strategy + return coroutine.result +} + +private class AsyncResultCoroutine( + override val context: CoroutineContext, + val result: CompletableAsyncResult = AsyncResult.incomplete() +) : Continuation, CoroutineScope { + override val coroutineContext: CoroutineContext get() = context + override val isActive: Boolean get() = context[Job]!!.isActive + override fun resume(value: T) { + result.complete(value) + } + + override fun resumeWithException(exception: Throwable) { + result.completeExceptionally(exception) + } +} + +/** + * Converts this deferred value to an [AsyncResult]. + * The deferred value is cancelled when the returned [AsyncResult] is cancelled or otherwise completed. + */ +fun Deferred.asAsyncResult(): AsyncResult { + val asyncResult = AsyncResult.incomplete() + asyncResult.whenComplete { _, exception -> cancel(exception) } + 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. + */ +fun AsyncResult.asDeferred(): Deferred { + // Fast path if already completed + if (isDone) { + return try { + CompletableDeferred(get()) + } catch (e: Throwable) { + // unwrap original cause from CompletionException + val original = (e as? CompletionException)?.cause ?: e + CompletableDeferred().also { it.completeExceptionally(original) } + } + } + val result = CompletableDeferred() + 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 AsyncResult.await(): T { + // fast path when CompletableFuture is already done (does not suspend) + if (isDone) { + try { + return get() + } catch (e: CompletionException) { + throw e.cause ?: e // unwrap original cause from CompletionException + } + } + // slow path -- suspend + return suspendCancellableCoroutine { cont: CancellableContinuation -> + val consumer = ContinuationBiConsumer(cont) + whenComplete(consumer) + cont.invokeOnCancellation { + consumer.cont = null // shall clear reference to continuation + } + } +} + +private class ContinuationBiConsumer( + @Volatile @JvmField var cont: Continuation? +) : BiConsumer { + 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!!) + else // the future has completed with an exception + cont.resumeWithException(exception) + } +} diff --git a/concurrent/src/test/java/net/consensys/cava/concurrent/AtomicSlotMapTest.java b/concurrent/src/test/java/net/consensys/cava/concurrent/AtomicSlotMapTest.java new file mode 100644 index 00000000..e8eea79f --- /dev/null +++ b/concurrent/src/test/java/net/consensys/cava/concurrent/AtomicSlotMapTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.Test; + +class AtomicSlotMapTest { + + @Test + void shouldUseSlotsIncrementally() throws Exception { + AtomicSlotMap slotMap = AtomicSlotMap.positiveIntegerSlots(); + + assertEquals(1, (int) slotMap.add("value")); + assertEquals(2, (int) slotMap.add("value")); + assertEquals(3, (int) slotMap.add("value")); + assertEquals(4, (int) slotMap.add("value")); + } + + @Test + void shouldReuseSlotsIncrementally() throws Exception { + AtomicSlotMap slotMap = AtomicSlotMap.positiveIntegerSlots(); + + assertEquals(1, (int) slotMap.add("value")); + assertEquals(2, (int) slotMap.add("value")); + assertEquals(3, (int) slotMap.add("value")); + assertEquals(4, (int) slotMap.add("value")); + slotMap.remove(2); + slotMap.remove(4); + assertEquals(2, (int) slotMap.add("value")); + assertEquals(4, (int) slotMap.add("value")); + } + + @Test + void shouldNotDuplicateSlotsWhileAddingAndRemoving() throws Exception { + AtomicSlotMap slotMap = AtomicSlotMap.positiveIntegerSlots(); + Set fastSlots = ConcurrentHashMap.newKeySet(); + Set slowSlots = ConcurrentHashMap.newKeySet(); + + Callable fastAdders = () -> { + int slot = slotMap.add("a fast value"); + fastSlots.add(slot); + return null; + }; + + Callable slowAdders = () -> { + CompletableAsyncResult result = AsyncResult.incomplete(); + slotMap.computeAsync(s -> result).thenAccept(slowSlots::add); + + Thread.sleep(10); + result.complete("a slow value"); + return null; + }; + + Callable addAndRemovers = () -> { + int slot = slotMap.add("a value"); + Thread.sleep(5); + slotMap.remove(slot); + return null; + }; + + ExecutorService fastPool = Executors.newFixedThreadPool(20); + ExecutorService slowPool = Executors.newFixedThreadPool(20); + ExecutorService addAndRemovePool = Executors.newFixedThreadPool(40); + List> fastFutures = fastPool.invokeAll(Collections.nCopies(1000, fastAdders)); + List> slowFutures = slowPool.invokeAll(Collections.nCopies(1000, slowAdders)); + List> addAndRemoveFutures = addAndRemovePool.invokeAll(Collections.nCopies(2000, addAndRemovers)); + + for (Future future : addAndRemoveFutures) { + future.get(); + } + for (Future future : slowFutures) { + future.get(); + } + for (Future future : fastFutures) { + future.get(); + } + + assertEquals(1000, fastSlots.size()); + assertEquals(1000, slowSlots.size()); + slowSlots.addAll(fastSlots); + assertEquals(2000, slowSlots.size()); + + assertEquals(2000, slotMap.size()); + } +} diff --git a/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletionTest.java b/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletionTest.java new file mode 100644 index 00000000..afc88db3 --- /dev/null +++ b/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncCompletionTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +class DefaultCompletableAsyncCompletionTest { + + @Test + void shouldReturnValueFromCompletedResult() { + AsyncCompletion completion = AsyncCompletion.completed(); + assertThat(completion.isDone()).isTrue(); + } + + @Test + void shouldReturnExceptionFromExceptionallyCompletedResult() throws Exception { + Exception exception = new RuntimeException(); + AsyncCompletion completion = AsyncCompletion.exceptional(exception); + assertThat(completion.isDone()).isTrue(); + assertCompletedWithException(completion, exception); + } + + @Test + void isNotDoneUntilCompleted() { + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + assertThat(completion.isDone()).isFalse(); + completion.complete(); + assertThat(completion.isDone()).isTrue(); + } + + @Test + void invokesContinuationFunctionWhenCompleted() throws Exception { + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + AsyncResult asyncResult = completion.then(() -> AsyncResult.completed("Completed")); + assertThat(asyncResult.isDone()).isFalse(); + completion.complete(); + assertThat(asyncResult.isDone()).isTrue(); + assertThat(asyncResult.get()).isEqualTo("Completed"); + } + + @Test + void completesExceptionallyWhenContinuationResultCompletesExceptionally() throws Exception { + Exception exception = new RuntimeException(); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + AsyncResult asyncResult = completion.then(() -> AsyncResult.exceptional(exception)); + assertThat(asyncResult.isDone()).isFalse(); + completion.complete(); + assertThat(asyncResult.isDone()).isTrue(); + assertCompletedWithException(asyncResult, exception); + } + + @Test + void completesExceptionallyWhenContinuationFunctionThrows() throws Exception { + RuntimeException exception = new RuntimeException(); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + AsyncResult asyncResult = completion.then(() -> { + throw exception; + }); + assertThat(asyncResult.isDone()).isFalse(); + completion.complete(); + assertThat(asyncResult.isDone()).isTrue(); + assertCompletedWithException(asyncResult, exception); + } + + @Test + void doesntInvokeContinuationFunctionIfCompletingExceptionally() throws Exception { + RuntimeException exception = new RuntimeException(); + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + AsyncResult asyncResult = completion.then(() -> { + fail("should not be invoked"); + throw new RuntimeException(); + }); + assertThat(asyncResult.isDone()).isFalse(); + completion.completeExceptionally(exception); + assertThat(asyncResult.isDone()).isTrue(); + assertCompletedWithException(asyncResult, exception); + } + + @Test + void completesWhenComposedCompletionCompletes() { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AtomicBoolean composed = new AtomicBoolean(false); + AsyncCompletion result = completion1.thenCompose(() -> { + composed.set(true); + return completion2; + }); + assertThat(result.isDone()).isFalse(); + assertThat(composed.get()).isFalse(); + + completion1.complete(); + assertThat(result.isDone()).isFalse(); + assertThat(composed.get()).isTrue(); + + completion2.complete(); + assertThat(result.isDone()).isTrue(); + } + + @Test + void completesExceptionallyWhenComposedCompletionThrows() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + RuntimeException exception = new RuntimeException(); + + AsyncCompletion result = completion1.thenCompose(() -> { + throw exception; + }); + assertThat(result.isDone()).isFalse(); + + completion1.complete(); + assertCompletedWithException(result, exception); + } + + @Test + void completesExceptionallyWhenComposedCompletionCompletesExceptionally() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AtomicBoolean composed = new AtomicBoolean(false); + AsyncCompletion result = completion1.thenCompose(() -> { + composed.set(true); + return completion2; + }); + assertThat(result.isDone()).isFalse(); + assertThat(composed.get()).isFalse(); + + completion1.complete(); + assertThat(result.isDone()).isFalse(); + assertThat(composed.get()).isTrue(); + + RuntimeException exception = new RuntimeException(); + completion2.completeExceptionally(exception); + assertThat(result.isDone()).isTrue(); + assertCompletedWithException(result, exception); + } + + @Test + void completesExceptionallyWhenComposerCompletesExceptionally() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AtomicBoolean composed = new AtomicBoolean(false); + AsyncCompletion result = completion1.thenCompose(() -> { + composed.set(true); + return completion2; + }); + assertThat(result.isDone()).isFalse(); + assertThat(composed.get()).isFalse(); + + RuntimeException exception = new RuntimeException(); + completion1.completeExceptionally(exception); + assertThat(result.isDone()).isTrue(); + assertThat(composed.get()).isFalse(); + assertCompletedWithException(result, exception); + } + + @Test + void completesWhenCombinedCompletionCompletes() { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AsyncCompletion result = completion1.thenCombine(completion2); + assertThat(result.isDone()).isFalse(); + + completion1.complete(); + assertThat(result.isDone()).isFalse(); + + completion2.complete(); + assertThat(result.isDone()).isTrue(); + } + + @Test + void completesExceptionallyWhenCombinedCompletionCompletesExceptionally() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AsyncCompletion result = completion1.thenCombine(completion2); + assertThat(result.isDone()).isFalse(); + + completion1.complete(); + assertThat(result.isDone()).isFalse(); + + RuntimeException exception = new RuntimeException(); + completion2.completeExceptionally(exception); + assertThat(result.isDone()).isTrue(); + assertCompletedWithException(result, exception); + } + + @Test + void completesExceptionallyWhenCombinerCompletesExceptionally() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AsyncCompletion result = completion1.thenCombine(completion2); + assertThat(result.isDone()).isFalse(); + + RuntimeException exception = new RuntimeException(); + completion1.completeExceptionally(exception); + assertThat(result.isDone()).isTrue(); + assertCompletedWithException(result, exception); + } + + @Test + void completesWhenAllInCollectionComplete() { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + Collection list = Arrays.asList(completion1, completion2); + + AsyncCompletion completion = AsyncCompletion.allOf(list); + assertThat(completion.isDone()).isFalse(); + + completion1.complete(); + assertThat(completion.isDone()).isFalse(); + completion2.complete(); + assertThat(completion.isDone()).isTrue(); + } + + @Test + void completesWithExceptionWhenAnyInCollectionFail() throws Exception { + CompletableAsyncCompletion completion1 = AsyncCompletion.incomplete(); + CompletableAsyncCompletion completion2 = AsyncCompletion.incomplete(); + + AsyncCompletion completion = AsyncCompletion.allOf(completion1, completion2); + assertThat(completion.isDone()).isFalse(); + + Exception exception = new RuntimeException(); + completion1.completeExceptionally(exception); + assertThat(completion.isDone()).isFalse(); + + completion2.complete(); + assertThat(completion.isDone()).isTrue(); + assertCompletedWithException(completion, exception); + } + + @Test + void invokesComposedWhenCanceled() { + CompletableAsyncCompletion completion = AsyncCompletion.incomplete(); + + AtomicReference completedThrowable = new AtomicReference<>(); + AsyncCompletion downstreamCompletion = completion.whenComplete(completedThrowable::set); + + completion.cancel(); + assertThat(completion.isDone()).isTrue(); + assertThat(completion.isCancelled()).isTrue(); + assertThat(completion.isCompletedExceptionally()).isTrue(); + + assertThat(downstreamCompletion.isDone()).isTrue(); + assertThat(downstreamCompletion.isCancelled()).isFalse(); + assertThat(downstreamCompletion.isCompletedExceptionally()).isTrue(); + + assertThat(completedThrowable.get()).isInstanceOf(CancellationException.class); + } + + private void assertCompletedWithException(AsyncCompletion completion, Exception exception) throws Exception { + try { + completion.join(); + fail("Expected exception not thrown"); + } catch (CompletionException ex) { + assertThat(ex.getCause()).isSameAs(exception); + } + } + + private void assertCompletedWithException(AsyncResult asyncResult, Exception exception) throws Exception { + try { + asyncResult.get(); + fail("Expected exception not thrown"); + } catch (CompletionException ex) { + assertThat(ex.getCause()).isSameAs(exception); + } + } +} diff --git a/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResultTest.java b/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResultTest.java new file mode 100644 index 00000000..becdbee5 --- /dev/null +++ b/concurrent/src/test/java/net/consensys/cava/concurrent/DefaultCompletableAsyncResultTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +class DefaultCompletableAsyncResultTest { + + @Test + void shouldReturnValueFromCompletedResult() throws Exception { + AsyncResult asyncResult = AsyncResult.completed("Completed"); + assertThat(asyncResult.isDone()).isTrue(); + assertThat(asyncResult.get()).isEqualTo("Completed"); + } + + @Test + void shouldReturnExceptionFromExceptionallyCompletedResult() throws Exception { + Exception exception = new RuntimeException(); + AsyncResult asyncResult = AsyncResult.exceptional(exception); + assertThat(asyncResult.isDone()).isTrue(); + assertCompletedWithException(asyncResult, exception); + } + + @Test + void isNotDoneUntilCompleted() throws Exception { + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + assertThat(asyncResult.isDone()).isFalse(); + asyncResult.complete("Completed"); + assertThat(asyncResult.isDone()).isTrue(); + assertThat(asyncResult.get()).isEqualTo("Completed"); + } + + @Test + void suppliesAsyncResultWhenCompleted() throws Exception { + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + AsyncResult asyncResult2 = asyncResult.then(value -> { + assertThat(value).isEqualTo("Completed1"); + return AsyncResult.completed("Completed2"); + }); + assertThat(asyncResult2.isDone()).isFalse(); + asyncResult.complete("Completed1"); + assertThat(asyncResult2.isDone()).isTrue(); + assertThat(asyncResult2.get()).isEqualTo("Completed2"); + } + + @Test + void completesExceptionallyWhenSuppliedResultCompletesExceptionally() throws Exception { + Exception exception = new RuntimeException(); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + AsyncResult asyncResult2 = asyncResult.then(value -> AsyncResult.exceptional(exception)); + assertThat(asyncResult2.isDone()).isFalse(); + asyncResult.complete("Complete"); + assertThat(asyncResult2.isDone()).isTrue(); + assertCompletedWithException(asyncResult2, exception); + } + + @Test + void completesExceptionallyWhenSupplierThrows() throws Exception { + RuntimeException exception = new RuntimeException(); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + AsyncResult asyncResult2 = asyncResult.then(value -> { + throw exception; + }); + assertThat(asyncResult2.isDone()).isFalse(); + asyncResult.complete("Complete"); + assertThat(asyncResult2.isDone()).isTrue(); + assertCompletedWithException(asyncResult2, exception); + } + + @Test + void doesntInvokeSupplierIfCompletingExceptionally() throws Exception { + RuntimeException exception = new RuntimeException(); + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + AsyncResult asyncResult2 = asyncResult.then(value -> { + fail("should not be invoked"); + throw new RuntimeException(); + }); + assertThat(asyncResult2.isDone()).isFalse(); + asyncResult.completeExceptionally(exception); + assertThat(asyncResult2.isDone()).isTrue(); + assertCompletedWithException(asyncResult2, exception); + } + + @Test + void completesWhenAllInCollectionComplete() { + CompletableAsyncResult asyncResult1 = AsyncResult.incomplete(); + CompletableAsyncResult asyncResult2 = AsyncResult.incomplete(); + Collection> list = Arrays.asList(asyncResult1, asyncResult2); + + AsyncCompletion completion = AsyncResult.allOf(list); + assertThat(completion.isDone()).isFalse(); + + asyncResult1.complete("one"); + assertThat(completion.isDone()).isFalse(); + asyncResult2.complete("two"); + assertThat(completion.isDone()).isTrue(); + } + + @Test + void completesWithExceptionWhenAnyInCollectionFail() throws Exception { + CompletableAsyncResult asyncResult1 = AsyncResult.incomplete(); + CompletableAsyncResult asyncResult2 = AsyncResult.incomplete(); + + AsyncCompletion completion = AsyncResult.allOf(asyncResult1, asyncResult2); + assertThat(completion.isDone()).isFalse(); + + Exception exception = new RuntimeException(); + asyncResult1.completeExceptionally(exception); + assertThat(completion.isDone()).isFalse(); + + asyncResult2.complete(2); + assertThat(completion.isDone()).isTrue(); + assertCompletedWithException(completion, exception); + } + + @Test + void completesWhenCombinedComplete() throws Exception { + CompletableAsyncResult asyncResult1 = AsyncResult.incomplete(); + CompletableAsyncResult asyncResult2 = AsyncResult.incomplete(); + Collection> list = Arrays.asList(asyncResult1, asyncResult2); + + AsyncResult> result = AsyncResult.combine(list); + assertThat(result.isDone()).isFalse(); + + asyncResult1.complete("one"); + assertThat(result.isDone()).isFalse(); + asyncResult2.complete("two"); + assertThat(result.isDone()).isTrue(); + + List strings = result.get(); + assertThat(strings).isEqualTo(Arrays.asList("one", "two")); + } + + @Test + void invokesComposedWhenCanceled() { + CompletableAsyncResult asyncResult = AsyncResult.incomplete(); + + AtomicReference completedThrowable = new AtomicReference<>(); + AsyncResult downstreamAsyncResult = + asyncResult.whenComplete((result, throwable) -> completedThrowable.set(throwable)); + + asyncResult.cancel(); + assertThat(asyncResult.isDone()).isTrue(); + assertThat(asyncResult.isCancelled()).isTrue(); + assertThat(asyncResult.isCompletedExceptionally()).isTrue(); + + assertThat(downstreamAsyncResult.isDone()).isTrue(); + assertThat(downstreamAsyncResult.isCancelled()).isFalse(); + assertThat(downstreamAsyncResult.isCompletedExceptionally()).isTrue(); + + assertThat(completedThrowable.get()).isInstanceOf(CancellationException.class); + } + + private void assertCompletedWithException(AsyncResult asyncResult, Exception exception) throws Exception { + try { + asyncResult.get(); + fail("Expected exception not thrown"); + } catch (CompletionException ex) { + assertThat(ex.getCause()).isSameAs(exception); + } + } + + private void assertCompletedWithException(AsyncCompletion completion, Exception exception) throws Exception { + try { + completion.join(); + fail("Expected exception not thrown"); + } catch (CompletionException ex) { + assertThat(ex.getCause()).isSameAs(exception); + } + } +} diff --git a/concurrent/src/test/java/net/consensys/cava/concurrent/ExpiringMapTest.java b/concurrent/src/test/java/net/consensys/cava/concurrent/ExpiringMapTest.java new file mode 100644 index 00000000..f4fcebc3 --- /dev/null +++ b/concurrent/src/test/java/net/consensys/cava/concurrent/ExpiringMapTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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 org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ExpiringMapTest { + + private Instant currentTime; + private ExpiringMap map; + + @BeforeEach + void setup() { + currentTime = Instant.now(); + map = new ExpiringMap<>(() -> currentTime.toEpochMilli()); + } + + @Test + void canAddAndRemoveWithoutExpiry() { + map.put(1, "foo"); + assertTrue(map.containsKey(1)); + assertTrue(map.containsValue("foo")); + assertEquals("foo", map.get(1)); + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); + + String removed = map.remove(1); + assertEquals("foo", removed); + assertFalse(map.containsKey(1)); + assertFalse(map.containsValue("foo")); + assertNull(map.get(1)); + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + + assertNull(map.remove(1)); + } + + @Test + void canAddAndRemoveWithExpiry() { + map.put(1, "foo", currentTime.plusMillis(1).toEpochMilli()); + assertTrue(map.containsKey(1)); + assertTrue(map.containsValue("foo")); + assertEquals("foo", map.get(1)); + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); + + String removed = map.remove(1); + assertEquals("foo", removed); + assertFalse(map.containsKey(1)); + assertFalse(map.containsValue("foo")); + assertNull(map.get(1)); + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + + assertNull(map.remove(1)); + } + + @Test + void itemIsExpiredAfterExpiry() { + Instant futureTime = Instant.now().plusSeconds(10); + map.put(1, "foo", futureTime.toEpochMilli()); + assertTrue(map.containsKey(1)); + assertEquals("foo", map.get(1)); + currentTime = futureTime; + assertFalse(map.containsKey(1)); + } + + @Test + void itemIsMissingAfterExpiry() { + Instant futureTime = Instant.now().plusSeconds(10); + map.put(1, "foo", futureTime.toEpochMilli()); + assertTrue(map.containsKey(1)); + assertEquals("foo", map.get(1)); + currentTime = futureTime; + assertNull(map.get(1)); + } + + @Test + void addingExpiredItemRemovesExisting() { + map.put(1, "foo"); + String prev = map.put(1, "bar", 0); + assertEquals("foo", prev); + assertFalse(map.containsKey(1)); + } + + @Test + void doesNotExpireItemThatWasReplaced() { + Instant futureTime = Instant.now().plusSeconds(10); + map.put(1, "foo", futureTime.toEpochMilli()); + map.put(1, "bar", futureTime.plusSeconds(1).toEpochMilli()); + currentTime = futureTime; + assertTrue(map.containsKey(1)); + assertEquals("bar", map.get(1)); + } +} diff --git a/config/build.gradle b/config/build.gradle new file mode 100644 index 00000000..6755100e --- /dev/null +++ b/config/build.gradle @@ -0,0 +1,11 @@ +description = 'Classes and utilities for working with configuration.' + +dependencies { + compile project(':toml') + compile 'com.google.guava:guava' + + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/config/src/main/java/net/consensys/cava/config/Configuration.java b/config/src/main/java/net/consensys/cava/config/Configuration.java new file mode 100644 index 00000000..3c161e99 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/Configuration.java @@ -0,0 +1,354 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.toml.Toml.canonicalDottedKey; + +import net.consensys.cava.toml.Toml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Represents collection of configuration properties, optionally validated against a schema. + */ +public interface Configuration { + + /** + * Read a configuration from a TOML-formatted string. + * + * @param toml A TOML-formatted string. + * @return A Configuration loaded from the TOML file. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(String toml) throws IOException { + return fromToml(toml, null); + } + + /** + * Read a configuration from a TOML-formatted string, associated with a validation schema. + * + * @param toml A TOML-formatted string. + * @param schema The validation schema for the configuration. + * @return A Configuration loaded from the TOML file. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(String toml, Schema schema) throws IOException { + requireNonNull(toml); + return new TomlBackedConfiguration(Toml.parse(toml), schema); + } + + /** + * Loads a configuration from a TOML-formatted file. + * + * @param file The path of the TOML-formatted configuration file. + * @return A Configuration loaded from the TOML file. + * @throws NoSuchFileException If the file could not be found. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(Path file) throws IOException { + return fromToml(file, null); + } + + /** + * Loads a configuration from a file, associated with a validation schema. + * + * @param file The path of the TOML-formatted configuration file. + * @param schema The validation schema for the configuration. + * @return A Configuration loaded from the TOML file. + * @throws NoSuchFileException If the file could not be found. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(Path file, Schema schema) throws IOException { + requireNonNull(file); + return new TomlBackedConfiguration(Toml.parse(file), schema); + } + + /** + * Loads a configuration from a TOML-formatted file. + * + * @param is An input stream providing TOML-formatted configuration. + * @return A Configuration loaded from the TOML file. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(InputStream is) throws IOException { + return fromToml(is, null); + } + + /** + * Loads a configuration from a file, associated with a validation schema. + * + * @param is An input stream providing TOML-formatted configuration. + * @param schema The validation schema for the configuration. + * @return A Configuration loaded from the TOML file. + * @throws IOException If an IO error occurs. + */ + static Configuration fromToml(InputStream is, Schema schema) throws IOException { + requireNonNull(is); + return new TomlBackedConfiguration(Toml.parse(is), schema); + } + + /** + * @return true if the TOML document contained errors. + */ + default boolean hasErrors() { + return !(errors().isEmpty()); + } + + /** + * The errors that occurred during parsing. + * + * @return A list of errors. + */ + List errors(); + + /** + * Get a TOML-formatted representation of this configuration. + * + * @return A TOML-formatted representation. + */ + default String toToml() { + StringBuilder builder = new StringBuilder(); + try { + toToml(builder); + } catch (IOException e) { + // Not reachable + throw new UncheckedIOException(e); + } + return builder.toString(); + } + + /** + * Save a configuration to a TOML-formatted file. + * + *

+ * If necessary, parent directories for the output file will be created. + * + * @param path The file path to write the TOML-formatted output to. + * @throws IOException If the file cannot be written. + */ + default void toToml(Path path) throws IOException { + Files.createDirectories(path.getParent()); + try (Writer writer = Files.newBufferedWriter(path)) { + toToml(writer); + } + } + + /** + * Writes a configuration in TOML format. + * + * @param appendable The output to write to. + * @throws IOException If the file cannot be written. + */ + void toToml(Appendable appendable) throws IOException; + + /** + * The keys of all entries present in this configuration. + * + * @return The keys of all entries in this configuration. + */ + Set keySet(); + + /** + * Check if a key is set in this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return true if the entry is present in this configuration. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + boolean contains(String key); + + /** + * Get an object from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return The value, or null if no value was set in the configuration. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + @Nullable + Object get(String key); + + /** + * Get the position where a key is defined in the TOML document. + * + * @param key A configuration key (e.g. {@code "server.address.port"}). + * @return The input position, or null if the key was not set in this configuration. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + @Nullable + DocumentPosition inputPositionOf(String key); + + /** + * Get a string from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a string. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + String getString(String key); + + /** + * Get an integer from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.port"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not an integer. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + int getInteger(String key); + + /** + * Get a long from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.port"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a long. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + long getLong(String key); + + /** + * Get a double from this configuration. + * + * @param key A configuration key (e.g. {@code "server.priority"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a double. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + double getDouble(String key); + + /** + * Get a boolean from this configuration. + * + * @param key A configuration key (e.g. {@code "server.active"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a boolean. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + boolean getBoolean(String key); + + /** + * Get a map from this configuration. + * + * @param key A configuration key (e.g. {@code "server.active"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a map. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + Map getMap(String key); + + /** + * Get a list from this configuration. + * + * @param key A configuration key (e.g. {@code "server.common_names"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getList(String key); + + /** + * Get a list of strings from this configuration. + * + * @param key A configuration key (e.g. {@code "server.common_names"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of strings. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getListOfString(String key); + + /** + * Get a list of integers from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.ports"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of integers. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getListOfInteger(String key); + + /** + * Get a list of longs from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.ports"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of longs. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getListOfLong(String key); + + /** + * Get a list of doubles from this configuration. + * + * @param key A configuration key (e.g. {@code "server.priorities"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of doubles. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getListOfDouble(String key); + + /** + * Get a list of booleans from this configuration. + * + * @param key A configuration key (e.g. {@code "server.flags"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of booleans. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List getListOfBoolean(String key); + + /** + * Get a list of maps from this configuration. + * + * @param key A configuration key (e.g. {@code "mainnet.servers"}). + * @return The value. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the value is not a list of maps. + * @throws NoConfigurationPropertyException If the key was not set in the configuration. + */ + List> getListOfMap(String key); + + /** + * Get the canonical form of a configuration key. + * + * @param key A configuration key (e.g. {@code "server.flags"}). + * @return The canonical form of the key. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + static String canonicalKey(String key) { + return canonicalDottedKey(key); + } +} diff --git a/config/src/main/java/net/consensys/cava/config/ConfigurationError.java b/config/src/main/java/net/consensys/cava/config/ConfigurationError.java new file mode 100644 index 00000000..41740316 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/ConfigurationError.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import javax.annotation.Nullable; + +/** + * Provides details regarding an error in the configuration. + */ +public final class ConfigurationError extends RuntimeException { + + @Nullable + private final DocumentPosition position; + + /** + * Construct an error. + * + * @param message The error message. + */ + public ConfigurationError(String message) { + this(null, message); + } + + /** + * Construct an error. + * + * @param message The error message. + * @param cause The cause of the error. + */ + public ConfigurationError(String message, @Nullable Throwable cause) { + this(null, message, cause); + } + + /** + * Construct an error. + * + * @param position The location of the error in the configuration document. + * @param message The error message. + */ + public ConfigurationError(@Nullable DocumentPosition position, String message) { + super(message); + this.position = position; + } + + /** + * Construct an error. + * + * @param position The location of the error in the configuration document. + * @param message The error message. + * @param cause The cause of the error. + */ + public ConfigurationError(@Nullable DocumentPosition position, String message, @Nullable Throwable cause) { + super(message, cause); + this.position = position; + } + + /** + * @return The position in the input where the error occurred, or null if no position information is + * available. + */ + @Nullable + public DocumentPosition position() { + return position; + } + + @Override + public String toString() { + if (position == null) { + return getMessage(); + } + return getMessage() + " (" + position + ")"; + } +} diff --git a/config/src/main/java/net/consensys/cava/config/ConfigurationErrors.java b/config/src/main/java/net/consensys/cava/config/ConfigurationErrors.java new file mode 100644 index 00000000..0a3bd1a0 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/ConfigurationErrors.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Factory methods for collections of {@link ConfigurationError}. + */ +public final class ConfigurationErrors { + private ConfigurationErrors() {} + + /** + * @return An empty list of errors. + */ + public static List noErrors() { + return Collections.emptyList(); + } + + /** + * Create a single error. + * + * @param message The error message. + * @return A list containing a single error. + */ + public static List singleError(String message) { + return Collections.singletonList(new ConfigurationError(message)); + } + + /** + * Create a single error. + * + * @param message The error message. + * @param cause The cause of the error. + * @return A list containing a single error. + */ + public static List singleError(String message, @Nullable Throwable cause) { + return Collections.singletonList(new ConfigurationError(message, cause)); + } + + /** + * Create a single error. + * + * @param position The location of the error in the configuration document. + * @param message The error message. + * @return A list containing a single error. + */ + public static List singleError(@Nullable DocumentPosition position, String message) { + return Collections.singletonList(new ConfigurationError(position, message)); + } + + /** + * Create a single error. + * + * @param position The location of the error in the configuration document. + * @param message The error message. + * @param cause The cause of the error. + * @return A list containing a single error. + */ + public static List singleError( + @Nullable DocumentPosition position, + String message, + @Nullable Throwable cause) { + return Collections.singletonList(new ConfigurationError(position, message, cause)); + } +} diff --git a/config/src/main/java/net/consensys/cava/config/ConfigurationValidator.java b/config/src/main/java/net/consensys/cava/config/ConfigurationValidator.java new file mode 100644 index 00000000..43f9b37e --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/ConfigurationValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import java.util.List; + +/** + * A validator for a configuration. + * + *

+ * Validators of this type are invoked during verification after all property validators. However, errors returned by + * property validators do not prevent this validator being evaluated, so properties of the configuration may be missing + * or invalid. + */ +public interface ConfigurationValidator { + + /** + * Validate a configuration. + * + * @param configuration The value associated with the configuration entry. + * @return A list of error messages. If no errors are found, an empty list should be returned. + */ + List validate(Configuration configuration); +} diff --git a/config/src/main/java/net/consensys/cava/config/DocumentPosition.java b/config/src/main/java/net/consensys/cava/config/DocumentPosition.java new file mode 100644 index 00000000..5aa68e6d --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/DocumentPosition.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + + +/** + * A position in an input document. + */ +public final class DocumentPosition { + private final int line; + private final int column; + + /** + * Create a position. + * + * @param line The line. + * @param column The column. + * @return A position. + */ + public static DocumentPosition positionAt(int line, int column) { + if (line < 1) { + throw new IllegalArgumentException("line must be >= 1"); + } + if (column < 1) { + throw new IllegalArgumentException("column must be >= 1"); + } + return new DocumentPosition(line, column); + } + + private DocumentPosition(int line, int column) { + this.line = line; + this.column = column; + } + + /** + * The line number. + * + *

+ * The first line of the document is line 1. + * + * @return The line number (1..). + */ + public int line() { + return line; + } + + /** + * The column number. + * + *

+ * The first column of the document is column 1. + * + * @return The column number (1..). + */ + public int column() { + return column; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof DocumentPosition)) { + return false; + } + DocumentPosition other = (DocumentPosition) obj; + return this.line == other.line && this.column == other.column; + } + + @Override + public int hashCode() { + return 31 * line + column; + } + + @Override + public String toString() { + return "line " + line + ", column " + column; + } +} diff --git a/config/src/main/java/net/consensys/cava/config/InvalidConfigurationPropertyTypeException.java b/config/src/main/java/net/consensys/cava/config/InvalidConfigurationPropertyTypeException.java new file mode 100644 index 00000000..081fbce1 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/InvalidConfigurationPropertyTypeException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import javax.annotation.Nullable; + +/** + * An exception thrown when an invalid type is encountered. + */ +public final class InvalidConfigurationPropertyTypeException extends RuntimeException { + + @Nullable + private final DocumentPosition position; + + InvalidConfigurationPropertyTypeException(@Nullable DocumentPosition position, String message) { + super(message); + this.position = position; + } + + InvalidConfigurationPropertyTypeException( + @Nullable DocumentPosition position, + String message, + @Nullable Throwable cause) { + super(message, cause); + this.position = position; + } + + /** + * @return The position of the property in the configuration document, or null if there is no position + * available. + */ + @Nullable + public DocumentPosition position() { + return position; + } + + @Override + public String toString() { + if (position == null) { + return getMessage(); + } + return getMessage() + " (" + position + ")"; + } +} diff --git a/config/src/main/java/net/consensys/cava/config/NoConfigurationPropertyException.java b/config/src/main/java/net/consensys/cava/config/NoConfigurationPropertyException.java new file mode 100644 index 00000000..503df2df --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/NoConfigurationPropertyException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +/** + * An exception thrown when a requested configuration property is not found. + * + *

+ * This exception can be avoided by using a schema that provides a default value or asserts that a value has been + * provided in the configuration. + */ +public final class NoConfigurationPropertyException extends RuntimeException { + + NoConfigurationPropertyException(String message) { + super(message); + } +} diff --git a/config/src/main/java/net/consensys/cava/config/PropertyValidator.java b/config/src/main/java/net/consensys/cava/config/PropertyValidator.java new file mode 100644 index 00000000..e6e36130 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/PropertyValidator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static net.consensys.cava.config.ConfigurationErrors.noErrors; +import static net.consensys.cava.config.ConfigurationErrors.singleError; + +import java.util.List; +import javax.annotation.Nullable; + +/** + * A validator associated with a specific configuration property. + */ +public interface PropertyValidator { + + /** + * A validator that ensures a property is present. + * + * @return A validator that ensures a property is present. + */ + static PropertyValidator isPresent() { + return PropertyValidators.IS_PRESENT; + } + + /** + * A validator that ensures a property is within a long integer range. + * + * @param from The lower bound of the range (inclusive). + * @param to The upper bound of the range (exclusive). + * @return A validator that ensures a property is within an integer range. + */ + static PropertyValidator inRange(long from, long to) { + return (key, position, value) -> { + if (value == null || value.longValue() < from || value.longValue() >= to) { + return singleError(position, "Value of property '" + key + "' is outside range [" + from + "," + to + ")"); + } + return noErrors(); + }; + } + + /** + * Validate a configuration property. + * + * @param key The configuration property key. + * @param position The position of the property in the input document, if supported. This should be used when + * constructing errors. + * @param value The value associated with the configuration entry. + * @return A list of errors. If no errors are found, an empty list should be returned. + */ + List validate(String key, @Nullable DocumentPosition position, @Nullable T value); +} diff --git a/config/src/main/java/net/consensys/cava/config/PropertyValidators.java b/config/src/main/java/net/consensys/cava/config/PropertyValidators.java new file mode 100644 index 00000000..157b8ada --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/PropertyValidators.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static net.consensys.cava.config.ConfigurationErrors.noErrors; +import static net.consensys.cava.config.ConfigurationErrors.singleError; + +final class PropertyValidators { + private PropertyValidators() {} + + static final PropertyValidator IS_PRESENT = (key, position, value) -> { + if (value == null) { + return singleError(position, "Required property '" + key + "' is missing"); + } + return noErrors(); + }; +} diff --git a/config/src/main/java/net/consensys/cava/config/Schema.java b/config/src/main/java/net/consensys/cava/config/Schema.java new file mode 100644 index 00000000..cd206fe3 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/Schema.java @@ -0,0 +1,410 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * A schema for a configuration, providing default values and validation rules. + */ +public final class Schema { + + static final Schema EMPTY = + new Schema(Collections.emptyMap(), Collections.emptyMap(), ArrayListMultimap.create(), Collections.emptyList()); + + private final Map propertyDescriptions; + private final Map propertyDefaults; + private final ListMultimap> propertyValidators; + private final List configurationValidators; + + Schema( + Map propertyDescriptions, + Map propertyDefaults, + ListMultimap> propertyValidators, + List configurationValidators) { + this.propertyDescriptions = propertyDescriptions; + this.propertyDefaults = propertyDefaults; + this.propertyValidators = propertyValidators; + this.configurationValidators = configurationValidators; + } + + /** + * The keys of all defaults provided by this schema. + * + * @return The keys for all defaults provided by this schema. + */ + public Set defaultsKeySet() { + return propertyDefaults.keySet(); + } + + /** + * Get the description for a key. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return A description associated with the key, or null if no description is available. + */ + @Nullable + public String description(String key) { + requireNonNull(key); + return propertyDescriptions.get(key); + } + + /** + * Check if a key has a default provided by this schema. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return true if this schema provides a default value for the key. + */ + public boolean hasDefault(String key) { + requireNonNull(key); + return propertyDefaults.containsKey(key); + } + + /** + * Get a default value from this configuration. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return The value, or null if no default was available. + */ + public Object getDefault(String key) { + requireNonNull(key); + return propertyDefaults.get(key); + } + + /** + * Get a default value from this configuration as a string. + * + * @param key A configuration key (e.g. {@code "server.address.hostname"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a string. + */ + @Nullable + public String getDefaultString(String key) { + requireNonNull(key); + return getTypedDefault(key, String.class, "string"); + } + + /** + * Get a default value from this configuration as a integer. + * + * @param key A configuration key (e.g. {@code "server.address.port"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not an integer. + */ + @Nullable + public Integer getDefaultInteger(String key) { + requireNonNull(key); + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof Integer) { + return (Integer) obj; + } + if (obj instanceof Long) { + Long longValue = (Long) obj; + if (longValue > Integer.MAX_VALUE) { + throw new InvalidConfigurationPropertyTypeException( + null, + "Value of property '" + key + "' is too large for an integer"); + } + return longValue.intValue(); + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not an integer"); + } + + /** + * Get a default value from this configuration as a long. + * + * @param key A configuration key (e.g. {@code "server.address.port"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a long. + */ + @Nullable + public Long getDefaultLong(String key) { + requireNonNull(key); + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof Long) { + return (Long) obj; + } + if (obj instanceof Integer) { + return ((Integer) obj).longValue(); + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a long"); + } + + /** + * Get a default value from this configuration as a double. + * + * @param key A configuration key (e.g. {@code "server.priority"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a double. + */ + @Nullable + public Double getDefaultDouble(String key) { + requireNonNull(key); + return getTypedDefault(key, Double.class, "double"); + } + + /** + * Get a default value from this configuration as a boolean. + * + * @param key A configuration key (e.g. {@code "server.active"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a boolean. + */ + @Nullable + public Boolean getDefaultBoolean(String key) { + requireNonNull(key); + return getTypedDefault(key, Boolean.class, "boolean"); + } + + /** + * Get a default value from this configuration as a map. + * + * @param key A configuration key (e.g. {@code "server.active"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a map. + */ + @SuppressWarnings("unchecked") + @Nullable + public Map getDefaultMap(String key) { + requireNonNull(key); + return getTypedDefault(key, Map.class, "map"); + } + + /** + * Get a default value from this configuration as a list. + * + * @param key A configuration key (e.g. {@code "server.common_names"}). + * @return The value, or null if no default was available. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list. + */ + @SuppressWarnings("unchecked") + @Nullable + public List getDefaultList(String key) { + requireNonNull(key); + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof List) { + return (List) obj; + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' is not a list"); + } + + /** + * Get a default value from this configuration as a list of strings. + * + * @param key A configuration key (e.g. {@code "server.common_names"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of strings. + */ + @Nullable + public List getDefaultListOfString(String key) { + requireNonNull(key); + return getListDefault(key, String.class, "strings"); + } + + /** + * Get a default value from this configuration as a list of integers. + * + * @param key A configuration key (e.g. {@code "server.address.ports"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of integers. + */ + @SuppressWarnings("unchecked") + @Nullable + public List getDefaultListOfInteger(String key) { + requireNonNull(key); + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof List) { + List list = (List) obj; + if (list.isEmpty() || list.get(0) instanceof Integer) { + return (List) list; + } + if (list.get(0) instanceof Long) { + return IntStream.range(0, list.size()).mapToObj(i -> { + Long longValue = (Long) list.get(i); + if (longValue == null) { + return null; + } + if (longValue > Integer.MAX_VALUE) { + throw new InvalidConfigurationPropertyTypeException( + null, + "Value of property '" + key + "', index " + i + ", is too large for an integer"); + } + return longValue.intValue(); + }).collect(Collectors.toList()); + } + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a list of integers"); + } + + /** + * Get a default value from this configuration as a list of longs. + * + * @param key A configuration key (e.g. {@code "server.address.ports"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of longs. + */ + @SuppressWarnings("unchecked") + @Nullable + public List getDefaultListOfLong(String key) { + requireNonNull(key); + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof List) { + List list = (List) obj; + if (list.isEmpty() || list.get(0) instanceof Long) { + return (List) list; + } + if (list.get(0) instanceof Integer) { + return ((List) list).stream().map(i -> { + if (i == null) { + return null; + } + return i.longValue(); + }).collect(Collectors.toList()); + } + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a list of longs"); + } + + /** + * Get a default value from this configuration as a list of doubles. + * + * @param key A configuration key (e.g. {@code "server.priorities"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of doubles. + */ + @Nullable + public List getDefaultListOfDouble(String key) { + requireNonNull(key); + return getListDefault(key, Double.class, "doubles"); + } + + /** + * Get a default value from this configuration as a list of booleans. + * + * @param key A configuration key (e.g. {@code "server.flags"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of booleans. + */ + @Nullable + public List getDefaultListOfBoolean(String key) { + requireNonNull(key); + return getListDefault(key, Boolean.class, "booleans"); + } + + /** + * Get a default value from this configuration as a list of maps. + * + * @param key A configuration key (e.g. {@code "mainnet.servers"}). + * @return The value, or null if no default was available. + * @throws InvalidConfigurationPropertyTypeException If the default value is not a list of maps. + */ + @Nullable + public List> getDefaultListOfMap(String key) { + requireNonNull(key); + return getListDefault(key, Map.class, "maps"); + } + + @Nullable + private T getTypedDefault(String key, Class clazz, String typeName) { + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (clazz.isInstance(obj)) { + return clazz.cast(obj); + } + throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a " + typeName); + } + + @SuppressWarnings("unchecked") + @Nullable + private List getListDefault(String key, Class listType, String typeName) { + Object obj = propertyDefaults.get(key); + if (obj == null) { + return null; + } + if (obj instanceof List) { + List list = (List) obj; + if (list.isEmpty() || listType.isInstance(list.get(0))) { + return (List) list; + } + } + throw new InvalidConfigurationPropertyTypeException( + null, + "Property at '" + key + "' was not a list of " + typeName); + } + + /** + * Validate a configuration against this schema. + * + *

+ * The validations are done incrementally as the stream is consumed. Use {@code .limit(...)} on the stream to control + * the maximum number of validation errors to receive. + * + * @param configuration The configuration to validate. + * @return A stream containing any errors encountered during validation. + */ + public Stream validate(Configuration configuration) { + requireNonNull(configuration); + + Stream propertyErrors = propertyValidators.entries().stream().flatMap(e -> { + String key = e.getKey(); + PropertyValidator validator = e.getValue(); + Object value = configuration.get(key); + DocumentPosition position = configuration.inputPositionOf(key); + List errors = validator.validate(key, position, value); + if (errors == null) { + return Stream.empty(); + } + return errors.stream(); + }); + + Stream configErrors = configurationValidators.stream().flatMap(v -> { + List errors = v.validate(configuration); + if (errors == null) { + return Stream.empty(); + } + return errors.stream(); + }); + + return Stream.concat(propertyErrors, configErrors); + } +} diff --git a/config/src/main/java/net/consensys/cava/config/SchemaBuilder.java b/config/src/main/java/net/consensys/cava/config/SchemaBuilder.java new file mode 100644 index 00000000..f46d0c9e --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/SchemaBuilder.java @@ -0,0 +1,673 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.config.Configuration.canonicalKey; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * This interface allows customers to determine a schema to associate with a configuration to validate the entries read + * from configuration files, and provide default values if no value is present in the configuration file. + */ +public final class SchemaBuilder { + + private final Map propertyDescriptions = new HashMap<>(); + private final Map propertyDefaults = new HashMap<>(); + private final ListMultimap> propertyValidators = ArrayListMultimap.create(); + private final List configurationValidators = new ArrayList<>(); + + /** + * Get a new builder for a schema. + * + * @return A new {@link SchemaBuilder}. + */ + public static SchemaBuilder create() { + return new SchemaBuilder(); + } + + SchemaBuilder() {} + + /** + * Provide documentation for a property. + * + *

+ * Invoking this method with the same key as a previous invocation will replace the description for that key. + * + * @param key The configuration property key. + * @param description The description to associate with the property. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder documentProperty(String key, String description) { + requireNonNull(key); + requireNonNull(description); + propertyDescriptions.put(canonicalKey(key), description); + return this; + } + + /** + * Provide a default value for a property. + * + *

+ * Invoking this method with the same key as a previous invocation will replace the default value for that key. + * + * @param key The configuration property key. + * @param value The default value for the property. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addDefault(String key, Object value) { + requireNonNull(key); + requireNonNull(value); + propertyDefaults.put(canonicalKey(key), value); + return this; + } + + /** + * Add a property validation to this schema. + * + *

+ * Multiple validators can be provided for the same key by invoking this method multiple times. + * + * @param key The configuration property key. + * @param validator A validator for the property. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder validateProperty(String key, PropertyValidator validator) { + requireNonNull(key); + requireNonNull(validator); + propertyValidators.put(canonicalKey(key), validator); + return this; + } + + /** + * Add a configuration validator to the schema. + * + *

+ * Multiple validators can be provided by invoking this method multiple times. + * + * @param validator A configuration validator. + * @return This builder. + */ + public SchemaBuilder validateConfiguration(ConfigurationValidator validator) { + requireNonNull(validator); + configurationValidators.add(validator); + return this; + } + + /** + * Add a string property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a string. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addString( + String key, + @Nullable String defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + requireNonNull(key); + return addScalar(String.class, "string", key, defaultValue, description, validator); + } + + /** + * Add an integer property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains an integer. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addInteger( + String key, + @Nullable Integer defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + requireNonNull(key); + if (defaultValue != null) { + addDefault(key, defaultValue); + } + if (description != null) { + documentProperty(key, description); + } + validateProperty(key, (canonicalKey, position, value) -> { + if (!(value == null || value instanceof Integer || value instanceof Long)) { + return Collections + .singletonList(new ConfigurationError(position, "Property at '" + canonicalKey + "' requires an integer")); + } + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + Integer intValue; + if (value instanceof Long) { + if (((Long) value) > Integer.MAX_VALUE) { + return Collections.singletonList( + new ConfigurationError(position, "Value of property '" + canonicalKey + "' is too large for an integer")); + } + intValue = ((Long) value).intValue(); + } else { + intValue = (Integer) value; + } + return validator.validate(canonicalKey, position, intValue); + }); + return this; + } + + /** + * Add a long property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a long. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addLong( + String key, + @Nullable Long defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + requireNonNull(key); + if (defaultValue != null) { + addDefault(key, defaultValue); + } + if (description != null) { + documentProperty(key, description); + } + validateProperty(key, (vkey, position, value) -> { + if (!(value == null || value instanceof Long || value instanceof Integer)) { + return Collections + .singletonList(new ConfigurationError(position, "Property at '" + vkey + "' requires a long")); + } + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + Long longValue; + if (value instanceof Integer) { + longValue = ((Integer) value).longValue(); + } else { + longValue = (Long) value; + } + return validator.validate(vkey, position, longValue); + }); + return this; + } + + /** + * Add a double property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a double. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addDouble( + String key, + @Nullable Double defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + requireNonNull(key); + return addScalar(Double.class, "double", key, defaultValue, description, validator); + } + + /** + * Add a boolean property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a boolean. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addBoolean( + String key, + @Nullable Boolean defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + requireNonNull(key); + return addScalar(Boolean.class, "boolean", key, defaultValue, description, validator); + } + + /** + * Add a list-of-strings property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of strings without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addListOfString( + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + requireNonNull(key); + return addList(String.class, "string", key, defaultValue, description, validator); + } + + /** + * Add a list-of-integers property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of integers without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + @SuppressWarnings("unchecked") + public SchemaBuilder addListOfInteger( + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + requireNonNull(key); + + if (defaultValue != null) { + if (defaultValue.stream().anyMatch(Objects::isNull)) { + throw new IllegalArgumentException("default value list contains null value(s)"); + } + addDefault(key, defaultValue); + } + + if (description != null) { + documentProperty(key, description); + } + + validateProperty(key, (vkey, position, value) -> { + if (value != null && !(value instanceof List)) { + return Collections + .singletonList(new ConfigurationError(position, "Property at '" + vkey + "' requires a list of integers")); + } + + boolean containsLong = false; + if (value != null) { + List objs = (List) value; + for (int i = 0; i < objs.size(); ++i) { + Object obj = objs.get(i); + if (obj == null) { + return Collections.singletonList( + new ConfigurationError(position, "Value of property '" + vkey + "', index " + i + ", is null")); + } + if (!(obj instanceof Integer) && !(obj instanceof Long)) { + return Collections.singletonList( + new ConfigurationError( + position, + "Value of property '" + vkey + "', index " + i + ", is not an integer")); + } + if (obj instanceof Long) { + containsLong = true; + if (((Long) obj) > Integer.MAX_VALUE) { + return Collections.singletonList( + new ConfigurationError( + position, + "Value of property '" + vkey + "', index " + i + ", is too large for an integer")); + } + } + } + } + + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + + if (!containsLong) { + return validator.validate(vkey, position, (List) value); + } else { + return validator.validate(vkey, position, ((List) value).stream().map(o -> { + if (o instanceof Integer) { + return (Integer) o; + } + assert (o instanceof Long); + return ((Long) o).intValue(); + }).collect(Collectors.toList())); + } + }); + + return this; + } + + /** + * Add a list-of-longs property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of longs without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + @SuppressWarnings("unchecked") + public SchemaBuilder addListOfLong( + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + requireNonNull(key); + + if (defaultValue != null) { + if (defaultValue.stream().anyMatch(Objects::isNull)) { + throw new IllegalArgumentException("default value list contains null value(s)"); + } + addDefault(key, defaultValue); + } + + if (description != null) { + documentProperty(key, description); + } + + validateProperty(key, (canonicalKey, position, value) -> { + if (value != null && !(value instanceof List)) { + return Collections.singletonList( + new ConfigurationError(position, "Property at '" + canonicalKey + "' requires a list of longs")); + } + + boolean containsInteger = false; + if (value != null) { + List objs = (List) value; + for (int i = 0; i < objs.size(); ++i) { + Object obj = objs.get(i); + if (obj == null) { + return Collections.singletonList( + new ConfigurationError(position, "Value of property '" + canonicalKey + "', index " + i + ", is null")); + } + + if (!(obj instanceof Long) && !(obj instanceof Integer)) { + return Collections.singletonList( + new ConfigurationError( + position, + "Value of property '" + canonicalKey + "', index " + i + ", is not a long")); + } + containsInteger |= (obj instanceof Integer); + } + } + + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + + if (!containsInteger) { + return validator.validate(canonicalKey, position, (List) value); + } else { + return validator.validate(canonicalKey, position, ((List) value).stream().map(o -> { + if (o instanceof Long) { + return (Long) o; + } + assert (o instanceof Integer); + return ((Integer) o).longValue(); + }).collect(Collectors.toList())); + } + }); + + return this; + } + + /** + * Add a list-of-doubles property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of doubles without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addListOfDouble( + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + requireNonNull(key); + return addList(Double.class, "double", key, defaultValue, description, validator); + } + + /** + * Add a list-of-booleans property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of booleans without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addListOfBoolean( + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + requireNonNull(key); + return addList(Boolean.class, "boolean", key, defaultValue, description, validator); + } + + /** + * Add a list-of-maps property to the schema. + * + *

+ * Even if no {@code validator} is provided, the schema will validate that the configuration property, if present, + * contains a list of maps without any null values. + * + *

+ * If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is + * present (i.e. it will not be provided a null value to validate). + * + * @param key The configuration property key. + * @param defaultValue A default value for the property or null if no default is provided. + * @param description The description to associate with this property, or null if no documentation is provided. + * @param validator A validator for the property, or null if no validator is provided. + * @return This builder. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + public SchemaBuilder addListOfMap( + String key, + @Nullable List> defaultValue, + @Nullable String description, + @Nullable PropertyValidator>> validator) { + requireNonNull(key); + return addList(Map.class, "map", key, defaultValue, description, validator); + } + + /** + * Return the {@link Schema} constructed by this builder. + * + * @return A {@link Schema}. + */ + public Schema toSchema() { + return new Schema(propertyDescriptions, propertyDefaults, propertyValidators, configurationValidators); + } + + private SchemaBuilder addScalar( + Class clazz, + String typeName, + String key, + @Nullable T defaultValue, + @Nullable String description, + @Nullable PropertyValidator validator) { + if (defaultValue != null) { + addDefault(key, defaultValue); + } + if (description != null) { + documentProperty(key, description); + } + validateProperty(key, (canonicalKey, position, value) -> { + if (value != null && !clazz.isInstance(value)) { + return Collections.singletonList( + new ConfigurationError(position, "Property at '" + canonicalKey + "' requires a " + typeName)); + } + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + return validator.validate(canonicalKey, position, clazz.cast(value)); + }); + return this; + } + + @SuppressWarnings("unchecked") + private SchemaBuilder addList( + Class innerClass, + String typeName, + String key, + @Nullable List defaultValue, + @Nullable String description, + @Nullable PropertyValidator> validator) { + + if (defaultValue != null) { + if (defaultValue.stream().anyMatch(Objects::isNull)) { + throw new IllegalArgumentException("default value list contains null value(s)"); + } + addDefault(key, defaultValue); + } + + if (description != null) { + documentProperty(key, description); + } + + validateProperty(key, (canonicalKey, position, value) -> { + if (value != null && !(value instanceof List)) { + return Collections.singletonList( + new ConfigurationError( + position, + "Property at '" + canonicalKey + "' requires a list of " + typeName + "s")); + } + + if (value != null) { + List objs = (List) value; + for (int i = 0; i < objs.size(); ++i) { + Object obj = objs.get(i); + if (obj == null) { + return Collections.singletonList( + new ConfigurationError(position, "Value of property '" + canonicalKey + "', index " + i + ", is null")); + } + if (!innerClass.isInstance(obj)) { + return Collections.singletonList( + new ConfigurationError( + position, + "Value of property '" + canonicalKey + "', index " + i + ", is not a " + typeName)); + } + } + } + + if (validator == null || (defaultValue != null && value == null)) { + return Collections.emptyList(); + } + return validator.validate(canonicalKey, position, (List) value); + }); + + return this; + } +} diff --git a/config/src/main/java/net/consensys/cava/config/TomlBackedConfiguration.java b/config/src/main/java/net/consensys/cava/config/TomlBackedConfiguration.java new file mode 100644 index 00000000..bb56b413 --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/TomlBackedConfiguration.java @@ -0,0 +1,302 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static net.consensys.cava.toml.Toml.joinKeyPath; +import static net.consensys.cava.toml.Toml.parseDottedKey; + +import net.consensys.cava.toml.TomlArray; +import net.consensys.cava.toml.TomlInvalidTypeException; +import net.consensys.cava.toml.TomlParseResult; +import net.consensys.cava.toml.TomlPosition; +import net.consensys.cava.toml.TomlTable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.annotation.Nullable; + +final class TomlBackedConfiguration implements Configuration { + + private final TomlTable toml; + private final Schema schema; + private final List errors; + + TomlBackedConfiguration(TomlParseResult toml, Schema schema) { + List errors = new ArrayList<>(); + toml.errors().forEach( + err -> errors.add(new ConfigurationError(documentPosition(err.position()), err.getMessage(), err))); + if (schema != null) { + schema.validate(new TomlBackedConfiguration(toml, null)).forEach(errors::add); + } else { + schema = Schema.EMPTY; + } + + this.toml = toml; + this.schema = schema; + this.errors = errors; + } + + @Override + public List errors() { + return errors; + } + + @Override + public void toToml(Appendable appendable) throws IOException { + new TomlSerializer(this, schema).writeTo(appendable); + } + + @Override + public Set keySet() { + Set keys = new HashSet<>(); + keys.addAll(toml.dottedKeySet()); + keys.addAll(schema.defaultsKeySet()); + return keys; + } + + @Override + public boolean contains(String key) { + return toml.contains(key) || schema.hasDefault(key); + } + + @Nullable + @Override + public Object get(String key) { + Object obj = toml.get(key); + if (obj != null) { + if (obj instanceof TomlArray) { + return deepToList((TomlArray) obj); + } + if (obj instanceof TomlTable) { + return deepToMap((TomlTable) obj); + } + return obj; + } + return schema.getDefault(key); + } + + @Nullable + @Override + public DocumentPosition inputPositionOf(String key) { + TomlPosition position = toml.inputPositionOf(key); + if (position == null) { + return null; + } + return documentPosition(position); + } + + private DocumentPosition inputPositionOf(List keyPath) { + TomlPosition position = toml.inputPositionOf(keyPath); + if (position == null) { + return null; + } + return documentPosition(position); + } + + @Override + public String getString(String key) { + return getValue(key, toml::getString, schema::getDefaultString); + } + + @Override + public int getInteger(String key) { + return getValue(key, keyPath -> { + Long longValue = toml.getLong(keyPath); + if (longValue != null && longValue > Integer.MAX_VALUE) { + throw new InvalidConfigurationPropertyTypeException( + inputPositionOf(keyPath), + "Value of property '" + joinKeyPath(keyPath) + "' is too large for an integer"); + } + return (longValue != null) ? longValue.intValue() : null; + }, schema::getDefaultInteger); + } + + @Override + public long getLong(String key) { + return getValue(key, toml::getLong, schema::getDefaultLong); + } + + @Override + public double getDouble(String key) { + return getValue(key, toml::getDouble, schema::getDefaultDouble); + } + + @Override + public boolean getBoolean(String key) { + return getValue(key, toml::getBoolean, schema::getDefaultBoolean); + } + + @Override + public Map getMap(String key) { + return getValue(key, keyPath -> { + TomlTable table = toml.getTable(keyPath); + if (table == null) { + return null; + } + return deepToMap(table); + }, schema::getDefaultMap); + } + + @Override + public List getList(String key) { + return getValue(key, keyPath -> { + TomlArray array = toml.getArray(keyPath); + if (array == null) { + return null; + } + return deepToList(array); + }, schema::getDefaultList); + } + + @Override + public List getListOfString(String key) { + return getList(key, "strings", TomlArray::containsStrings, schema::getDefaultListOfString); + } + + @Override + public List getListOfInteger(String key) { + return getValue(key, keyPath -> { + TomlArray array = toml.getArray(keyPath); + if (array == null) { + return null; + } + if (!array.containsLongs()) { + throw new InvalidConfigurationPropertyTypeException( + inputPositionOf(keyPath), + "List property '" + joinKeyPath(keyPath) + "' does not contain integers"); + } + @SuppressWarnings("unchecked") + List longList = (List) (List) array.toList(); + return IntStream.range(0, longList.size()).mapToObj(i -> { + Long value = longList.get(i); + if (value > Integer.MAX_VALUE) { + throw new InvalidConfigurationPropertyTypeException( + inputPositionOf(keyPath), + "Value of property '" + joinKeyPath(keyPath) + "', index " + i + ", is too large for an integer"); + } + return value.intValue(); + }).collect(Collectors.toList()); + }, schema::getDefaultListOfInteger); + } + + @Override + public List getListOfLong(String key) { + return getList(key, "longs", TomlArray::containsLongs, schema::getDefaultListOfLong); + } + + @Override + public List getListOfDouble(String key) { + return getList(key, "doubles", TomlArray::containsDoubles, schema::getDefaultListOfDouble); + } + + @Override + public List getListOfBoolean(String key) { + return getList(key, "booleans", TomlArray::containsBooleans, schema::getDefaultListOfBoolean); + } + + @Override + public List> getListOfMap(String key) { + return getValue(key, keyPath -> { + TomlArray array = toml.getArray(keyPath); + if (array == null) { + return null; + } + if (!array.containsTables()) { + throw new InvalidConfigurationPropertyTypeException( + inputPositionOf(keyPath), + "List property '" + joinKeyPath(keyPath) + "' does not contain maps"); + } + @SuppressWarnings("unchecked") + List> typedList = (List>) (List) deepToList(array); + return typedList; + }, schema::getDefaultListOfMap); + } + + private DocumentPosition documentPosition(TomlPosition position) { + return DocumentPosition.positionAt(position.line(), position.column()); + } + + private T getValue(String key, Function, T> tomlGet, Function defaultGet) { + List keyPath = parseDottedKey(key); + T value; + try { + value = tomlGet.apply(keyPath); + } catch (TomlInvalidTypeException e) { + throw new InvalidConfigurationPropertyTypeException(inputPositionOf(keyPath), e.getMessage(), e); + } + if (value != null) { + return value; + } + String canonicalPath = joinKeyPath(keyPath); + value = defaultGet.apply(canonicalPath); + if (value != null) { + return value; + } + throw new NoConfigurationPropertyException("No value for property '" + canonicalPath + "'"); + } + + private List getList( + String key, + String typeName, + Predicate tomlCheck, + Function> defaultGet) { + return getValue(key, keyPath -> { + TomlArray array = toml.getArray(keyPath); + if (array == null) { + return null; + } + if (!tomlCheck.test(array)) { + throw new InvalidConfigurationPropertyTypeException( + inputPositionOf(keyPath), + "List property '" + joinKeyPath(keyPath) + "' does not contain " + typeName); + } + @SuppressWarnings("unchecked") + List typedList = (List) array.toList(); + return typedList; + }, defaultGet); + } + + private static List deepToList(TomlArray array) { + return array.toList().stream().map(o -> { + if (o instanceof TomlArray) { + return deepToList((TomlArray) o); + } + if (o instanceof TomlTable) { + return deepToMap((TomlTable) o); + } + return o; + }).collect(Collectors.toList()); + } + + private static Map deepToMap(TomlTable table) { + return table.toMap().entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> { + Object o = e.getValue(); + if (o instanceof TomlArray) { + return deepToList((TomlArray) o); + } + if (o instanceof TomlTable) { + return deepToMap((TomlTable) o); + } + return o; + })); + } +} diff --git a/config/src/main/java/net/consensys/cava/config/TomlSerializer.java b/config/src/main/java/net/consensys/cava/config/TomlSerializer.java new file mode 100644 index 00000000..fd96fc1d --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/TomlSerializer.java @@ -0,0 +1,176 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static net.consensys.cava.config.Configuration.canonicalKey; + +import net.consensys.cava.toml.Toml; + +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +final class TomlSerializer { + + private final Configuration configuration; + private final Schema schema; + // tableKey -> configKey + private final ListMultimap tableMap; + // configKey -> leafName + private final Map keyMap; + + TomlSerializer(Configuration configuration, Schema schema) { + ListMultimap tableMap = ArrayListMultimap.create(); + Map keyMap = new HashMap<>(); + + configuration.keySet().forEach(configKey -> { + List keyPath; + try { + keyPath = Toml.parseDottedKey(configKey); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Configuration key '" + configKey + "' is not valid in TOML"); + } + String tableKey = Toml.joinKeyPath(keyPath.subList(0, keyPath.size() - 1)); + tableMap.put(tableKey, configKey); + keyMap.put(configKey, keyPath.get(keyPath.size() - 1)); + }); + + this.configuration = configuration; + this.schema = schema; + this.tableMap = tableMap; + this.keyMap = keyMap; + } + + void writeTo(Appendable appendable) throws IOException { + List tableKeys = tableMap.keySet().stream().sorted().collect(Collectors.toList()); + for (Iterator iterator = tableKeys.iterator(); iterator.hasNext();) { + String tableKey = iterator.next(); + if (!tableKey.isEmpty()) { + writeDocumentation(tableKey, appendable); + } + if (!tableKey.isEmpty()) { + appendable.append('['); + appendable.append(tableKey); + appendable.append(']'); + appendable.append(System.lineSeparator()); + } + + List configKeys = tableMap.get(tableKey); + configKeys.sort(Comparator.naturalOrder()); + for (String configKey : configKeys) { + String leafName = keyMap.get(configKey); + Object obj = configuration.get(configKey); + if (obj == null) { + throw new IllegalStateException("Configuration key '" + configKey + "' was unexpectedly null"); + } + Object defaultValue = schema.getDefault(configKey); + if (obj instanceof Integer) { + obj = ((Integer) obj).longValue(); + } + if (defaultValue instanceof Integer) { + defaultValue = ((Integer) defaultValue).longValue(); + } + + writeDocumentation(configKey, appendable); + + String leafKey = Toml.joinKeyPath(Collections.singletonList(leafName)); + if (defaultValue != null) { + appendable.append('#'); + appendable.append(leafKey); + appendable.append(" = "); + writeValue(defaultValue, appendable); + appendable.append(System.lineSeparator()); + } + if (!obj.equals(defaultValue)) { + appendable.append(leafKey); + appendable.append(" = "); + writeValue(obj, appendable); + appendable.append(System.lineSeparator()); + } + } + + if (iterator.hasNext()) { + appendable.append(System.lineSeparator()); + } + } + } + + private void writeValue(Object obj, Appendable appendable) throws IOException { + if (obj instanceof String) { + appendable.append('\"'); + appendable.append(Toml.tomlEscape((String) obj)); + appendable.append('\"'); + } else if (obj instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) obj; + writeList(list, appendable); + } else if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) obj; + writeMap(map, appendable); + } else { + appendable.append(obj.toString()); + } + } + + private void writeList(List list, Appendable appendable) throws IOException { + appendable.append('['); + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + Object obj = iterator.next(); + if (obj == null) { + throw new IllegalStateException("Unexpected null in list property"); + } + writeValue(obj, appendable); + if (iterator.hasNext()) { + appendable.append(", "); + } + } + appendable.append(']'); + } + + private void writeMap(Map map, Appendable appendable) throws IOException { + appendable.append('{'); + for (Iterator iterator = map.keySet().stream().sorted().iterator(); iterator.hasNext();) { + String key = iterator.next(); + Object obj = map.get(key); + if (obj == null) { + throw new IllegalStateException("Unexpected null in map property"); + } + + appendable.append(Toml.joinKeyPath(Collections.singletonList(key))); + appendable.append(" = "); + writeValue(obj, appendable); + if (iterator.hasNext()) { + appendable.append(", "); + } + } + appendable.append('}'); + } + + private void writeDocumentation(String key, Appendable appendable) throws IOException { + String description = schema.description(canonicalKey(key)); + if (description != null) { + appendable.append("## "); + appendable.append(description); + appendable.append(System.lineSeparator()); + } + } +} diff --git a/config/src/main/java/net/consensys/cava/config/package-info.java b/config/src/main/java/net/consensys/cava/config/package-info.java new file mode 100644 index 00000000..cde800ab --- /dev/null +++ b/config/src/main/java/net/consensys/cava/config/package-info.java @@ -0,0 +1,10 @@ +/** + * A general-purpose library for managing configuration data. + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-config' (cava-config.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.config; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/config/src/test/java/net/consensys/cava/config/SchemaBuilderTest.java b/config/src/test/java/net/consensys/cava/config/SchemaBuilderTest.java new file mode 100644 index 00000000..70249f7f --- /dev/null +++ b/config/src/test/java/net/consensys/cava/config/SchemaBuilderTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +class SchemaBuilderTest { + + @Test + void shouldThrowForDefaultListContainingNulls() { + SchemaBuilder schemaBuilder = new SchemaBuilder(); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder.addListOfString("strings", Arrays.asList("a", null, "b"), null, null)); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder.addListOfInteger("ints", Arrays.asList(null, 1, 2), null, null)); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder.addListOfLong("longs", Arrays.asList(1L, 2L, null), null, null)); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder.addListOfDouble("doubles", Arrays.asList(1.0, 2.0, 3.0, null), null, null)); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder.addListOfBoolean("bools", Arrays.asList(true, null, false), null, null)); + assertThrows( + IllegalArgumentException.class, + () -> schemaBuilder + .addListOfMap("maps", Arrays.asList(Collections.emptyMap(), null, Collections.emptyMap()), null, null)); + } + +} diff --git a/config/src/test/java/net/consensys/cava/config/TomlBackedConfigurationTest.java b/config/src/test/java/net/consensys/cava/config/TomlBackedConfigurationTest.java new file mode 100644 index 00000000..095020e1 --- /dev/null +++ b/config/src/test/java/net/consensys/cava/config/TomlBackedConfigurationTest.java @@ -0,0 +1,564 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.config; + +import static net.consensys.cava.config.ConfigurationErrors.noErrors; +import static net.consensys.cava.config.ConfigurationErrors.singleError; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +class TomlBackedConfigurationTest { + + @Test + void loadSimpleConfigFile() throws Exception { + // @formatter:off + Configuration config = Configuration.fromToml( + "foo = \"12\"\n" + + "bar = 13\n" + + "foobar = 156.34\n" + + "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n" + + "\n" + + "[boo]\n" + + "baz=[1,2,3]\n" + + "\n" + + "[amap]\n" + + "a=1\n" + + "b=2\n" + + "[deepmap]\n" + + "a=4\n" + + "[deepmap.deeper]\n" + + "b=5\n" + ); + // @formatter:on + assertFalse(config.hasErrors()); + assertEquals("12", config.getString("foo")); + assertEquals(13, config.getInteger("bar")); + assertEquals(13L, config.getLong("bar")); + assertEquals(156.34d, config.getDouble("foobar")); + assertEquals(Arrays.asList(1, 2, 3), config.getListOfInteger("boo.baz")); + assertEquals(Arrays.asList(1L, 2L, 3L), config.getListOfLong("boo.baz")); + assertEquals(Arrays.asList(1L, 2L, 3L), config.getList("boo.baz")); + + Map expectedMap = new HashMap<>(); + expectedMap.put("a", 1L); + expectedMap.put("b", 2L); + assertEquals(expectedMap, config.getMap("amap")); + + assertEquals(Collections.singletonMap("baz", Arrays.asList(1L, 2L, 3L)), config.getMap("boo")); + + Map expectedDeepMap = new HashMap<>(); + expectedDeepMap.put("a", 4L); + expectedDeepMap.put("deeper", Collections.singletonMap("b", 5L)); + assertEquals(expectedDeepMap, config.getMap("deepmap")); + + List> expectedList = Arrays.asList(expectedMap, Collections.singletonMap("a", "hello")); + assertEquals(expectedList, config.getListOfMap("amaps")); + } + + @Test + void testKeyPresent() throws Exception { + Configuration config = Configuration.fromToml("foo=\"12\"\nbar=\"13\"\n[baz]\nfoobar = 156.34"); + assertFalse(config.hasErrors()); + assertEquals(new HashSet<>(Arrays.asList("foo", "bar", "baz.foobar")), config.keySet()); + assertTrue(config.contains("foo")); + assertTrue(config.contains("bar")); + assertTrue(config.contains("baz.foobar")); + assertFalse(config.contains("example")); + } + + @Test + void testFindsValuesBasedOnCanonicalKey() throws Exception { + Configuration config = Configuration.fromToml("foo=12\nbar=\"13\"\n[baz]\nfoobar = 156.34"); + assertFalse(config.hasErrors()); + assertEquals(new HashSet<>(Arrays.asList("foo", "bar", "baz.foobar")), config.keySet()); + assertEquals(12, config.getInteger("foo")); + assertEquals(12, config.getInteger(" foo ")); + assertEquals(12, config.getInteger(" 'foo' ")); + assertEquals(12, config.getInteger(" \"foo\" ")); + assertTrue(config.contains("baz.foobar")); + assertTrue(config.contains("baz . foobar")); + assertTrue(config.contains("baz . 'foobar'")); + assertTrue(config.contains("\"baz\" . 'foobar'")); + } + + @Test + void throwsForMissingValue() throws Exception { + Configuration config = Configuration.fromToml("foo=12\nbar=\"13\"\n[baz]\nfoobar = 156.34"); + assertFalse(config.hasErrors()); + Exception e = assertThrows(NoConfigurationPropertyException.class, () -> config.getString("foo.blah")); + assertEquals("No value for property 'foo.blah'", e.getMessage()); + e = assertThrows(NoConfigurationPropertyException.class, () -> config.getInteger(" foobaz ")); + assertEquals("No value for property 'foobaz'", e.getMessage()); + } + + @Test + void throwsForInvalidType() throws Exception { + Configuration config = Configuration.fromToml( + "[foo]\n" + "bar=99\n" + "baz='buz'\n" + "biz=false\n" + "buz = [1,2,3]\n" + "sbuz = ['1', '2', '3']\n"); + assertFalse(config.hasErrors()); + + assertEquals(99, config.getInteger("foo.bar")); + InvalidConfigurationPropertyTypeException e = + assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getString("foo.bar")); + assertEquals("Value of 'foo.bar' is a integer", e.getMessage()); + assertEquals(DocumentPosition.positionAt(2, 1), e.position()); + + assertEquals("buz", config.getString("foo.baz")); + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getDouble("foo.baz")); + assertEquals("Value of 'foo.baz' is a string", e.getMessage()); + assertEquals(DocumentPosition.positionAt(3, 1), e.position()); + + assertFalse(config.getBoolean("foo.biz")); + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getLong("foo.biz")); + assertEquals("Value of 'foo.biz' is a boolean", e.getMessage()); + assertEquals(DocumentPosition.positionAt(4, 1), e.position()); + + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getLong("foo")); + assertEquals("Value of 'foo' is a table", e.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 1), e.position()); + + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfString("foo.buz")); + assertEquals("List property 'foo.buz' does not contain strings", e.getMessage()); + assertEquals(DocumentPosition.positionAt(5, 1), e.position()); + + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfInteger("foo.sbuz")); + assertEquals("List property 'foo.sbuz' does not contain integers", e.getMessage()); + assertEquals(DocumentPosition.positionAt(6, 1), e.position()); + + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfMap("foo.sbuz")); + assertEquals("List property 'foo.sbuz' does not contain maps", e.getMessage()); + assertEquals(DocumentPosition.positionAt(6, 1), e.position()); + } + + @Test + void throwsForImpossibleIntegerConversion() throws Exception { + // @formatter:off + Configuration config = Configuration.fromToml( + "[foo]\n" + + "\" bar\" = " + + (1L + Integer.MAX_VALUE) + + "\n" + + "buz = [1, 2, " + + (1L + Integer.MAX_VALUE) + + ", 4]\n" + ); + // @formatter:on + assertFalse(config.hasErrors()); + Exception e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getInteger("foo.' bar'")); + assertEquals("Value of property 'foo.\" bar\"' is too large for an integer", e.getMessage()); + + e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfInteger("foo.buz")); + assertEquals("Value of property 'foo.buz', index 2, is too large for an integer", e.getMessage()); + } + + @Test + void loadMissingFile() { + assertThrows(NoSuchFileException.class, () -> { + Configuration.fromToml(Paths.get("FileThatDoesntExist")); + }); + } + + @Test + void invalidTOMLFile() throws Exception { + Configuration config = Configuration.fromToml("foo=\"12\"\nfoobar = \"156.34"); + assertTrue(config.hasErrors()); + assertEquals("Unexpected end of input, expected \" or a character", config.errors().get(0).getMessage()); + assertEquals(DocumentPosition.positionAt(2, 17), config.errors().get(0).position()); + assertEquals("12", config.getString("foo")); + } + + @Test + void getDefaultValue() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("foo", "goodbye", null, null); + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml("foobar = 'hello'", schema); + assertEquals("hello", config.getString("foobar")); + assertTrue(config.contains("foo")); + assertEquals("goodbye", config.getString("foo")); + } + + @Test + void keysContainSchemaKeys() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("foo", "bar", null, null); + builder.addString("foo.bar", "buz", null, null); + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml("foobar = 'hello'", schema); + assertEquals(new HashSet<>(Arrays.asList("foo", "foo.bar", "foobar")), config.keySet()); + } + + @Test + void validateConfiguration() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.validateConfiguration(config -> { + if (config.getInteger("expenses") > config.getInteger("revenue")) { + return singleError("Expenses cannot be larger than revenue"); + } + return noErrors(); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml("expenses = 2000\nrevenue = 1500\n", schema); + assertTrue(config.hasErrors()); + assertEquals("Expenses cannot be larger than revenue", config.errors().get(0).getMessage()); + } + + @Test + void validateConfigurationProperty() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("foo", "foobar", null, (key, position, value) -> { + if ("bar".equals(value)) { + return singleError(position, "No bar allowed"); + } + return noErrors(); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = \"bar\"", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("No bar allowed", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void validateIntegerProperty() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addInteger("foo", null, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = " + (1L + Integer.MAX_VALUE) + "\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo' is too large for an integer", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void shouldValidateStringList() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addListOfString("foo", null, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = [1, 2]\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo', index 0, is not a string", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void shouldValidateIntegerList() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addListOfInteger("foo", null, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo', index 0, is not an integer", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void shouldValidateLongListAsIntegers() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addListOfInteger("foo", null, null, (key, position, value) -> { + assertEquals(Arrays.asList(1, 2, 3), value); + return singleError("should reach here"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = [1, 2, 3]\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("should reach here", error.getMessage()); + assertNull(error.position()); + } + + @Test + void shouldValidateWithinIntegerList() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addListOfInteger("foo", null, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = [1, 2, " + (1L + Integer.MAX_VALUE) + ", 3]\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo', index 2, is too large for an integer", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void shouldValidateLongList() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addListOfLong("foo", null, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo', index 0, is not a long", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + } + + @Test + void shouldValidateWithIsPresent() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("foo.bar", null, null, PropertyValidator.isPresent()); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Required property 'foo.bar' is missing", error.getMessage()); + assertNull(error.position()); + } + + @Test + void shouldValidateWithInRange() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addInteger("foo", null, null, PropertyValidator.inRange(0, 10)); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml(" foo = 15\n", schema); + assertTrue(config.hasErrors()); + ConfigurationError error = config.errors().get(0); + assertEquals("Value of property 'foo' is outside range [0,10)", error.getMessage()); + assertEquals(DocumentPosition.positionAt(1, 2), error.position()); + + Configuration config2 = Configuration.fromToml(" foo = 9\n", schema); + assertFalse(config2.hasErrors()); + } + + @Test + void validatorNotCalledWhenDefaultUsed() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("fooS", "hello", null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + builder.addInteger("fooI", 2, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + builder.addLong("fooL", 2L, null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + builder.addListOfString("fooLS", Collections.emptyList(), null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + builder.addListOfInteger("fooLI", Collections.emptyList(), null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + builder.addListOfLong("fooLL", Collections.emptyList(), null, (key, position, value) -> { + throw new AssertionFailedError("should not be reached"); + }); + + Schema schema = builder.toSchema(); + Configuration config = Configuration.fromToml("\n", schema); + assertFalse(config.hasErrors()); + } + + @Test + void writeConfigurationToToml() throws Exception { + // @formatter:off + Configuration config = Configuration.fromToml( + "foo = \"12\"\n" + + "bar = 13\n" + + "foobar = 156.34\n" + + "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n" + + "\n" + + "[boo]\n" + + "baz=[1,2,3]\n" + + "\n" + + "[amap]\n" + + "a=1\n" + + "b=2\n" + + "[deepmap]\n" + + "' a' = 4\n" + + "[deepmap.' deep']\n" + + "'' = 'emptykey'\n" + + "[deepmap.deeper]\n" + + "farewell='goodbye'\n" + ); + // @formatter:on + + // @formatter:off + String expected = + "amaps = [{a = 1, b = 2}, {a = \"hello\"}]\n" + + "bar = 13\n" + + "foo = \"12\"\n" + + "foobar = 156.34\n" + + "\n" + + "[amap]\n" + + "a = 1\n" + + "b = 2\n" + + "\n" + + "[boo]\n" + + "baz = [1, 2, 3]\n" + + "\n" + + "[deepmap]\n" + + "\" a\" = 4\n" + + "\n" + + "[deepmap.\" deep\"]\n" + + "\"\" = \"emptykey\"\n" + + "\n" + + "[deepmap.deeper]\n" + + "farewell = \"goodbye\"\n"; + // @formatter:on + assertEquals(expected, config.toToml()); + } + + + @Test + void writeConfigurationWithDocumentationToToml() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.documentProperty("deepmap", "This table will have sub-tables"); + builder.documentProperty("boo.baz", "A list of longs"); + builder.documentProperty("bar", "Lucky number 13"); + Schema schema = builder.toSchema(); + + // @formatter:off + Configuration config = Configuration.fromToml( + "foo = \"12\"\n" + + "bar = 13\n" + + "foobar = 156.34\n" + + "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n" + + "\n" + + "[boo]\n" + + "baz=[1,2,3]\n" + + "\n" + + "[amap]\n" + + "a=1\n" + + "b=2\n" + + "[deepmap]\n" + + "' a' = 4\n" + + "[deepmap.' deep']\n" + + "'' = 'emptykey'\n" + + "[deepmap.deeper]\n" + + "farewell='goodbye'\n", schema); + // @formatter:on + + // @formatter:off + String expected = + "amaps = [{a = 1, b = 2}, {a = \"hello\"}]\n" + + "## Lucky number 13\n" + + "bar = 13\n" + + "foo = \"12\"\n" + + "foobar = 156.34\n" + + "\n" + + "[amap]\n" + + "a = 1\n" + + "b = 2\n" + + "\n" + + "[boo]\n" + + "## A list of longs\n" + + "baz = [1, 2, 3]\n" + + "\n" + + "## This table will have sub-tables\n" + + "[deepmap]\n" + + "\" a\" = 4\n" + + "\n" + + "[deepmap.\" deep\"]\n" + + "\"\" = \"emptykey\"\n" + + "\n" + + "[deepmap.deeper]\n" + + "farewell = \"goodbye\"\n"; + // @formatter:on + assertEquals(expected, config.toToml()); + } + + @Test + void writeConfigurationToTomlIncludesDefaults() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("farewell", "goodbye", "a farewell", null); + builder.addInteger("xxx", 10, null, null); + builder.addInteger("zzz", 10, null, null); + Schema schema = builder.toSchema(); + + // @formatter:off + Configuration config = Configuration.fromToml( + "foo = \"12\"\n" + + "foobar = 156.34\n" + + "xxx = 10\n" + + "zzz = 5\n" + + "\n" + + "[boo]\n" + + "baz=[1,2,3]\n" + + "\n", schema); + // @formatter:on + + // @formatter:off + String expected = + "## a farewell\n" + + "#farewell = \"goodbye\"\n" + + "foo = \"12\"\n" + + "foobar = 156.34\n" + + "#xxx = 10\n" + + "#zzz = 10\n" + + "zzz = 5\n" + + "\n" + + "[boo]\n" + + "baz = [1, 2, 3]\n"; + // @formatter:on + assertEquals(expected, config.toToml()); + } + + @Test + void buildSchemaAndDumpToToml() throws Exception { + SchemaBuilder builder = SchemaBuilder.create(); + builder.addString("somekey", "somevalue", "Got milk", null); + builder.addBoolean("foo", false, "Toggle switch", null); + builder.addDouble("bar", 1.0, "Value of currency", null); + builder.addLong("'Here now'", 123L, "One two three", null); + builder.addString("'No defaults'", null, null, null); + Configuration config = Configuration.fromToml("", builder.toSchema()); + // @formatter:off + String expected = + "## One two three\n" + + "#\"Here now\" = 123\n" + + "## Value of currency\n" + + "#bar = 1.0\n" + + "## Toggle switch\n" + + "#foo = false\n" + + "## Got milk\n" + + "#somekey = \"somevalue\"\n"; + // @formatter:on + assertEquals(expected, config.toToml()); + } +} diff --git a/crypto/build.gradle b/crypto/build.gradle new file mode 100644 index 00000000..68263a88 --- /dev/null +++ b/crypto/build.gradle @@ -0,0 +1,20 @@ +description = 'Classes and utilities for working with cryptography.' + +javadoc { exclude '**/LibSodium*' } + +dependencies { + compile project(':bytes') + compile project(':io') + compile project(':units') + compile 'com.google.guava:guava' + compileOnly 'com.github.jnr:jnr-ffi' + compileOnly 'org.bouncycastle:bcprov-jdk15on' + + testCompile project(':junit') + testCompile 'com.github.jnr:jnr-ffi' + testCompile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/Hash.java b/crypto/src/main/java/net/consensys/cava/crypto/Hash.java new file mode 100644 index 00000000..6f501869 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/Hash.java @@ -0,0 +1,245 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Various utilities for providing hashes (digests) of arbitrary data. + * + * Requires the BouncyCastleProvider to be loaded and available. See + * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation for detail. + */ +public final class Hash { + private Hash() {} + + // SHA-2 + private static String SHA2_256 = "SHA-256"; + private static String SHA2_512_256 = "SHA-512/256"; + + // Keccak + private static String KECCAK_256 = "KECCAK-256"; + private static String KECCAK_512 = "KECCAK-512"; + + // SHA-3 + private static String SHA3_256 = "SHA3-256"; + private static String SHA3_512 = "SHA3-512"; + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + * @throws NoSuchAlgorithmException If no Provider supports a MessageDigestSpi implementation for the specified + * algorithm. + */ + public static byte[] digestUsingAlgorithm(byte[] input, String alg) throws NoSuchAlgorithmException { + requireNonNull(input); + requireNonNull(alg); + MessageDigest digest = MessageDigest.getInstance(alg); + digest.update(input); + return digest.digest(); + } + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + * @throws NoSuchAlgorithmException If no Provider supports a MessageDigestSpi implementation for the specified + * algorithm. + */ + public static Bytes digestUsingAlgorithm(Bytes input, String alg) throws NoSuchAlgorithmException { + requireNonNull(input); + requireNonNull(alg); + MessageDigest digest = MessageDigest.getInstance(alg); + input.update(digest); + return Bytes.wrap(digest.digest()); + } + + /** + * Digest using SHA2-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] sha2_256(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA2_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA2-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes32 sha2_256(Bytes input) { + try { + return Bytes32.wrap(digestUsingAlgorithm(input, SHA2_256).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA2-512/256. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha2_512_256(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA2_512_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA-512/256. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes32 sha2_512_256(Bytes input) { + try { + return Bytes32.wrap(digestUsingAlgorithm(input, SHA2_512_256).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] keccak256(byte[] input) { + try { + return digestUsingAlgorithm(input, KECCAK_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes32 keccak256(Bytes input) { + try { + return Bytes32.wrap(digestUsingAlgorithm(input, KECCAK_256).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-512. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] keccak512(byte[] input) { + try { + return digestUsingAlgorithm(input, KECCAK_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-512. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes keccak512(Bytes input) { + try { + return Bytes.wrap(digestUsingAlgorithm(input, KECCAK_512).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-256. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha3_256(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA3_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-256. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes32 sha3_256(Bytes input) { + try { + return Bytes32.wrap(digestUsingAlgorithm(input, SHA3_256).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-512. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha3_512(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA3_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-512. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes sha3_512(Bytes input) { + try { + return Bytes.wrap(digestUsingAlgorithm(input, SHA3_512).toArrayUnsafe()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/InvalidSEC256K1PrivateKeyStoreException.java b/crypto/src/main/java/net/consensys/cava/crypto/InvalidSEC256K1PrivateKeyStoreException.java new file mode 100644 index 00000000..6f97cace --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/InvalidSEC256K1PrivateKeyStoreException.java @@ -0,0 +1,19 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +/** + * Exception thrown when reading a store that contains an invalid SEC256K1 private keys. + */ +public final class InvalidSEC256K1PrivateKeyStoreException extends RuntimeException { +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1.java b/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1.java new file mode 100644 index 00000000..832edf12 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1.java @@ -0,0 +1,751 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static net.consensys.cava.crypto.Hash.keccak256; +import static net.consensys.cava.io.file.Files.atomicReplace; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; +import net.consensys.cava.bytes.MutableBytes; +import net.consensys.cava.units.bigints.UInt256; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Objects; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; + +/* + * Adapted from the BitcoinJ ECKey (Apache 2 License) implementation: + * https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java + * + * + * Adapted from the web3j (Apache 2 License) implementations: + * https://github.com/web3j/web3j/crypto/src/main/java/org/web3j/crypto/*.java + */ + +/** + * An Elliptic Curve Digital Signature using parameters as used by Bitcoin, and defined in Standards for Efficient + * Cryptography (SEC) (Certicom Research, http://www.secg.org/sec2-v2.pdf). + * + *

+ * This class depends upon the BouncyCastle library being available and added as a {@link java.security.Provider}. See + * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation. + * + *

+ * BouncyCastle can be included using the gradle dependency 'org.bouncycastle:bcprov-jdk15on'. + */ +public final class SECP256K1 { + private SECP256K1() {} + + private static final String ALGORITHM = "ECDSA"; + private static final String CURVE_NAME = "secp256k1"; + private static final String PROVIDER = "BC"; + + // Lazily initialize parameters by using java initialization on demand + static final class Parameters { + static final ECDomainParameters CURVE; + static final BigInteger HALF_CURVE_ORDER; + static final KeyPairGenerator KEY_PAIR_GENERATOR; + + static { + try { + Class.forName("org.bouncycastle.asn1.sec.SECNamedCurves"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "BouncyCastle is not available on the classpath, see https://www.bouncycastle.org/latest_releases.html"); + } + X9ECParameters params = SECNamedCurves.getByName(CURVE_NAME); + CURVE = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + HALF_CURVE_ORDER = CURVE.getN().shiftRight(1); + try { + KEY_PAIR_GENERATOR = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); + } catch (NoSuchProviderException e) { + throw new IllegalStateException( + "BouncyCastleProvider is not available, see https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation", + e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(CURVE_NAME); + try { + KEY_PAIR_GENERATOR.initialize(ecGenParameterSpec, new SecureRandom()); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalStateException("Algorithm parameter should be available but was not", e); + } + } + } + + // Decompress a compressed public key (x co-ord and low-bit of y-coord). + private static ECPoint decompressKey(BigInteger xBN, boolean yBit) { + X9IntegerConverter x9 = new X9IntegerConverter(); + byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(Parameters.CURVE.getCurve())); + compEnc[0] = (byte) (yBit ? 0x03 : 0x02); + return Parameters.CURVE.getCurve().decodePoint(compEnc); + } + + /** + * Given the components of a signature and a selector value, recover and return the public key that generated the + * signature according to the algorithm in SEC1v2 section 4.1.6. + * + *

+ * The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the correct one. Because the key + * recovery operation yields multiple potential keys, the correct key must either be stored alongside the signature, + * or you must be willing to try each recId in turn until you find one that outputs the key you are expecting. + * + *

+ * If this method returns null it means recovery was not possible and recId should be iterated. + * + *

+ * Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, and if the output is + * null OR a key that is not the one you expect, you try again with the next recId. + * + * @param recId Which possible key to recover. + * @param r The R component of the signature. + * @param s The S component of the signature. + * @param message Hash of the data that was signed. + * @throws IllegalArgumentException if no key can be recovered from the components + * @return A ECKey containing only the public part. + */ + private static BigInteger recoverFromSignature(int recId, BigInteger r, BigInteger s, Bytes32 message) { + assert (recId >= 0); + assert (r.signum() >= 0); + assert (s.signum() >= 0); + assert (message != null); + + // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) + // 1.1 Let x = r + jn + BigInteger n = Parameters.CURVE.getN(); // Curve order. + BigInteger i = BigInteger.valueOf((long) recId / 2); + BigInteger x = r.add(i.multiply(n)); + // 1.2. Convert the integer x to an octet string X of length mlen using the conversion + // routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. + // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R + // using the conversion routine specified in Section 2.3.4. If this conversion + // routine outputs "invalid", then do another iteration of Step 1. + // + // More concisely, what these points mean is to use X as a compressed public key. + BigInteger prime = SecP256K1Curve.q; + if (x.compareTo(prime) >= 0) { + // Cannot have point co-ordinates larger than this as everything takes place modulo Q. + throw new IllegalArgumentException("x is larger than curve q"); + } + // Compressed keys require you to know an extra bit of data about the y-coord as there are + // two possibilities. So it's encoded in the recId. + ECPoint R = decompressKey(x, (recId & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers + // responsibility). + if (!R.multiply(n).isInfinity()) { + throw new IllegalArgumentException("R times n does not point at infinity"); + } + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + BigInteger e = message.unsignedBigIntegerValue(); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via + // iterating recId) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). + // In the above equation ** is point multiplication and + is point addition (the EC group + // operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For + // example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and + // -3 mod 11 = 8. + BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); + BigInteger rInv = r.modInverse(n); + BigInteger srInv = rInv.multiply(s).mod(n); + BigInteger eInvrInv = rInv.multiply(eInv).mod(n); + ECPoint q = ECAlgorithms.sumOfTwoMultiplies(Parameters.CURVE.getG(), eInvrInv, R, srInv); + + byte[] qBytes = q.getEncoded(false); + // We remove the prefix + return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); + } + + /** + * Generates an ECDSA signature. + * + * @param data The data to sign. + * @param keyPair The keypair to sign using. + * @return The signature. + */ + public static Signature sign(Bytes data, KeyPair keyPair) { + ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + + ECPrivateKeyParameters privKey = + new ECPrivateKeyParameters(keyPair.getPrivateKey().encodedBytes().unsignedBigIntegerValue(), Parameters.CURVE); + signer.init(true, privKey); + + Bytes32 dataHash = keccak256(data); + BigInteger[] components = signer.generateSignature(dataHash.toArrayUnsafe()); + BigInteger r = components[0]; + BigInteger s = components[1]; + + // Automatically adjust the S component to be less than or equal to half the curve + // order, if necessary. This is required because for every signature (r,s) the signature + // (r, -s (mod N)) is a valid signature of the same message. However, we dislike the + // ability to modify the bits of a Bitcoin transaction after it's been signed, as that + // violates various assumed invariants. Thus in future only one of those forms will be + // considered legal and the other will be banned. + if (s.compareTo(Parameters.HALF_CURVE_ORDER) > 0) { + // The order of the curve is the number of valid points that exist on that curve. + // If S is in the upper half of the number of valid points, then bring it back to + // the lower half. Otherwise, imagine that + // N = 10 + // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. + // 10 - 8 == 2, giving us always the latter solution, which is canonical. + s = Parameters.CURVE.getN().subtract(s); + } + + // Now we have to work backwards to figure out the recId needed to recover the signature. + int recId = -1; + BigInteger publicKeyBI = keyPair.getPublicKey().encodedBytes().unsignedBigIntegerValue(); + for (int i = 0; i < 4; i++) { + BigInteger k = recoverFromSignature(i, r, s, dataHash); + if (k.equals(publicKeyBI)) { + recId = i; + break; + } + } + if (recId == -1) { + throw new Error("Unexpected error - could not construct a recoverable key."); + } + + byte v = (byte) (recId + 27); + + return new Signature(r, s, v); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + *

+ * When using native ECDSA verification, data must be 32 bytes, and no element may be larger than 520 bytes. + * + * @param data Hash of the data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + * @return True if the verification is successful. + */ + public static boolean verify(Bytes data, Signature signature, PublicKey pub) { + ECDSASigner signer = new ECDSASigner(); + Bytes toDecode = Bytes.wrap(Bytes.of((byte) 4), pub.encodedBytes()); + ECPublicKeyParameters params = + new ECPublicKeyParameters(Parameters.CURVE.getCurve().decodePoint(toDecode.toArray()), Parameters.CURVE); + signer.init(false, params); + try { + Bytes32 dataHash = keccak256(data); + return signer.verifySignature(dataHash.toArrayUnsafe(), signature.r, signature.s); + } catch (NullPointerException e) { + // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those + // signatures + // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. + return false; + } + } + + /** + * A SECP256K1 private key. + */ + public static class PrivateKey implements java.security.PrivateKey { + + private final Bytes32 encoded; + + private static Bytes32 toBytes32(byte[] backing) { + if (backing.length == Bytes32.SIZE) { + return Bytes32.wrap(backing); + } else if (backing.length > Bytes32.SIZE) { + return Bytes32.wrap(backing, backing.length - Bytes32.SIZE); + } else { + return Bytes32.leftPad(Bytes.wrap(backing)); + } + } + + /** + * Create the private key from a {@link BigInteger}. + * + * @param key The integer describing the key. + * @return The private key. + */ + public static PrivateKey create(BigInteger key) { + checkNotNull(key); + return create(toBytes32(key.toByteArray())); + } + + /** + * Create the private key from encoded bytes. + * + * @param encoded The encoded key bytes. + * @return The private key. + */ + public static PrivateKey create(Bytes32 encoded) { + return new PrivateKey(encoded); + } + + /** + * Load a private key from a file. + * + * @param file The file to read the key from. + * @return The private key. + * @throws IOException On a filesystem error. + * @throws InvalidSEC256K1PrivateKeyStoreException If the file does not contain a valid key. + */ + public static PrivateKey load(Path file) throws IOException, InvalidSEC256K1PrivateKeyStoreException { + try { + List info = Files.readAllLines(file); + if (info.size() != 1) { + throw new InvalidSEC256K1PrivateKeyStoreException(); + } + return SECP256K1.PrivateKey.create(Bytes32.fromHexString((info.get(0)))); + } catch (IllegalArgumentException ex) { + throw new InvalidSEC256K1PrivateKeyStoreException(); + } + } + + private PrivateKey(Bytes32 encoded) { + checkNotNull(encoded); + this.encoded = encoded; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PrivateKey)) { + return false; + } + + PrivateKey that = (PrivateKey) other; + return this.encoded.equals(that.encoded); + } + + @Override + public byte[] getEncoded() { + return encoded.toArrayUnsafe(); + } + + /** + * @return The encoded bytes of the key. + */ + public Bytes32 encodedBytes() { + return encoded; + } + + @Override + public String getAlgorithm() { + return ALGORITHM; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public int hashCode() { + return encoded.hashCode(); + } + + /** + * Write the private key to a file. + * + * @param file The file to write to. + * @throws IOException On a filesystem error. + */ + public void store(Path file) throws IOException { + atomicReplace(file, encoded.toString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String toString() { + return encoded.toString(); + } + } + + /** + * A SECP256K1 public key. + */ + public static class PublicKey implements java.security.PublicKey { + + private static final int BYTE_LENGTH = 64; + + private final Bytes encoded; + + /** + * Create the public key from a private key. + * + * @param privateKey The private key. + * @return The associated public key. + */ + public static PublicKey create(PrivateKey privateKey) { + BigInteger privKey = privateKey.encodedBytes().unsignedBigIntegerValue(); + + /* + * TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group + * order, but that could change in future versions. + */ + if (privKey.bitLength() > Parameters.CURVE.getN().bitLength()) { + privKey = privKey.mod(Parameters.CURVE.getN()); + } + + ECPoint point = new FixedPointCombMultiplier().multiply(Parameters.CURVE.getG(), privKey); + return PublicKey.create(Bytes.wrap(Arrays.copyOfRange(point.getEncoded(false), 1, 65))); + } + + private static Bytes toBytes64(byte[] backing) { + if (backing.length == BYTE_LENGTH) { + return Bytes.wrap(backing); + } else if (backing.length > BYTE_LENGTH) { + return Bytes.wrap(backing, backing.length - BYTE_LENGTH, BYTE_LENGTH); + } else { + MutableBytes res = MutableBytes.create(BYTE_LENGTH); + Bytes.wrap(backing).copyTo(res, BYTE_LENGTH - backing.length); + return res; + } + } + + /** + * Create the public key from a private key. + * + * @param privateKey The private key. + * @return The associated public key. + */ + public static PublicKey create(BigInteger privateKey) { + checkNotNull(privateKey); + return create(toBytes64(privateKey.toByteArray())); + } + + /** + * Create the public key from encoded bytes. + * + * @param encoded The encoded key bytes. + * @return The public key. + */ + public static PublicKey create(Bytes encoded) { + return new PublicKey(encoded); + } + + /** + * Create a public key using a digital signature. + * + * @param data The signed data. + * @param signature The digital signature. + * @throws SECP256K1KeyRecoveryException If no signature can be recovered from the data. + * @return The associated public key. + */ + public static PublicKey recoverFromSignature(Bytes data, Signature signature) { + Bytes32 dataHash = keccak256(data); + int v = signature.v(); + v = v == 27 || v == 28 ? v - 27 : v; + BigInteger publicKeyBI; + try { + publicKeyBI = SECP256K1.recoverFromSignature(v, signature.r(), signature.s(), dataHash); + } catch (IllegalArgumentException e) { + throw new SECP256K1KeyRecoveryException("Public key cannot be recovered: " + e.getMessage(), e); + } + return create(publicKeyBI); + } + + private PublicKey(Bytes encoded) { + checkNotNull(encoded); + checkArgument( + encoded.size() == BYTE_LENGTH, + "Encoding must be %s bytes long, got %s", + BYTE_LENGTH, + encoded.size()); + this.encoded = encoded; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PublicKey)) { + return false; + } + + PublicKey that = (PublicKey) other; + return this.encoded.equals(that.encoded); + } + + @Override + public byte[] getEncoded() { + return encoded.toArrayUnsafe(); + } + + /** + * @return The encoded bytes of the key. + */ + public Bytes encodedBytes() { + return encoded; + } + + @Override + public String getAlgorithm() { + return ALGORITHM; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public int hashCode() { + return encoded.hashCode(); + } + + @Override + public String toString() { + return encoded.toString(); + } + } + + /** + * A SECP256K1 key pair. + */ + public static class KeyPair { + + private final PrivateKey privateKey; + private final PublicKey publicKey; + + /** + * Create a keypair from a private and public key. + * + * @param privateKey The private key. + * @param publicKey The public key. + * @return The key pair. + */ + public static KeyPair create(PrivateKey privateKey, PublicKey publicKey) { + return new KeyPair(privateKey, publicKey); + } + + /** + * Create a keypair using only a private key. + * + * @param privateKey The private key. + * @return The key pair. + */ + public static KeyPair create(PrivateKey privateKey) { + return new KeyPair(privateKey, PublicKey.create(privateKey)); + } + + /** + * Generate a new keypair. + * + * Entropy for the generation is drawn from {@link SecureRandom}. + * + * @return A new keypair. + */ + public static KeyPair random() { + java.security.KeyPair rawKeyPair = Parameters.KEY_PAIR_GENERATOR.generateKeyPair(); + BCECPrivateKey privateKey = (BCECPrivateKey) rawKeyPair.getPrivate(); + BCECPublicKey publicKey = (BCECPublicKey) rawKeyPair.getPublic(); + + BigInteger privateKeyValue = privateKey.getD(); + + // Ethereum does not use encoded public keys like bitcoin - see + // https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm for details + // Additionally, as the first bit is a constant prefix (0x04) we ignore this value + byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); + BigInteger publicKeyValue = new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length)); + + return new KeyPair(PrivateKey.create(privateKeyValue), PublicKey.create(publicKeyValue)); + } + + /** + * Load a key pair from a path. + * + * @param file The file containing an encoded private key. + * @return The key pair. + * @throws IOException On a filesystem error. + * @throws InvalidSEC256K1PrivateKeyStoreException If the file does not contain a valid key. + */ + public static KeyPair load(Path file) throws IOException, InvalidSEC256K1PrivateKeyStoreException { + return create(PrivateKey.load(file)); + } + + private KeyPair(PrivateKey privateKey, PublicKey publicKey) { + checkNotNull(privateKey); + checkNotNull(publicKey); + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + @Override + public int hashCode() { + return Objects.hashCode(privateKey, publicKey); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof KeyPair)) { + return false; + } + + KeyPair that = (KeyPair) other; + return this.privateKey.equals(that.privateKey) && this.publicKey.equals(that.publicKey); + } + + /** + * @return The private key. + */ + public PrivateKey getPrivateKey() { + return privateKey; + } + + /** + * @return The public key. + */ + public PublicKey getPublicKey() { + return publicKey; + } + + /** + * Write the key pair to a file. + * + * @param file The file to write to. + * @throws IOException On a filesystem error. + */ + public void store(Path file) throws IOException { + privateKey.store(file); + } + } + + /** + * A SECP256K1 digital signature. + */ + public static class Signature { + private byte v; + private final BigInteger r; + private final BigInteger s; + + /** + * Create a signature from encoded bytes. + * + * @param encoded The encoded bytes. + * @return The signature. + */ + public static Signature create(Bytes encoded) { + checkNotNull(encoded); + checkArgument(encoded.size() == 65, "encoded must be 65 bytes, but got %s instead", encoded.size()); + BigInteger r = encoded.slice(0, 32).unsignedBigIntegerValue(); + BigInteger s = encoded.slice(32, 32).unsignedBigIntegerValue(); + return new Signature(r, s, encoded.get(64)); + } + + public static Signature create(byte v, BigInteger r, BigInteger s) { + return new Signature(r, s, v); + } + + Signature(BigInteger r, BigInteger s, byte v) { + checkNotNull(r); + checkNotNull(s); + checkInBounds("r", r); + checkInBounds("s", s); + this.r = r; + this.s = s; + this.v = v; + } + + private static void checkInBounds(String name, BigInteger value) { + if (value.compareTo(BigInteger.ONE) < 0) { + throw new IllegalArgumentException(String.format("Invalid '%s' value, should be >= 1 but got %s", name, value)); + } + + if (value.compareTo(Parameters.CURVE.getN()) >= 0) { + throw new IllegalArgumentException( + String.format("Invalid '%s' value, should be < %s but got %s", name, Parameters.CURVE.getN(), value)); + } + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Signature)) { + return false; + } + + Signature that = (Signature) other; + return this.r.equals(that.r) && this.s.equals(that.s) && this.v == that.v; + } + + /** + * @return The encoded bytes of the signature. + */ + public Bytes encodedBytes() { + MutableBytes encoded = MutableBytes.create(65); + UInt256.valueOf(r).toBytes().copyTo(encoded, 0); + UInt256.valueOf(s).toBytes().copyTo(encoded, 32); + encoded.set(64, v); + return encoded; + } + + @Override + public int hashCode() { + return Objects.hashCode(r, s, v); + } + + @Override + public String toString() { + return "Signature{" + "v=" + v + ", r=" + r + ", s=" + s + '}'; + } + + /** + * @return The v-value of the signature. + */ + public byte v() { + return v; + } + + /** + * @return The r-value of the signature. + */ + public BigInteger r() { + return r; + } + + /** + * @return The s-value of the signature. + */ + public BigInteger s() { + return s; + } + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1KeyRecoveryException.java b/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1KeyRecoveryException.java new file mode 100644 index 00000000..70a5f3c0 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/SECP256K1KeyRecoveryException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +/** + * Exception thrown when a key cannot be recovered from a signed data. + */ +public class SECP256K1KeyRecoveryException extends RuntimeException { + + SECP256K1KeyRecoveryException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/package-info.java b/crypto/src/main/java/net/consensys/cava/crypto/package-info.java new file mode 100644 index 00000000..805e45b4 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/package-info.java @@ -0,0 +1,8 @@ +/** + * Classes and utilities for working with cryptography. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-crypto' (cava-crypto.jar). + */ +package net.consensys.cava.crypto; diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/AES256GCM.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/AES256GCM.java new file mode 100644 index 00000000..50abf804 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/AES256GCM.java @@ -0,0 +1,947 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; + +import java.util.Arrays; +import javax.annotation.Nullable; + +import jnr.ffi.Pointer; +import jnr.ffi.byref.LongLongByReference; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/aes-256-gcm.md + +/** + * Authenticated Encryption with Additional Data using AES-GCM. + * + *

+ * WARNING: Despite being the most popular AEAD construction due to its use in TLS, safely using AES-GCM in a different + * context is tricky. + * + *

+ * No more than ~350 GB of input data should be encrypted with a given key. This is for ~16 KB messages -- Actual + * figures vary according to message sizes. + * + *

+ * In addition, nonces are short and repeated nonces would totally destroy the security of this scheme. Nonces should + * thus come from atomic counters, which can be difficult to set up in a distributed environment. + * + *

+ * Unless you absolutely need AES-GCM, use {@link XChaCha20Poly1305} instead. It doesn't have any of these limitations. + * Or, if you don't need to authenticate additional data, just stick to + * {@link Sodium#crypto_box(byte[], byte[], long, byte[], byte[], byte[])}. + * + *

+ * This class depends upon the JNR-FFI library being available on the classpath, along with its dependencies. See + * https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +public final class AES256GCM implements AutoCloseable { + + private static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * Check if Sodium and the AES256-GCM algorithm is available. + * + * @return true if Sodium and the AES256-GCM algorithm is available. + */ + public static boolean isAvailable() { + try { + return Sodium.crypto_aead_aes256gcm_is_available() != 0; + } catch (LinkageError e) { + return false; + } + } + + private static void assertAvailable() { + if (!isAvailable()) { + throw new IllegalStateException("AES256-GCM is not available"); + } + } + + /** + * A AES256-GSM key. + */ + public static final class Key { + private final Pointer ptr; + + private Key(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_aead_aes256gcm_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_aead_aes256gcm_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_aead_aes256gcm_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_aead_aes256gcm_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + assertAvailable(); + Pointer ptr = Sodium.malloc(length()); + try { + Sodium.crypto_aead_aes256gcm_keygen(ptr); + return new Key(ptr); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A AES256-GSM nonce. + */ + public static final class Nonce { + private final Pointer ptr; + + private Nonce(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_aead_aes256gcm_npubbytes()) { + throw new IllegalArgumentException( + "nonce must be " + Sodium.crypto_aead_aes256gcm_npubbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (12). + * + * @return The length of the nonce in bytes (12). + */ + public static int length() { + long npubbytes = Sodium.crypto_aead_aes256gcm_npubbytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_aead_aes256gcm_npubbytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Generate a new {@link Nonce} using a random generator. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

+ * Note that this is not synchronized. If multiple threads are creating encrypted messages and incrementing this + * nonce, then external synchronization is required to ensure no two encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(ptr, length(), Nonce::new); + } + + /** + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + private Pointer ctx; + + private AES256GCM(Key key) { + ctx = Sodium.malloc(Sodium.crypto_aead_aes256gcm_statebytes()); + try { + int rc = Sodium.crypto_aead_aes256gcm_beforenm(ctx, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_beforenm: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(ctx); + ctx = null; + throw e; + } + } + + /** + * Precompute the expansion for the key. + * + *

+ * Note that the returned instance of {@link AES256GCM} should be closed using {@link #close()} (or + * try-with-resources) to ensure timely release of the expanded key, which is held in native memory. + * + * @param key The key to precompute an expansion for. + * @return A {@link AES256GCM} instance. + */ + public static AES256GCM forKey(Key key) { + requireNonNull(key); + assertAvailable(); + return new AES256GCM(key); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Bytes data, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + + byte[] cipherText = new byte[maxCombinedCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_encrypt( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return maybeSliceResult(cipherText, cipherTextLen, "crypto_aead_aes256gcm_encrypt"); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, nonce); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Bytes data, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[maxCombinedCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_encrypt_afternm( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + ctx); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt_afternm: failed with result " + rc); + } + + return maybeSliceResult(cipherText, cipherTextLen, "crypto_aead_aes256gcm_encrypt_afternm"); + } + + private static int maxCombinedCypherTextLength(byte[] message) { + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + return (int) abytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Bytes data, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_encrypt_detached( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, + maybeSliceResult(mac, macLen, "crypto_aead_aes256gcm_encrypt_detached")); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Bytes data, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_encrypt_detached_afternm( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + ctx); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt_detached_afternm: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, + maybeSliceResult(mac, macLen, "crypto_aead_aes256gcm_encrypt_detached_afternm")); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce to use when decrypting. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce to use when decrypting. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_decrypt( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.ptr, + key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_aes256gcm_decrypt"); + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decrypt(byte[] cipherText, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, nonce); + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Bytes data, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decrypt(byte[] cipherText, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_aes256gcm_decrypt_afternm( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.ptr, + ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_decrypt_afternm: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_aes256gcm_decrypt_afternm"); + } + + private static int maxClearTextLength(byte[] cipherText) { + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) abytes); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium.crypto_aead_aes256gcm_decrypt_detached( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.ptr, + key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, nonce); + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Bytes data, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, byte[] data, Nonce nonce) { + assertAvailable(); + + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium.crypto_aead_aes256gcm_decrypt_detached_afternm( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.ptr, + ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_decrypt_detached_afternm: failed with result " + rc); + } + + return clearText; + } + + private void assertOpen() { + if (ctx == null) { + throw new IllegalStateException(getClass().getName() + ": already closed"); + } + } + + private static byte[] maybeSliceResult(byte[] bytes, LongLongByReference actualLength, String methodName) { + if (actualLength.longValue() == bytes.length) { + return bytes; + } + if (actualLength.longValue() > Integer.MAX_VALUE) { + throw new SodiumException(methodName + ": result of length " + actualLength.longValue() + " is too large"); + } + return Arrays.copyOfRange(bytes, 0, actualLength.intValue()); + } + + @Override + public void close() { + if (ctx != null) { + Sodium.sodium_free(ctx); + ctx = null; + } + } + + @Override + protected void finalize() { + close(); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/Auth.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Auth.java new file mode 100644 index 00000000..c87e1073 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Auth.java @@ -0,0 +1,201 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import jnr.ffi.Pointer; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/secret-key_authentication.md + +/** + * Secret-key authentication. + * + *

+ * These operations computes an authentication tag for a message and a secret key, and provides a way to verify that a + * given tag is valid for a given message and a key. + * + *

+ * The function computing the tag is deterministic: the same (message, key) tuple will always produce the same output. + * + *

+ * However, even if the message is public, knowing the key is required in order to be able to compute a valid tag. + * Therefore, the key should remain confidential. The tag, however, can be public. + * + *

+ * A typical use case is: + * + *

    + *
  • {@code A} prepares a message, add an authentication tag, sends it to {@code B}
  • + *
  • {@code A} doesn't store the message
  • + *
  • Later on, {@code B} sends the message and the authentication tag to {@code A}
  • + *
  • {@code A} uses the authentication tag to verify that it created this message.
  • + *
+ * + *

+ * This operation does not encrypt the message. It only computes and verifies an authentication tag. + */ +public final class Auth { + private Auth() {} + + /** + * An Auth key. + */ + public static final class Key { + private final Pointer ptr; + + private Key(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_auth_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_auth_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_auth_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + Pointer ptr = Sodium.malloc(length()); + try { + Sodium.crypto_auth_keygen(ptr); + return new Key(ptr); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * Create an authentication tag for a given input. + * + * @param input The input to generate an authentication tag for. + * @param key A confidential key. + * @return The authentication tag. + */ + public static Bytes auth(Bytes input, Key key) { + return Bytes.wrap(auth(input.toArrayUnsafe(), key)); + } + + /** + * Create an authentication tag for a given input. + * + * @param input The input to generate an authentication tag for. + * @param key A confidential key. + * @return The authentication tag. + */ + public static byte[] auth(byte[] input, Key key) { + long abytes = Sodium.crypto_auth_bytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_auth_bytes: " + abytes + " is too large"); + } + byte[] tag = new byte[(int) abytes]; + + int rc = Sodium.crypto_auth(tag, input, input.length, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_auth_bytes: failed with result " + rc); + } + return tag; + } + + /** + * Verify an input using an authentication tag. + * + * @param tag The authentication tag for the input. + * @param input The input. + * @param key A confidential key that was used for tag creation. + * @return true if the tag correction authenticates the input (using the specified key). + */ + public static boolean verify(Bytes tag, Bytes input, Key key) { + return verify(tag.toArrayUnsafe(), input.toArrayUnsafe(), key); + } + + /** + * Verify an input using an authentication tag. + * + * @param tag The authentication tag for the input. + * @param input The input. + * @param key A confidential key that was used for tag creation. + * @return true if the tag correction authenticates the input (using the specified key). + */ + public static boolean verify(byte[] tag, byte[] input, Key key) { + long abytes = Sodium.crypto_auth_bytes(); + if (tag.length != abytes) { + throw new IllegalArgumentException("tag must be " + abytes + " bytes, got " + tag.length); + } + int rc = Sodium.crypto_auth_verify(tag, input, input.length, key.ptr); + return (rc == 0); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/Box.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Box.java new file mode 100644 index 00000000..df612d59 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Box.java @@ -0,0 +1,999 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import javax.annotation.Nullable; + +import jnr.ffi.Pointer; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/public-key_cryptography/authenticated_encryption.md + +/** + * Public-key authenticated encryption. + * + *

+ * Using public-key authenticated encryption, Bob can encrypt a confidential message specifically for Alice, using + * Alice's public key. + * + *

+ * Using Bob's public key, Alice can compute a shared secret key. Using Alice's public key and his secret key, Bob can + * compute the exact same shared secret key. That shared secret key can be used to verify that the encrypted message was + * not tampered with, before eventually decrypting it. + * + *

+ * Alice only needs Bob's public key, the nonce and the ciphertext. Bob should never ever share his secret key, even + * with Alice. + * + *

+ * And in order to send messages to Alice, Bob only needs Alice's public key. Alice should never ever share her secret + * key either, even with Bob. + * + *

+ * Alice can reply to Bob using the same system, without having to generate a distinct key pair. + * + *

+ * The nonce doesn't have to be confidential, but it should be used with just one encryption for a particular pair of + * public and secret keys. + * + *

+ * One easy way to generate a nonce is to use {@link Nonce#random()}, considering the size of the nonces the risk of any + * random collisions is negligible. For some applications, if you wish to use nonces to detect missing messages or to + * ignore replayed messages, it is also acceptable to use an incrementing counter as a nonce. + * + *

+ * When doing so you must ensure that the same value can never be re-used (for example you may have multiple threads or + * even hosts generating messages using the same key pairs). + * + *

+ * As stated above, senders can decrypt their own messages, and compute a valid authentication tag for any messages + * encrypted with a given shared secret key. This is generally not an issue for online protocols. + * + *

+ * This class depends upon the JNR-FFI library being available on the classpath, along with its dependencies. See + * https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +public final class Box implements AutoCloseable { + + /** + * A Box public key. + */ + public static final class PublicKey { + private final Pointer ptr; + + private PublicKey(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_box_publickeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_publickeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A Box secret key. + */ + public static final class SecretKey { + private final Pointer ptr; + + private SecretKey(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_secretkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_secretkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_box_secretkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_secretkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A Box key pair seed. + */ + public static final class Seed { + private final Pointer ptr; + + private Seed(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_seedbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_seedbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Seed::new); + } + + /** + * Obtain the length of the seed in bytes (32). + * + * @return The length of the seed in bytes (32). + */ + public static int length() { + long seedbytes = Sodium.crypto_box_seedbytes(); + if (seedbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_seedbytes: " + seedbytes + " is too large"); + } + return (int) seedbytes; + } + + /** + * Generate a new {@link Seed} using a random generator. + * + * @return A randomly generated seed. + */ + public static Seed random() { + return Sodium.randomBytes(length(), Seed::new); + } + + /** + * @return The bytes of this seed. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this seed. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A Box key pair. + */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from an array of secret key bytes. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + return Sodium.scalarMultBase(secretKey.ptr, SecretKey.length(), (ptr, len) -> { + if (len != PublicKey.length()) { + throw new IllegalStateException( + "Public key length " + PublicKey.length() + " is not same as generated key length " + len); + } + return new KeyPair(new PublicKey(ptr), secretKey); + }); + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + Pointer publicKey = Sodium.malloc(PublicKey.length()); + Pointer secretKey = null; + try { + secretKey = Sodium.malloc(SecretKey.length()); + int rc = Sodium.crypto_box_keypair(publicKey, secretKey); + if (rc != 0) { + throw new SodiumException("crypto_box_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey); + publicKey = null; + SecretKey sk = new SecretKey(secretKey); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Generate a new key using a seed. + * + * @param seed A seed. + * @return The generated key pair. + */ + public static KeyPair fromSeed(Seed seed) { + Pointer publicKey = Sodium.malloc(PublicKey.length()); + Pointer secretKey = null; + try { + secretKey = Sodium.malloc(SecretKey.length()); + int rc = Sodium.crypto_box_seed_keypair(publicKey, secretKey, seed.ptr); + if (rc != 0) { + throw new SodiumException("crypto_box_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey); + publicKey = null; + SecretKey sk = new SecretKey(secretKey); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + } + + /** + * A Box nonce. + */ + public static final class Nonce { + private final Pointer ptr; + + private Nonce(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_noncebytes()) { + throw new IllegalArgumentException( + "nonce must be " + Sodium.crypto_box_noncebytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + */ + public static int length() { + long npubbytes = Sodium.crypto_box_noncebytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_noncebytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Generate a new {@link Nonce} using a random generator. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

+ * Note that this is not synchronized. If multiple threads are creating encrypted messages and incrementing this + * nonce, then external synchronization is required to ensure no two encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(ptr, length(), Nonce::new); + } + + /** + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + private Pointer ctx; + + private Box(PublicKey publicKey, SecretKey secretKey) { + ctx = Sodium.malloc(Sodium.crypto_box_beforenmbytes()); + try { + int rc = Sodium.crypto_box_beforenm(ctx, publicKey.ptr, secretKey.ptr); + if (rc != 0) { + throw new SodiumException("crypto_box_beforenm: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(ctx); + ctx = null; + throw e; + } + } + + /** + * Precompute the shared key for a given sender and receiver. + * + *

+ * Note that the returned instance of {@link Box} should be closed using {@link #close()} (or try-with-resources) to + * ensure timely release of the shared key, which is held in native memory. + * + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @return A {@link Box} instance. + */ + public static Box forKeys(PublicKey receiver, SecretKey sender) { + return new Box(receiver, sender); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, PublicKey receiver, SecretKey sender, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), receiver, sender, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, PublicKey receiver, SecretKey sender, Nonce nonce) { + byte[] cipherText = new byte[combinedCypherTextLength(message)]; + + int rc = Sodium.crypto_box_easy(cipherText, message, message.length, nonce.ptr, receiver.ptr, sender.ptr); + if (rc != 0) { + throw new SodiumException("crypto_box_easy: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[combinedCypherTextLength(message)]; + + int rc = Sodium.crypto_box_easy_afternm(cipherText, message, message.length, nonce.ptr, ctx); + if (rc != 0) { + throw new SodiumException("crypto_box_easy_afternm: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a sealed message for a given key. + * + *

+ * Sealed boxes are designed to anonymously send messages to a recipient given its public key. + * + *

+ * Only the recipient can decrypt these messages, using its private key. While the recipient can verify the integrity + * of the message, it cannot verify the identity of the sender. + * + *

+ * A message is encrypted using an ephemeral key pair, whose secret part is destroyed right after the encryption + * process. + * + *

+ * Without knowing the secret key used for a given message, the sender cannot decrypt its own message later. And + * without additional data, a message cannot be correlated with the identity of its sender. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @return The encrypted data. + */ + public static Bytes encryptSealed(Bytes message, PublicKey receiver) { + return Bytes.wrap(encryptSealed(message.toArrayUnsafe(), receiver)); + } + + /** + * Encrypt a sealed message for a given key. + * + *

+ * Sealed boxes are designed to anonymously send messages to a recipient given its public key. + * + *

+ * Only the recipient can decrypt these messages, using its private key. While the recipient can verify the integrity + * of the message, it cannot verify the identity of the sender. + * + *

+ * A message is encrypted using an ephemeral key pair, whose secret part is destroyed right after the encryption + * process. + * + *

+ * Without knowing the secret key used for a given message, the sender cannot decrypt its own message later. And + * without additional data, a message cannot be correlated with the identity of its sender. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @return The encrypted data. + */ + public static byte[] encryptSealed(byte[] message, PublicKey receiver) { + long sealbytes = Sodium.crypto_box_sealbytes(); + if (sealbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_sealbytes: " + sealbytes + " is too large"); + } + byte[] cipherText = new byte[(int) sealbytes + message.length]; + + int rc = Sodium.crypto_box_seal(cipherText, message, message.length, receiver.ptr); + if (rc != 0) { + throw new SodiumException("crypto_box_seal: failed with result " + rc); + } + + return cipherText; + } + + private static int combinedCypherTextLength(byte[] message) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + return (int) macbytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, + PublicKey receiver, + SecretKey sender, + Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), receiver, sender, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, + PublicKey receiver, + SecretKey sender, + Nonce nonce) { + byte[] cipherText = new byte[message.length]; + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + byte[] mac = new byte[(int) macbytes]; + + int rc = Sodium.crypto_box_detached(cipherText, mac, message, message.length, nonce.ptr, receiver.ptr, sender.ptr); + if (rc != 0) { + throw new SodiumException("crypto_box_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[message.length]; + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + byte[] mac = new byte[(int) macbytes]; + + int rc = Sodium.crypto_box_detached_afternm(cipherText, mac, message, message.length, nonce.ptr, ctx); + if (rc != 0) { + throw new SodiumException("crypto_box_detached_afternm: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, PublicKey sender, SecretKey receiver, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), sender, receiver, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, PublicKey sender, SecretKey receiver, Nonce nonce) { + byte[] clearText = new byte[clearTextLength(cipherText)]; + + int rc = Sodium.crypto_box_open_easy(clearText, cipherText, cipherText.length, nonce.ptr, sender.ptr, receiver.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_easy: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decrypt(byte[] cipherText, Nonce nonce) { + assertOpen(); + + byte[] clearText = new byte[clearTextLength(cipherText)]; + + int rc = Sodium.crypto_box_open_easy_afternm(clearText, cipherText, cipherText.length, nonce.ptr, ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_easy_afternm: failed with result " + rc); + } + + return clearText; + } + + private static int clearTextLength(byte[] cipherText) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (macbytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) macbytes); + } + + /** + * Decrypt a sealed message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptSealed(Bytes cipherText, PublicKey sender, SecretKey receiver) { + byte[] bytes = decryptSealed(cipherText.toArrayUnsafe(), sender, receiver); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a sealed message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptSealed(byte[] cipherText, PublicKey sender, SecretKey receiver) { + long sealbytes = Sodium.crypto_box_sealbytes(); + if (sealbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_sealbytes: " + sealbytes + " is too large"); + } + if (sealbytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + byte[] clearText = new byte[cipherText.length - ((int) sealbytes)]; + + int rc = Sodium.crypto_box_seal_open(clearText, cipherText, cipherText.length, sender.ptr, receiver.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_seal_open: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, PublicKey sender, SecretKey receiver, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), sender, receiver, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, + byte[] mac, + PublicKey sender, + SecretKey receiver, + Nonce nonce) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (mac.length != macbytes) { + throw new IllegalArgumentException("mac must be " + macbytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium + .crypto_box_open_detached(clearText, cipherText, mac, cipherText.length, nonce.ptr, sender.ptr, receiver.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_detached: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, Nonce nonce) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (mac.length != macbytes) { + throw new IllegalArgumentException("mac must be " + macbytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium.crypto_box_open_detached_afternm(clearText, cipherText, mac, cipherText.length, nonce.ptr, ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_detached_afternm: failed with result " + rc); + } + + return clearText; + } + + private void assertOpen() { + if (ctx == null) { + throw new IllegalStateException(getClass().getName() + ": already closed"); + } + } + + @Override + public void close() { + if (ctx != null) { + Sodium.sodium_free(ctx); + ctx = null; + } + } + + @Override + protected void finalize() { + close(); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/DefaultDetachedEncryptionResult.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/DefaultDetachedEncryptionResult.java new file mode 100644 index 00000000..2209210a --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/DefaultDetachedEncryptionResult.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +final class DefaultDetachedEncryptionResult implements DetachedEncryptionResult { + + private final byte[] cipherText; + private final byte[] mac; + + public DefaultDetachedEncryptionResult(byte[] cipherText, byte[] mac) { + this.cipherText = cipherText; + this.mac = mac; + } + + @Override + public Bytes cipherText() { + return Bytes.wrap(cipherText); + } + + @Override + public byte[] cipherTextArray() { + return cipherText; + } + + @Override + public Bytes mac() { + return Bytes.wrap(mac); + } + + @Override + public byte[] macArray() { + return mac; + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/DetachedEncryptionResult.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/DetachedEncryptionResult.java new file mode 100644 index 00000000..e27fcf20 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/DetachedEncryptionResult.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +/** + * The result from a detached encryption. + */ +public interface DetachedEncryptionResult { + + /** + * @return The cipher text. + */ + Bytes cipherText(); + + /** + * @return The cipher text. + */ + byte[] cipherTextArray(); + + /** + * @return The message authentication code. + */ + Bytes mac(); + + /** + * @return The message authentication code. + */ + byte[] macArray(); +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyDerivation.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyDerivation.java new file mode 100644 index 00000000..97e3f77a --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyDerivation.java @@ -0,0 +1,244 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import java.util.Arrays; + +import com.google.common.base.Charsets; +import jnr.ffi.Pointer; + +/** + * Key derivation. + * + *

+ * Multiple secret subkeys can be derived from a single master key. + * + *

+ * Given the master key and a key identifier, a subkey can be deterministically computed. However, given a subkey, an + * attacker cannot compute the master key nor any other subkeys. + */ +public final class KeyDerivation { + + /** + * A KeyDerivation master key. + */ + public static final class Key { + private final Pointer ptr; + + private Key(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kdf_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kdf_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kdf_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kdf_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + Pointer ptr = Sodium.malloc(length()); + try { + Sodium.crypto_kdf_keygen(ptr); + return new Key(ptr); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * Derive a sub key. + * + * @param length The length of the sub key, which must be between {@link #minSubKeyLength()} and + * {@link #maxSubKeyLength()}. + * @param subkeyId The id for the sub key. + * @param context The context for the sub key, which must be of length {@link #contextLength()}. + * @param key The master key. + * @return The derived sub key. + */ + public static Bytes deriveKey(int length, long subkeyId, byte[] context, Key key) { + return Bytes.wrap(deriveKeyArray(length, subkeyId, context, key)); + } + + /** + * Derive a sub key. + * + * @param length The length of the sub key, which must be between {@link #minSubKeyLength()} and + * {@link #maxSubKeyLength()}. + * @param subkeyId The id for the sub key. + * @param context The context for the sub key, which must be of length {@link #contextLength()}. + * @param key The master key. + * @return The derived sub key. + */ + public static byte[] deriveKeyArray(int length, long subkeyId, byte[] context, Key key) { + assertSubKeyLength(length); + assertContextLength(context); + + byte[] subKey = new byte[length]; + int rc = Sodium.crypto_kdf_derive_from_key(subKey, subKey.length, subkeyId, context, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_kdf_derive_from_key: failed with result " + rc); + } + return subKey; + } + + /** + * Derive a sub key. + * + * @param length The length of the subkey. + * @param subkeyId The id for the subkey. + * @param context The context for the sub key, which must be of length ≤ {@link #contextLength()}. + * @param key The master key. + * @return The derived sub key. + */ + public static Bytes deriveKey(int length, long subkeyId, String context, Key key) { + return Bytes.wrap(deriveKeyArray(length, subkeyId, context, key)); + } + + /** + * Derive a sub key. + * + * @param length The length of the subkey. + * @param subkeyId The id for the subkey. + * @param context The context for the sub key, which must be of length ≤ {@link #contextLength()}. + * @param key The master key. + * @return The derived sub key. + */ + public static byte[] deriveKeyArray(int length, long subkeyId, String context, Key key) { + int contextLen = contextLength(); + byte[] contextBytes = context.getBytes(Charsets.UTF_8); + if (context.length() > contextLen) { + throw new IllegalArgumentException("context must be " + contextLen + " bytes, got " + context.length()); + } + byte[] ctx; + if (contextBytes.length == contextLen) { + ctx = contextBytes; + } else { + ctx = Arrays.copyOf(contextBytes, contextLen); + } + + return deriveKeyArray(length, subkeyId, ctx, key); + } + + /** + * @return The required length for the context (8). + */ + public static int contextLength() { + long contextbytes = Sodium.crypto_kdf_contextbytes(); + if (contextbytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_min: " + contextbytes + " is too large"); + } + return (int) contextbytes; + } + + /** + * @return The minimum length for a new sub key (16). + */ + public static int minSubKeyLength() { + long length = Sodium.crypto_kdf_bytes_min(); + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_min: " + length + " is too large"); + } + return (int) length; + } + + /** + * @return The maximum length for a new sub key (64). + */ + public static int maxSubKeyLength() { + long length = Sodium.crypto_kdf_bytes_max(); + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_max: " + length + " is too large"); + } + return (int) length; + } + + private static void assertContextLength(byte[] context) { + long contextBytes = Sodium.crypto_kdf_contextbytes(); + if (context.length != contextBytes) { + throw new IllegalArgumentException("context must be " + contextBytes + " bytes, got " + context.length); + } + } + + private static void assertSubKeyLength(int length) { + long minLength = Sodium.crypto_kdf_bytes_min(); + long maxLength = Sodium.crypto_kdf_bytes_max(); + if (length < minLength || length > maxLength) { + throw new IllegalArgumentException("length is out of range [" + minLength + ", " + maxLength + "]"); + } + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyExchange.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyExchange.java new file mode 100644 index 00000000..25b2f3d9 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/KeyExchange.java @@ -0,0 +1,546 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import jnr.ffi.Pointer; + +/** + * Key exchange. + * + *

+ * Allows two parties can securely compute a set of shared keys using their peer's public key and their own secret key. + */ +public final class KeyExchange { + + /** + * A KeyExchange public key. + */ + public static final class PublicKey { + private final Pointer ptr; + + private PublicKey(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_publickeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_publickeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A KeyExchange secret key. + */ + public static final class SecretKey { + private final Pointer ptr; + + private SecretKey(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_secretkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_secretkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_secretkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_secretkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A KeyExchange key pair seed. + */ + public static final class Seed { + private final Pointer ptr; + + private Seed(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_seedbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_seedbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Seed::new); + } + + /** + * Obtain the length of the seed in bytes (32). + * + * @return The length of the seed in bytes (32). + */ + public static int length() { + long seedbytes = Sodium.crypto_kx_seedbytes(); + if (seedbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_seedbytes: " + seedbytes + " is too large"); + } + return (int) seedbytes; + } + + /** + * Generate a new {@link Seed} using a random generator. + * + * @return A randomly generated seed. + */ + public static Seed random() { + return Sodium.randomBytes(length(), Seed::new); + } + + /** + * @return The bytes of this seed. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this seed. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A KeyExchange key pair. + */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from a secret key. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + return Sodium.scalarMultBase(secretKey.ptr, SecretKey.length(), (ptr, len) -> { + if (len != PublicKey.length()) { + throw new IllegalStateException( + "Public key length " + PublicKey.length() + " is not same as generated key length " + len); + } + return new KeyPair(new PublicKey(ptr), secretKey); + }); + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + Pointer publicKey = Sodium.malloc(PublicKey.length()); + Pointer secretKey = null; + try { + secretKey = Sodium.malloc(SecretKey.length()); + int rc = Sodium.crypto_kx_keypair(publicKey, secretKey); + if (rc != 0) { + throw new SodiumException("crypto_kx_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey); + publicKey = null; + SecretKey sk = new SecretKey(secretKey); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Generate a new key using a seed. + * + * @param seed A seed. + * @return The generated key pair. + */ + public static KeyPair fromSeed(Seed seed) { + Pointer publicKey = Sodium.malloc(PublicKey.length()); + Pointer secretKey = null; + try { + secretKey = Sodium.malloc(SecretKey.length()); + int rc = Sodium.crypto_kx_seed_keypair(publicKey, secretKey, seed.ptr); + if (rc != 0) { + throw new SodiumException("crypto_kx_seed_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey); + publicKey = null; + SecretKey sk = new SecretKey(secretKey); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + } + + /** + * A KeyExchange session key. + */ + public static final class SessionKey { + private final Pointer ptr; + + private SessionKey(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SessionKey} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_sessionkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_sessionkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_sessionkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_sessionkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A KeyExchange session key pair. + */ + public static final class SessionKeyPair { + + private final SessionKey rxKey; + private final SessionKey txKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param rxKey The bytes for the secret key. + * @param txKey The bytes for the public key. + */ + public SessionKeyPair(SessionKey rxKey, SessionKey txKey) { + this.rxKey = rxKey; + this.txKey = txKey; + } + + /** @return The session key that will be used to receive data. */ + public SessionKey rx() { + return rxKey; + } + + /** @return The session key that will be used to send data. */ + public SessionKey tx() { + return txKey; + } + } + + /** + * Computer a pair of session keys for use by a client. + * + * @param clientKeys The client key pair. + * @param serverKey The server public key. + * @return A pair of session keys. + */ + public static SessionKeyPair client(KeyPair clientKeys, PublicKey serverKey) { + long sessionkeybytes = Sodium.crypto_kx_sessionkeybytes(); + Pointer rxPtr = null; + Pointer txPtr = null; + try { + rxPtr = Sodium.malloc(sessionkeybytes); + txPtr = Sodium.malloc(sessionkeybytes); + int rc = Sodium.crypto_kx_client_session_keys( + rxPtr, + txPtr, + clientKeys.publicKey.ptr, + clientKeys.secretKey.ptr, + serverKey.ptr); + if (rc != 0) { + throw new SodiumException("crypto_kx_client_session_keys: failed with result " + rc); + } + SessionKey rxKey = new SessionKey(rxPtr); + rxPtr = null; + SessionKey txKey = new SessionKey(txPtr); + txPtr = null; + return new SessionKeyPair(rxKey, txKey); + } catch (Throwable e) { + if (rxPtr != null) { + Sodium.sodium_free(rxPtr); + } + if (txPtr != null) { + Sodium.sodium_free(txPtr); + } + throw e; + } + } + + /** + * Computer a pair of session keys for use by a client. + * + * @param serverKeys The server key pair. + * @param clientKey The client public key. + * @return A pair of session keys. + */ + public static SessionKeyPair server(KeyPair serverKeys, PublicKey clientKey) { + long sessionkeybytes = Sodium.crypto_kx_sessionkeybytes(); + Pointer rxPtr = null; + Pointer txPtr = null; + try { + rxPtr = Sodium.malloc(sessionkeybytes); + txPtr = Sodium.malloc(sessionkeybytes); + int rc = Sodium.crypto_kx_server_session_keys( + rxPtr, + txPtr, + serverKeys.publicKey.ptr, + serverKeys.secretKey.ptr, + clientKey.ptr); + if (rc != 0) { + throw new SodiumException("crypto_kx_client_session_keys: failed with result " + rc); + } + SessionKey rxKey = new SessionKey(rxPtr); + rxPtr = null; + SessionKey txKey = new SessionKey(txPtr); + txPtr = null; + return new SessionKeyPair(rxKey, txKey); + } catch (Throwable e) { + if (rxPtr != null) { + Sodium.sodium_free(rxPtr); + } + if (txPtr != null) { + Sodium.sodium_free(txPtr); + } + throw e; + } + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/LibSodium.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/LibSodium.java new file mode 100644 index 00000000..07748262 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/LibSodium.java @@ -0,0 +1,2596 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import jnr.ffi.Pointer; +import jnr.ffi.annotations.In; +import jnr.ffi.annotations.Out; +import jnr.ffi.byref.ByteByReference; +import jnr.ffi.byref.LongLongByReference; +import jnr.ffi.types.ssize_t; +import jnr.ffi.types.u_int32_t; +import jnr.ffi.types.u_int64_t; +import jnr.ffi.types.u_int8_t; + +// Generated with https://gist.github.com/cleishm/39fbad03378f5e1ad82521ad821cd065, then modified +public interface LibSodium { + // const char * sodium_version_string(void); + String sodium_version_string(); + + // int sodium_library_version_major(void); + int sodium_library_version_major(); + + // int sodium_library_version_minor(void); + int sodium_library_version_minor(); + + // int sodium_library_minimal(void); + int sodium_library_minimal(); + + // int sodium_init(void); + int sodium_init(); + + // int sodium_set_misuse_handler(void * handler); + int sodium_set_misuse_handler(/*both*/ Pointer handler); + + // void sodium_misuse(void); + void sodium_misuse(); + + // int crypto_aead_aes256gcm_is_available(void); + int crypto_aead_aes256gcm_is_available(); + + // size_t crypto_aead_aes256gcm_keybytes(void); + @ssize_t + long crypto_aead_aes256gcm_keybytes(); + + // size_t crypto_aead_aes256gcm_nsecbytes(void); + @ssize_t + long crypto_aead_aes256gcm_nsecbytes(); + + // size_t crypto_aead_aes256gcm_npubbytes(void); + @ssize_t + long crypto_aead_aes256gcm_npubbytes(); + + // size_t crypto_aead_aes256gcm_abytes(void); + @ssize_t + long crypto_aead_aes256gcm_abytes(); + + // size_t crypto_aead_aes256gcm_messagebytes_max(void); + @ssize_t + long crypto_aead_aes256gcm_messagebytes_max(); + + // size_t crypto_aead_aes256gcm_statebytes(void); + @ssize_t + long crypto_aead_aes256gcm_statebytes(); + + // int crypto_aead_aes256gcm_encrypt(unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_aes256gcm_encrypt( + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + /*null*/ Pointer nsec, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_aes256gcm_decrypt(unsigned char * m, unsigned long long * mlen_p, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_aes256gcm_decrypt( + @Out byte[] m, + @Out LongLongByReference mlen_p, + /*null*/ Pointer nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_aes256gcm_encrypt_detached(unsigned char * c, unsigned char * mac, unsigned long long * maclen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_aes256gcm_encrypt_detached( + @Out byte[] c, + @Out byte[] mac, + @Out LongLongByReference maclen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + /*null*/ Pointer nsec, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_aes256gcm_decrypt_detached(unsigned char * m, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * mac, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_aes256gcm_decrypt_detached( + @Out byte[] m, + /*null*/ Pointer nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] mac, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_aes256gcm_beforenm(crypto_aead_aes256gcm_state * ctx_, const unsigned char * k); + int crypto_aead_aes256gcm_beforenm(@Out Pointer ctx_, @In Pointer k); + + // int crypto_aead_aes256gcm_encrypt_afternm(unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const crypto_aead_aes256gcm_state * ctx_); + int crypto_aead_aes256gcm_encrypt_afternm( + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + /*null*/ Pointer nsec, + @In Pointer npub, + @In Pointer ctx_); + + // int crypto_aead_aes256gcm_decrypt_afternm(unsigned char * m, unsigned long long * mlen_p, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const crypto_aead_aes256gcm_state * ctx_); + int crypto_aead_aes256gcm_decrypt_afternm( + @Out byte[] m, + @Out LongLongByReference mlen_p, + /*null*/ Pointer nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer ctx_); + + // int crypto_aead_aes256gcm_encrypt_detached_afternm(unsigned char * c, unsigned char * mac, unsigned long long * maclen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const crypto_aead_aes256gcm_state * ctx_); + int crypto_aead_aes256gcm_encrypt_detached_afternm( + @Out byte[] c, + @Out byte[] mac, + @Out LongLongByReference maclen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + /*null*/ Pointer nsec, + @In Pointer npub, + @In Pointer ctx_); + + // int crypto_aead_aes256gcm_decrypt_detached_afternm(unsigned char * m, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * mac, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const crypto_aead_aes256gcm_state * ctx_); + int crypto_aead_aes256gcm_decrypt_detached_afternm( + @Out byte[] m, + /*null*/ Pointer nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] mac, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer ctx_); + + // void crypto_aead_aes256gcm_keygen(unsigned char[] k); + void crypto_aead_aes256gcm_keygen(@Out Pointer k); + + // size_t crypto_aead_chacha20poly1305_ietf_keybytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_ietf_keybytes(); + + // size_t crypto_aead_chacha20poly1305_ietf_nsecbytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_ietf_nsecbytes(); + + // size_t crypto_aead_chacha20poly1305_ietf_npubbytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_ietf_npubbytes(); + + // size_t crypto_aead_chacha20poly1305_ietf_abytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_ietf_abytes(); + + // size_t crypto_aead_chacha20poly1305_ietf_messagebytes_max(void); + @ssize_t + long crypto_aead_chacha20poly1305_ietf_messagebytes_max(); + + // int crypto_aead_chacha20poly1305_ietf_encrypt(unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_ietf_encrypt( + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_ietf_decrypt(unsigned char * m, unsigned long long * mlen_p, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_ietf_decrypt( + @Out byte[] m, + @Out LongLongByReference mlen_p, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_ietf_encrypt_detached(unsigned char * c, unsigned char * mac, unsigned long long * maclen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_ietf_encrypt_detached( + @Out byte[] c, + @Out byte[] mac, + @Out LongLongByReference maclen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_ietf_decrypt_detached(unsigned char * m, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * mac, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_ietf_decrypt_detached( + @Out byte[] m, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] mac, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] npub, + @In byte[] k); + + // void crypto_aead_chacha20poly1305_ietf_keygen(unsigned char[] k); + void crypto_aead_chacha20poly1305_ietf_keygen(@Out byte[] k); + + // size_t crypto_aead_chacha20poly1305_keybytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_keybytes(); + + // size_t crypto_aead_chacha20poly1305_nsecbytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_nsecbytes(); + + // size_t crypto_aead_chacha20poly1305_npubbytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_npubbytes(); + + // size_t crypto_aead_chacha20poly1305_abytes(void); + @ssize_t + long crypto_aead_chacha20poly1305_abytes(); + + // size_t crypto_aead_chacha20poly1305_messagebytes_max(void); + @ssize_t + long crypto_aead_chacha20poly1305_messagebytes_max(); + + // int crypto_aead_chacha20poly1305_encrypt(unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_encrypt( + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_decrypt(unsigned char * m, unsigned long long * mlen_p, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_decrypt( + @Out byte[] m, + @Out LongLongByReference mlen_p, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_encrypt_detached(unsigned char * c, unsigned char * mac, unsigned long long * maclen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_encrypt_detached( + @Out byte[] c, + /*null*/ byte[] mac, + @Out LongLongByReference maclen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In byte[] npub, + @In byte[] k); + + // int crypto_aead_chacha20poly1305_decrypt_detached(unsigned char * m, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * mac, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_chacha20poly1305_decrypt_detached( + @Out byte[] m, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] mac, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] npub, + @In byte[] k); + + // void crypto_aead_chacha20poly1305_keygen(unsigned char[] k); + void crypto_aead_chacha20poly1305_keygen(@Out byte[] k); + + // size_t crypto_aead_xchacha20poly1305_ietf_keybytes(void); + @ssize_t + long crypto_aead_xchacha20poly1305_ietf_keybytes(); + + // size_t crypto_aead_xchacha20poly1305_ietf_nsecbytes(void); + @ssize_t + long crypto_aead_xchacha20poly1305_ietf_nsecbytes(); + + // size_t crypto_aead_xchacha20poly1305_ietf_npubbytes(void); + @ssize_t + long crypto_aead_xchacha20poly1305_ietf_npubbytes(); + + // size_t crypto_aead_xchacha20poly1305_ietf_abytes(void); + @ssize_t + long crypto_aead_xchacha20poly1305_ietf_abytes(); + + // size_t crypto_aead_xchacha20poly1305_ietf_messagebytes_max(void); + @ssize_t + long crypto_aead_xchacha20poly1305_ietf_messagebytes_max(); + + // int crypto_aead_xchacha20poly1305_ietf_encrypt(unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_xchacha20poly1305_ietf_encrypt( + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_xchacha20poly1305_ietf_decrypt(unsigned char * m, unsigned long long * mlen_p, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_xchacha20poly1305_ietf_decrypt( + @Out byte[] m, + @Out LongLongByReference mlen_p, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_xchacha20poly1305_ietf_encrypt_detached(unsigned char * c, unsigned char * mac, unsigned long long * maclen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, const unsigned char * nsec, const unsigned char * npub, const unsigned char * k); + int crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + @Out byte[] c, + /*null*/ byte[] mac, + @Out LongLongByReference maclen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In byte[] nsec, + @In Pointer npub, + @In Pointer k); + + // int crypto_aead_xchacha20poly1305_ietf_decrypt_detached(unsigned char * m, unsigned char * nsec, const unsigned char * c, unsigned long long clen, const unsigned char * mac, const unsigned char * ad, unsigned long long adlen, const unsigned char * npub, const unsigned char * k); + int crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + @Out byte[] m, + /*null*/ byte[] nsec, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] mac, + @In byte[] ad, + @In @u_int64_t long adlen, + @In Pointer npub, + @In Pointer k); + + // void crypto_aead_xchacha20poly1305_ietf_keygen(unsigned char[] k); + void crypto_aead_xchacha20poly1305_ietf_keygen(@Out Pointer k); + + // size_t crypto_hash_sha512_statebytes(void); + @ssize_t + long crypto_hash_sha512_statebytes(); + + // size_t crypto_hash_sha512_bytes(void); + @ssize_t + long crypto_hash_sha512_bytes(); + + // int crypto_hash_sha512(unsigned char * out, const unsigned char * in, unsigned long long inlen); + int crypto_hash_sha512(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_hash_sha512_init(crypto_hash_sha512_state * state); + int crypto_hash_sha512_init(@Out Pointer state); + + // int crypto_hash_sha512_update(crypto_hash_sha512_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_hash_sha512_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_hash_sha512_final(crypto_hash_sha512_state * state, unsigned char * out); + int crypto_hash_sha512_final(/*both*/ Pointer state, @Out byte[] out); + + // size_t crypto_auth_hmacsha512_bytes(void); + @ssize_t + long crypto_auth_hmacsha512_bytes(); + + // size_t crypto_auth_hmacsha512_keybytes(void); + @ssize_t + long crypto_auth_hmacsha512_keybytes(); + + // int crypto_auth_hmacsha512(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha512(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_auth_hmacsha512_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha512_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // size_t crypto_auth_hmacsha512_statebytes(void); + @ssize_t + long crypto_auth_hmacsha512_statebytes(); + + // int crypto_auth_hmacsha512_init(crypto_auth_hmacsha512_state * state, const unsigned char * key, size_t keylen); + int crypto_auth_hmacsha512_init(@Out Pointer state, @In byte[] key, @In @ssize_t long keylen); + + // int crypto_auth_hmacsha512_update(crypto_auth_hmacsha512_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_auth_hmacsha512_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_auth_hmacsha512_final(crypto_auth_hmacsha512_state * state, unsigned char * out); + int crypto_auth_hmacsha512_final(/*both*/ Pointer state, @Out byte[] out); + + // void crypto_auth_hmacsha512_keygen(unsigned char[] k); + void crypto_auth_hmacsha512_keygen(@Out byte[] k); + + // size_t crypto_auth_hmacsha512256_bytes(void); + @ssize_t + long crypto_auth_hmacsha512256_bytes(); + + // size_t crypto_auth_hmacsha512256_keybytes(void); + @ssize_t + long crypto_auth_hmacsha512256_keybytes(); + + // int crypto_auth_hmacsha512256(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha512256(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_auth_hmacsha512256_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha512256_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // size_t crypto_auth_hmacsha512256_statebytes(void); + @ssize_t + long crypto_auth_hmacsha512256_statebytes(); + + // int crypto_auth_hmacsha512256_init(crypto_auth_hmacsha512256_state * state, const unsigned char * key, size_t keylen); + int crypto_auth_hmacsha512256_init(@Out Pointer state, @In byte[] key, @In @ssize_t long keylen); + + // int crypto_auth_hmacsha512256_update(crypto_auth_hmacsha512256_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_auth_hmacsha512256_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_auth_hmacsha512256_final(crypto_auth_hmacsha512256_state * state, unsigned char * out); + int crypto_auth_hmacsha512256_final(/*both*/ Pointer state, @Out byte[] out); + + // void crypto_auth_hmacsha512256_keygen(unsigned char[] k); + void crypto_auth_hmacsha512256_keygen(@Out byte[] k); + + // size_t crypto_auth_bytes(void); + @ssize_t + long crypto_auth_bytes(); + + // size_t crypto_auth_keybytes(void); + @ssize_t + long crypto_auth_keybytes(); + + // const char * crypto_auth_primitive(void); + String crypto_auth_primitive(); + + // int crypto_auth(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In Pointer k); + + // int crypto_auth_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In Pointer k); + + // void crypto_auth_keygen(unsigned char[] k); + void crypto_auth_keygen(@Out Pointer k); + + // size_t crypto_hash_sha256_statebytes(void); + @ssize_t + long crypto_hash_sha256_statebytes(); + + // size_t crypto_hash_sha256_bytes(void); + @ssize_t + long crypto_hash_sha256_bytes(); + + // int crypto_hash_sha256(unsigned char * out, const unsigned char * in, unsigned long long inlen); + int crypto_hash_sha256(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_hash_sha256_init(crypto_hash_sha256_state * state); + int crypto_hash_sha256_init(@Out Pointer state); + + // int crypto_hash_sha256_update(crypto_hash_sha256_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_hash_sha256_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_hash_sha256_final(crypto_hash_sha256_state * state, unsigned char * out); + int crypto_hash_sha256_final(/*both*/ Pointer state, @Out byte[] out); + + // size_t crypto_auth_hmacsha256_bytes(void); + @ssize_t + long crypto_auth_hmacsha256_bytes(); + + // size_t crypto_auth_hmacsha256_keybytes(void); + @ssize_t + long crypto_auth_hmacsha256_keybytes(); + + // int crypto_auth_hmacsha256(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha256(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_auth_hmacsha256_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_auth_hmacsha256_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // size_t crypto_auth_hmacsha256_statebytes(void); + @ssize_t + long crypto_auth_hmacsha256_statebytes(); + + // int crypto_auth_hmacsha256_init(crypto_auth_hmacsha256_state * state, const unsigned char * key, size_t keylen); + int crypto_auth_hmacsha256_init(@Out Pointer state, @In byte[] key, @In @ssize_t long keylen); + + // int crypto_auth_hmacsha256_update(crypto_auth_hmacsha256_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_auth_hmacsha256_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_auth_hmacsha256_final(crypto_auth_hmacsha256_state * state, unsigned char * out); + int crypto_auth_hmacsha256_final(/*both*/ Pointer state, @Out byte[] out); + + // void crypto_auth_hmacsha256_keygen(unsigned char[] k); + void crypto_auth_hmacsha256_keygen(@Out byte[] k); + + // size_t crypto_stream_xsalsa20_keybytes(void); + @ssize_t + long crypto_stream_xsalsa20_keybytes(); + + // size_t crypto_stream_xsalsa20_noncebytes(void); + @ssize_t + long crypto_stream_xsalsa20_noncebytes(); + + // size_t crypto_stream_xsalsa20_messagebytes_max(void); + @ssize_t + long crypto_stream_xsalsa20_messagebytes_max(); + + // int crypto_stream_xsalsa20(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_xsalsa20(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_xsalsa20_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_xsalsa20_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // int crypto_stream_xsalsa20_xor_ic(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, uint64_t ic, const unsigned char * k); + int crypto_stream_xsalsa20_xor_ic( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In @u_int64_t long ic, + @In byte[] k); + + // void crypto_stream_xsalsa20_keygen(unsigned char[] k); + void crypto_stream_xsalsa20_keygen(@Out byte[] k); + + // size_t crypto_box_curve25519xsalsa20poly1305_seedbytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_seedbytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_publickeybytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_publickeybytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_secretkeybytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_secretkeybytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_beforenmbytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_beforenmbytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_noncebytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_noncebytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_macbytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_macbytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_messagebytes_max(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_messagebytes_max(); + + // int crypto_box_curve25519xsalsa20poly1305_seed_keypair(unsigned char * pk, unsigned char * sk, const unsigned char * seed); + int crypto_box_curve25519xsalsa20poly1305_seed_keypair(@Out byte[] pk, @Out byte[] sk, @In byte[] seed); + + // int crypto_box_curve25519xsalsa20poly1305_keypair(unsigned char * pk, unsigned char * sk); + int crypto_box_curve25519xsalsa20poly1305_keypair(@Out byte[] pk, @Out byte[] sk); + + // int crypto_box_curve25519xsalsa20poly1305_beforenm(unsigned char * k, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xsalsa20poly1305_beforenm(@Out Pointer k, @In byte[] pk, @In byte[] sk); + + // size_t crypto_box_curve25519xsalsa20poly1305_boxzerobytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_boxzerobytes(); + + // size_t crypto_box_curve25519xsalsa20poly1305_zerobytes(void); + @ssize_t + long crypto_box_curve25519xsalsa20poly1305_zerobytes(); + + // int crypto_box_curve25519xsalsa20poly1305(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xsalsa20poly1305( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xsalsa20poly1305_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xsalsa20poly1305_open( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xsalsa20poly1305_afternm(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xsalsa20poly1305_afternm( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In Pointer k); + + // int crypto_box_curve25519xsalsa20poly1305_open_afternm(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xsalsa20poly1305_open_afternm( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In Pointer k); + + // size_t crypto_box_seedbytes(void); + @ssize_t + long crypto_box_seedbytes(); + + // size_t crypto_box_publickeybytes(void); + @ssize_t + long crypto_box_publickeybytes(); + + // size_t crypto_box_secretkeybytes(void); + @ssize_t + long crypto_box_secretkeybytes(); + + // size_t crypto_box_noncebytes(void); + @ssize_t + long crypto_box_noncebytes(); + + // size_t crypto_box_macbytes(void); + @ssize_t + long crypto_box_macbytes(); + + // size_t crypto_box_messagebytes_max(void); + @ssize_t + long crypto_box_messagebytes_max(); + + // const char * crypto_box_primitive(void); + String crypto_box_primitive(); + + // int crypto_box_seed_keypair(unsigned char * pk, unsigned char * sk, const unsigned char * seed); + int crypto_box_seed_keypair(@Out Pointer pk, @Out Pointer sk, @In Pointer seed); + + // int crypto_box_keypair(unsigned char * pk, unsigned char * sk); + int crypto_box_keypair(@Out Pointer pk, @Out Pointer sk); + + // int crypto_box_easy(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_easy( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In Pointer n, + @In Pointer pk, + @In Pointer sk); + + // int crypto_box_open_easy(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_open_easy( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In Pointer n, + @In Pointer pk, + @In Pointer sk); + + // int crypto_box_detached(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_detached( + @Out byte[] c, + @Out byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In Pointer n, + @In Pointer pk, + @In Pointer sk); + + // int crypto_box_open_detached(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_open_detached( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In Pointer n, + @In Pointer pk, + @In Pointer sk); + + // size_t crypto_box_beforenmbytes(void); + @ssize_t + long crypto_box_beforenmbytes(); + + // int crypto_box_beforenm(unsigned char * k, const unsigned char * pk, const unsigned char * sk); + int crypto_box_beforenm(@Out Pointer k, @In Pointer pk, @In Pointer sk); + + // int crypto_box_easy_afternm(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_easy_afternm(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In Pointer n, @In Pointer k); + + // int crypto_box_open_easy_afternm(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_open_easy_afternm(@Out byte[] m, @In byte[] c, @In @u_int64_t long clen, @In Pointer n, @In Pointer k); + + // int crypto_box_detached_afternm(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_detached_afternm( + @Out byte[] c, + @Out byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In Pointer n, + @In Pointer k); + + // int crypto_box_open_detached_afternm(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_open_detached_afternm( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In Pointer n, + @In Pointer k); + + // size_t crypto_box_sealbytes(void); + @ssize_t + long crypto_box_sealbytes(); + + // int crypto_box_seal(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * pk); + int crypto_box_seal(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In Pointer pk); + + // int crypto_box_seal_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * pk, const unsigned char * sk); + int crypto_box_seal_open(@Out byte[] m, @In byte[] c, @In @u_int64_t long clen, @In Pointer pk, @In Pointer sk); + + // size_t crypto_box_zerobytes(void); + @ssize_t + long crypto_box_zerobytes(); + + // size_t crypto_box_boxzerobytes(void); + @ssize_t + long crypto_box_boxzerobytes(); + + // int crypto_box(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] pk, @In byte[] sk); + + // int crypto_box_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_open( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_afternm(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_afternm(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In Pointer k); + + // int crypto_box_open_afternm(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_open_afternm(@Out byte[] m, @In byte[] c, @In @u_int64_t long clen, @In byte[] n, @In Pointer k); + + // size_t crypto_core_hsalsa20_outputbytes(void); + @ssize_t + long crypto_core_hsalsa20_outputbytes(); + + // size_t crypto_core_hsalsa20_inputbytes(void); + @ssize_t + long crypto_core_hsalsa20_inputbytes(); + + // size_t crypto_core_hsalsa20_keybytes(void); + @ssize_t + long crypto_core_hsalsa20_keybytes(); + + // size_t crypto_core_hsalsa20_constbytes(void); + @ssize_t + long crypto_core_hsalsa20_constbytes(); + + // int crypto_core_hsalsa20(unsigned char * out, const unsigned char * in, const unsigned char * k, const unsigned char * c); + int crypto_core_hsalsa20(@Out byte[] out, @In byte[] in, @In byte[] k, @In byte[] c); + + // size_t crypto_core_hchacha20_outputbytes(void); + @ssize_t + long crypto_core_hchacha20_outputbytes(); + + // size_t crypto_core_hchacha20_inputbytes(void); + @ssize_t + long crypto_core_hchacha20_inputbytes(); + + // size_t crypto_core_hchacha20_keybytes(void); + @ssize_t + long crypto_core_hchacha20_keybytes(); + + // size_t crypto_core_hchacha20_constbytes(void); + @ssize_t + long crypto_core_hchacha20_constbytes(); + + // int crypto_core_hchacha20(unsigned char * out, const unsigned char * in, const unsigned char * k, const unsigned char * c); + int crypto_core_hchacha20(@Out byte[] out, @In byte[] in, @In byte[] k, @In byte[] c); + + // size_t crypto_core_salsa20_outputbytes(void); + @ssize_t + long crypto_core_salsa20_outputbytes(); + + // size_t crypto_core_salsa20_inputbytes(void); + @ssize_t + long crypto_core_salsa20_inputbytes(); + + // size_t crypto_core_salsa20_keybytes(void); + @ssize_t + long crypto_core_salsa20_keybytes(); + + // size_t crypto_core_salsa20_constbytes(void); + @ssize_t + long crypto_core_salsa20_constbytes(); + + // int crypto_core_salsa20(unsigned char * out, const unsigned char * in, const unsigned char * k, const unsigned char * c); + int crypto_core_salsa20(@Out byte[] out, @In byte[] in, @In byte[] k, @In byte[] c); + + // size_t crypto_core_salsa2012_outputbytes(void); + @ssize_t + long crypto_core_salsa2012_outputbytes(); + + // size_t crypto_core_salsa2012_inputbytes(void); + @ssize_t + long crypto_core_salsa2012_inputbytes(); + + // size_t crypto_core_salsa2012_keybytes(void); + @ssize_t + long crypto_core_salsa2012_keybytes(); + + // size_t crypto_core_salsa2012_constbytes(void); + @ssize_t + long crypto_core_salsa2012_constbytes(); + + // int crypto_core_salsa2012(unsigned char * out, const unsigned char * in, const unsigned char * k, const unsigned char * c); + int crypto_core_salsa2012(@Out byte[] out, @In byte[] in, @In byte[] k, @In byte[] c); + + // size_t crypto_core_salsa208_outputbytes(void); + @ssize_t + long crypto_core_salsa208_outputbytes(); + + // size_t crypto_core_salsa208_inputbytes(void); + @ssize_t + long crypto_core_salsa208_inputbytes(); + + // size_t crypto_core_salsa208_keybytes(void); + @ssize_t + long crypto_core_salsa208_keybytes(); + + // size_t crypto_core_salsa208_constbytes(void); + @ssize_t + long crypto_core_salsa208_constbytes(); + + // int crypto_core_salsa208(unsigned char * out, const unsigned char * in, const unsigned char * k, const unsigned char * c); + int crypto_core_salsa208(@Out byte[] out, @In byte[] in, @In byte[] k, @In byte[] c); + + // size_t crypto_generichash_blake2b_bytes_min(void); + @ssize_t + long crypto_generichash_blake2b_bytes_min(); + + // size_t crypto_generichash_blake2b_bytes_max(void); + @ssize_t + long crypto_generichash_blake2b_bytes_max(); + + // size_t crypto_generichash_blake2b_bytes(void); + @ssize_t + long crypto_generichash_blake2b_bytes(); + + // size_t crypto_generichash_blake2b_keybytes_min(void); + @ssize_t + long crypto_generichash_blake2b_keybytes_min(); + + // size_t crypto_generichash_blake2b_keybytes_max(void); + @ssize_t + long crypto_generichash_blake2b_keybytes_max(); + + // size_t crypto_generichash_blake2b_keybytes(void); + @ssize_t + long crypto_generichash_blake2b_keybytes(); + + // size_t crypto_generichash_blake2b_saltbytes(void); + @ssize_t + long crypto_generichash_blake2b_saltbytes(); + + // size_t crypto_generichash_blake2b_personalbytes(void); + @ssize_t + long crypto_generichash_blake2b_personalbytes(); + + // size_t crypto_generichash_blake2b_statebytes(void); + @ssize_t + long crypto_generichash_blake2b_statebytes(); + + // int crypto_generichash_blake2b(unsigned char * out, size_t outlen, const unsigned char * in, unsigned long long inlen, const unsigned char * key, size_t keylen); + int crypto_generichash_blake2b( + @Out byte[] out, + @In @ssize_t long outlen, + @In byte[] in, + @In @u_int64_t long inlen, + @In byte[] key, + @In @ssize_t long keylen); + + // int crypto_generichash_blake2b_salt_personal(unsigned char * out, size_t outlen, const unsigned char * in, unsigned long long inlen, const unsigned char * key, size_t keylen, const unsigned char * salt, const unsigned char * personal); + int crypto_generichash_blake2b_salt_personal( + @Out byte[] out, + @In @ssize_t long outlen, + @In byte[] in, + @In @u_int64_t long inlen, + @In byte[] key, + @In @ssize_t long keylen, + @In byte[] salt, + @In byte[] personal); + + // int crypto_generichash_blake2b_init(crypto_generichash_blake2b_state * state, const unsigned char * key, const size_t keylen, const size_t outlen); + int crypto_generichash_blake2b_init( + @Out Pointer state, + @In byte[] key, + @In @ssize_t long keylen, + @In @ssize_t long outlen); + + // int crypto_generichash_blake2b_init_salt_personal(crypto_generichash_blake2b_state * state, const unsigned char * key, const size_t keylen, const size_t outlen, const unsigned char * salt, const unsigned char * personal); + int crypto_generichash_blake2b_init_salt_personal( + /*both*/ Pointer state, + @In byte[] key, + @In @ssize_t long keylen, + @In @ssize_t long outlen, + @In byte[] salt, + @In byte[] personal); + + // int crypto_generichash_blake2b_update(crypto_generichash_blake2b_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_generichash_blake2b_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_generichash_blake2b_final(crypto_generichash_blake2b_state * state, unsigned char * out, const size_t outlen); + int crypto_generichash_blake2b_final(/*both*/ Pointer state, @Out byte[] out, @In @ssize_t long outlen); + + // void crypto_generichash_blake2b_keygen(unsigned char[] k); + void crypto_generichash_blake2b_keygen(@Out byte[] k); + + // size_t crypto_generichash_bytes_min(void); + @ssize_t + long crypto_generichash_bytes_min(); + + // size_t crypto_generichash_bytes_max(void); + @ssize_t + long crypto_generichash_bytes_max(); + + // size_t crypto_generichash_bytes(void); + @ssize_t + long crypto_generichash_bytes(); + + // size_t crypto_generichash_keybytes_min(void); + @ssize_t + long crypto_generichash_keybytes_min(); + + // size_t crypto_generichash_keybytes_max(void); + @ssize_t + long crypto_generichash_keybytes_max(); + + // size_t crypto_generichash_keybytes(void); + @ssize_t + long crypto_generichash_keybytes(); + + // const char * crypto_generichash_primitive(void); + String crypto_generichash_primitive(); + + // size_t crypto_generichash_statebytes(void); + @ssize_t + long crypto_generichash_statebytes(); + + // int crypto_generichash(unsigned char * out, size_t outlen, const unsigned char * in, unsigned long long inlen, const unsigned char * key, size_t keylen); + int crypto_generichash( + @Out byte[] out, + @In @ssize_t long outlen, + @In byte[] in, + @In @u_int64_t long inlen, + @In byte[] key, + @In @ssize_t long keylen); + + // int crypto_generichash_init(crypto_generichash_state * state, const unsigned char * key, const size_t keylen, const size_t outlen); + int crypto_generichash_init(@Out Pointer state, @In byte[] key, @In @ssize_t long keylen, @In @ssize_t long outlen); + + // int crypto_generichash_update(crypto_generichash_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_generichash_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_generichash_final(crypto_generichash_state * state, unsigned char * out, const size_t outlen); + int crypto_generichash_final(/*both*/ Pointer state, @Out byte[] out, @In @ssize_t long outlen); + + // void crypto_generichash_keygen(unsigned char[] k); + void crypto_generichash_keygen(@Out byte[] k); + + // size_t crypto_hash_bytes(void); + @ssize_t + long crypto_hash_bytes(); + + // int crypto_hash(unsigned char * out, const unsigned char * in, unsigned long long inlen); + int crypto_hash(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen); + + // const char * crypto_hash_primitive(void); + String crypto_hash_primitive(); + + // size_t crypto_kdf_blake2b_bytes_min(void); + @ssize_t + long crypto_kdf_blake2b_bytes_min(); + + // size_t crypto_kdf_blake2b_bytes_max(void); + @ssize_t + long crypto_kdf_blake2b_bytes_max(); + + // size_t crypto_kdf_blake2b_contextbytes(void); + @ssize_t + long crypto_kdf_blake2b_contextbytes(); + + // size_t crypto_kdf_blake2b_keybytes(void); + @ssize_t + long crypto_kdf_blake2b_keybytes(); + + // int crypto_kdf_blake2b_derive_from_key(unsigned char * subkey, size_t subkey_len, uint64_t subkey_id, const char[] ctx, const unsigned char[] key); + int crypto_kdf_blake2b_derive_from_key( + @Out byte[] subkey, + @In @ssize_t long subkey_len, + @In @u_int64_t long subkey_id, + @In byte[] ctx, + @In Pointer key); + + // size_t crypto_kdf_bytes_min(void); + @ssize_t + long crypto_kdf_bytes_min(); + + // size_t crypto_kdf_bytes_max(void); + @ssize_t + long crypto_kdf_bytes_max(); + + // size_t crypto_kdf_contextbytes(void); + @ssize_t + long crypto_kdf_contextbytes(); + + // size_t crypto_kdf_keybytes(void); + @ssize_t + long crypto_kdf_keybytes(); + + // const char * crypto_kdf_primitive(void); + String crypto_kdf_primitive(); + + // int crypto_kdf_derive_from_key(unsigned char * subkey, size_t subkey_len, uint64_t subkey_id, const char[] ctx, const unsigned char[] key); + int crypto_kdf_derive_from_key( + @Out byte[] subkey, + @In @ssize_t long subkey_len, + @In @u_int64_t long subkey_id, + @In byte[] ctx, + @In Pointer key); + + // void crypto_kdf_keygen(unsigned char[] k); + void crypto_kdf_keygen(@Out Pointer k); + + // size_t crypto_kx_publickeybytes(void); + @ssize_t + long crypto_kx_publickeybytes(); + + // size_t crypto_kx_secretkeybytes(void); + @ssize_t + long crypto_kx_secretkeybytes(); + + // size_t crypto_kx_seedbytes(void); + @ssize_t + long crypto_kx_seedbytes(); + + // size_t crypto_kx_sessionkeybytes(void); + @ssize_t + long crypto_kx_sessionkeybytes(); + + // const char * crypto_kx_primitive(void); + String crypto_kx_primitive(); + + // int crypto_kx_seed_keypair(unsigned char[] pk, unsigned char[] sk, const unsigned char[] seed); + int crypto_kx_seed_keypair(@Out Pointer pk, @Out Pointer sk, @In Pointer seed); + + // int crypto_kx_keypair(unsigned char[] pk, unsigned char[] sk); + int crypto_kx_keypair(@Out Pointer pk, @Out Pointer sk); + + // int crypto_kx_client_session_keys(unsigned char[] rx, unsigned char[] tx, const unsigned char[] client_pk, const unsigned char[] client_sk, const unsigned char[] server_pk); + int crypto_kx_client_session_keys( + @Out Pointer rx, + @Out Pointer tx, + @In Pointer client_pk, + @In Pointer client_sk, + @In Pointer server_pk); + + // int crypto_kx_server_session_keys(unsigned char[] rx, unsigned char[] tx, const unsigned char[] server_pk, const unsigned char[] server_sk, const unsigned char[] client_pk); + int crypto_kx_server_session_keys( + @Out Pointer rx, + @Out Pointer tx, + @In Pointer server_pk, + @In Pointer server_sk, + @In Pointer client_pk); + + // size_t crypto_onetimeauth_poly1305_statebytes(void); + @ssize_t + long crypto_onetimeauth_poly1305_statebytes(); + + // size_t crypto_onetimeauth_poly1305_bytes(void); + @ssize_t + long crypto_onetimeauth_poly1305_bytes(); + + // size_t crypto_onetimeauth_poly1305_keybytes(void); + @ssize_t + long crypto_onetimeauth_poly1305_keybytes(); + + // int crypto_onetimeauth_poly1305(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_onetimeauth_poly1305(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_onetimeauth_poly1305_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_onetimeauth_poly1305_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_onetimeauth_poly1305_init(crypto_onetimeauth_poly1305_state * state, const unsigned char * key); + int crypto_onetimeauth_poly1305_init(@Out Pointer state, @In byte[] key); + + // int crypto_onetimeauth_poly1305_update(crypto_onetimeauth_poly1305_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_onetimeauth_poly1305_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_onetimeauth_poly1305_final(crypto_onetimeauth_poly1305_state * state, unsigned char * out); + int crypto_onetimeauth_poly1305_final(/*both*/ Pointer state, @Out byte[] out); + + // void crypto_onetimeauth_poly1305_keygen(unsigned char[] k); + void crypto_onetimeauth_poly1305_keygen(@Out byte[] k); + + // size_t crypto_onetimeauth_statebytes(void); + @ssize_t + long crypto_onetimeauth_statebytes(); + + // size_t crypto_onetimeauth_bytes(void); + @ssize_t + long crypto_onetimeauth_bytes(); + + // size_t crypto_onetimeauth_keybytes(void); + @ssize_t + long crypto_onetimeauth_keybytes(); + + // const char * crypto_onetimeauth_primitive(void); + String crypto_onetimeauth_primitive(); + + // int crypto_onetimeauth(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_onetimeauth(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_onetimeauth_verify(const unsigned char * h, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_onetimeauth_verify(@In byte[] h, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // int crypto_onetimeauth_init(crypto_onetimeauth_state * state, const unsigned char * key); + int crypto_onetimeauth_init(@Out Pointer state, @In byte[] key); + + // int crypto_onetimeauth_update(crypto_onetimeauth_state * state, const unsigned char * in, unsigned long long inlen); + int crypto_onetimeauth_update(/*both*/ Pointer state, @In byte[] in, @In @u_int64_t long inlen); + + // int crypto_onetimeauth_final(crypto_onetimeauth_state * state, unsigned char * out); + int crypto_onetimeauth_final(/*both*/ Pointer state, @Out byte[] out); + + // void crypto_onetimeauth_keygen(unsigned char[] k); + void crypto_onetimeauth_keygen(@Out byte[] k); + + // int crypto_pwhash_argon2i_alg_argon2i13(void); + int crypto_pwhash_argon2i_alg_argon2i13(); + + // size_t crypto_pwhash_argon2i_bytes_min(void); + @ssize_t + long crypto_pwhash_argon2i_bytes_min(); + + // size_t crypto_pwhash_argon2i_bytes_max(void); + @ssize_t + long crypto_pwhash_argon2i_bytes_max(); + + // size_t crypto_pwhash_argon2i_passwd_min(void); + @ssize_t + long crypto_pwhash_argon2i_passwd_min(); + + // size_t crypto_pwhash_argon2i_passwd_max(void); + @ssize_t + long crypto_pwhash_argon2i_passwd_max(); + + // size_t crypto_pwhash_argon2i_saltbytes(void); + @ssize_t + long crypto_pwhash_argon2i_saltbytes(); + + // size_t crypto_pwhash_argon2i_strbytes(void); + @ssize_t + long crypto_pwhash_argon2i_strbytes(); + + // const char * crypto_pwhash_argon2i_strprefix(void); + String crypto_pwhash_argon2i_strprefix(); + + // size_t crypto_pwhash_argon2i_opslimit_min(void); + @ssize_t + long crypto_pwhash_argon2i_opslimit_min(); + + // size_t crypto_pwhash_argon2i_opslimit_max(void); + @ssize_t + long crypto_pwhash_argon2i_opslimit_max(); + + // size_t crypto_pwhash_argon2i_memlimit_min(void); + @ssize_t + long crypto_pwhash_argon2i_memlimit_min(); + + // size_t crypto_pwhash_argon2i_memlimit_max(void); + @ssize_t + long crypto_pwhash_argon2i_memlimit_max(); + + // size_t crypto_pwhash_argon2i_opslimit_interactive(void); + @ssize_t + long crypto_pwhash_argon2i_opslimit_interactive(); + + // size_t crypto_pwhash_argon2i_memlimit_interactive(void); + @ssize_t + long crypto_pwhash_argon2i_memlimit_interactive(); + + // size_t crypto_pwhash_argon2i_opslimit_moderate(void); + @ssize_t + long crypto_pwhash_argon2i_opslimit_moderate(); + + // size_t crypto_pwhash_argon2i_memlimit_moderate(void); + @ssize_t + long crypto_pwhash_argon2i_memlimit_moderate(); + + // size_t crypto_pwhash_argon2i_opslimit_sensitive(void); + @ssize_t + long crypto_pwhash_argon2i_opslimit_sensitive(); + + // size_t crypto_pwhash_argon2i_memlimit_sensitive(void); + @ssize_t + long crypto_pwhash_argon2i_memlimit_sensitive(); + + // int crypto_pwhash_argon2i(unsigned char *const out, unsigned long long outlen, const char *const passwd, unsigned long long passwdlen, const unsigned char *const salt, unsigned long long opslimit, size_t memlimit, int alg); + int crypto_pwhash_argon2i( + @Out byte[] out, + @In @u_int64_t long outlen, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In byte[] salt, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit, + @In int alg); + + // int crypto_pwhash_argon2i_str(char[] out, const char *const passwd, unsigned long long passwdlen, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_argon2i_str( + @Out byte[] out, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // int crypto_pwhash_argon2i_str_verify(const char[] str, const char *const passwd, unsigned long long passwdlen); + int crypto_pwhash_argon2i_str_verify(@In byte[] str, @In byte[] passwd, @In @u_int64_t long passwdlen); + + // int crypto_pwhash_argon2i_str_needs_rehash(const char[] str, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_argon2i_str_needs_rehash(@In byte[] str, @In @u_int64_t long opslimit, @In @ssize_t long memlimit); + + // int crypto_pwhash_argon2id_alg_argon2id13(void); + int crypto_pwhash_argon2id_alg_argon2id13(); + + // size_t crypto_pwhash_argon2id_bytes_min(void); + @ssize_t + long crypto_pwhash_argon2id_bytes_min(); + + // size_t crypto_pwhash_argon2id_bytes_max(void); + @ssize_t + long crypto_pwhash_argon2id_bytes_max(); + + // size_t crypto_pwhash_argon2id_passwd_min(void); + @ssize_t + long crypto_pwhash_argon2id_passwd_min(); + + // size_t crypto_pwhash_argon2id_passwd_max(void); + @ssize_t + long crypto_pwhash_argon2id_passwd_max(); + + // size_t crypto_pwhash_argon2id_saltbytes(void); + @ssize_t + long crypto_pwhash_argon2id_saltbytes(); + + // size_t crypto_pwhash_argon2id_strbytes(void); + @ssize_t + long crypto_pwhash_argon2id_strbytes(); + + // const char * crypto_pwhash_argon2id_strprefix(void); + String crypto_pwhash_argon2id_strprefix(); + + // size_t crypto_pwhash_argon2id_opslimit_min(void); + @ssize_t + long crypto_pwhash_argon2id_opslimit_min(); + + // size_t crypto_pwhash_argon2id_opslimit_max(void); + @ssize_t + long crypto_pwhash_argon2id_opslimit_max(); + + // size_t crypto_pwhash_argon2id_memlimit_min(void); + @ssize_t + long crypto_pwhash_argon2id_memlimit_min(); + + // size_t crypto_pwhash_argon2id_memlimit_max(void); + @ssize_t + long crypto_pwhash_argon2id_memlimit_max(); + + // size_t crypto_pwhash_argon2id_opslimit_interactive(void); + @ssize_t + long crypto_pwhash_argon2id_opslimit_interactive(); + + // size_t crypto_pwhash_argon2id_memlimit_interactive(void); + @ssize_t + long crypto_pwhash_argon2id_memlimit_interactive(); + + // size_t crypto_pwhash_argon2id_opslimit_moderate(void); + @ssize_t + long crypto_pwhash_argon2id_opslimit_moderate(); + + // size_t crypto_pwhash_argon2id_memlimit_moderate(void); + @ssize_t + long crypto_pwhash_argon2id_memlimit_moderate(); + + // size_t crypto_pwhash_argon2id_opslimit_sensitive(void); + @ssize_t + long crypto_pwhash_argon2id_opslimit_sensitive(); + + // size_t crypto_pwhash_argon2id_memlimit_sensitive(void); + @ssize_t + long crypto_pwhash_argon2id_memlimit_sensitive(); + + // int crypto_pwhash_argon2id(unsigned char *const out, unsigned long long outlen, const char *const passwd, unsigned long long passwdlen, const unsigned char *const salt, unsigned long long opslimit, size_t memlimit, int alg); + int crypto_pwhash_argon2id( + @Out byte[] out, + @In @u_int64_t long outlen, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In byte[] salt, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit, + @In int alg); + + // int crypto_pwhash_argon2id_str(char[] out, const char *const passwd, unsigned long long passwdlen, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_argon2id_str( + @Out byte[] out, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // int crypto_pwhash_argon2id_str_verify(const char[] str, const char *const passwd, unsigned long long passwdlen); + int crypto_pwhash_argon2id_str_verify(@In byte[] str, @In byte[] passwd, @In @u_int64_t long passwdlen); + + // int crypto_pwhash_argon2id_str_needs_rehash(const char[] str, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_argon2id_str_needs_rehash(@In byte[] str, @In @u_int64_t long opslimit, @In @ssize_t long memlimit); + + // int crypto_pwhash_alg_argon2i13(void); + int crypto_pwhash_alg_argon2i13(); + + // int crypto_pwhash_alg_argon2id13(void); + int crypto_pwhash_alg_argon2id13(); + + // int crypto_pwhash_alg_default(void); + int crypto_pwhash_alg_default(); + + // size_t crypto_pwhash_bytes_min(void); + @ssize_t + long crypto_pwhash_bytes_min(); + + // size_t crypto_pwhash_bytes_max(void); + @ssize_t + long crypto_pwhash_bytes_max(); + + // size_t crypto_pwhash_passwd_min(void); + @ssize_t + long crypto_pwhash_passwd_min(); + + // size_t crypto_pwhash_passwd_max(void); + @ssize_t + long crypto_pwhash_passwd_max(); + + // size_t crypto_pwhash_saltbytes(void); + @ssize_t + long crypto_pwhash_saltbytes(); + + // size_t crypto_pwhash_strbytes(void); + @ssize_t + long crypto_pwhash_strbytes(); + + // const char * crypto_pwhash_strprefix(void); + String crypto_pwhash_strprefix(); + + // size_t crypto_pwhash_opslimit_min(void); + @ssize_t + long crypto_pwhash_opslimit_min(); + + // size_t crypto_pwhash_opslimit_max(void); + @ssize_t + long crypto_pwhash_opslimit_max(); + + // size_t crypto_pwhash_memlimit_min(void); + @ssize_t + long crypto_pwhash_memlimit_min(); + + // size_t crypto_pwhash_memlimit_max(void); + @ssize_t + long crypto_pwhash_memlimit_max(); + + // size_t crypto_pwhash_opslimit_interactive(void); + @ssize_t + long crypto_pwhash_opslimit_interactive(); + + // size_t crypto_pwhash_memlimit_interactive(void); + @ssize_t + long crypto_pwhash_memlimit_interactive(); + + // size_t crypto_pwhash_opslimit_moderate(void); + @ssize_t + long crypto_pwhash_opslimit_moderate(); + + // size_t crypto_pwhash_memlimit_moderate(void); + @ssize_t + long crypto_pwhash_memlimit_moderate(); + + // size_t crypto_pwhash_opslimit_sensitive(void); + @ssize_t + long crypto_pwhash_opslimit_sensitive(); + + // size_t crypto_pwhash_memlimit_sensitive(void); + @ssize_t + long crypto_pwhash_memlimit_sensitive(); + + // int crypto_pwhash(unsigned char *const out, unsigned long long outlen, const char *const passwd, unsigned long long passwdlen, const unsigned char *const salt, unsigned long long opslimit, size_t memlimit, int alg); + int crypto_pwhash( + @Out byte[] out, + @In @u_int64_t long outlen, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In Pointer salt, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit, + @In int alg); + + // int crypto_pwhash_str(char[] out, const char *const passwd, unsigned long long passwdlen, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_str( + @Out byte[] out, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // int crypto_pwhash_str_alg(char[] out, const char *const passwd, unsigned long long passwdlen, unsigned long long opslimit, size_t memlimit, int alg); + int crypto_pwhash_str_alg( + @Out byte[] out, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit, + @In int alg); + + // int crypto_pwhash_str_verify(const char[] str, const char *const passwd, unsigned long long passwdlen); + int crypto_pwhash_str_verify(@In Pointer str, @In byte[] passwd, @In @u_int64_t long passwdlen); + + // int crypto_pwhash_str_needs_rehash(const char[] str, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_str_needs_rehash(@In Pointer str, @In @u_int64_t long opslimit, @In @ssize_t long memlimit); + + // const char * crypto_pwhash_primitive(void); + String crypto_pwhash_primitive(); + + // size_t crypto_scalarmult_curve25519_bytes(void); + @ssize_t + long crypto_scalarmult_curve25519_bytes(); + + // size_t crypto_scalarmult_curve25519_scalarbytes(void); + @ssize_t + long crypto_scalarmult_curve25519_scalarbytes(); + + // int crypto_scalarmult_curve25519(unsigned char * q, const unsigned char * n, const unsigned char * p); + int crypto_scalarmult_curve25519(@Out byte[] q, @In byte[] n, @In byte[] p); + + // int crypto_scalarmult_curve25519_base(unsigned char * q, const unsigned char * n); + int crypto_scalarmult_curve25519_base(@Out byte[] q, @In byte[] n); + + // size_t crypto_scalarmult_bytes(void); + @ssize_t + long crypto_scalarmult_bytes(); + + // size_t crypto_scalarmult_scalarbytes(void); + @ssize_t + long crypto_scalarmult_scalarbytes(); + + // const char * crypto_scalarmult_primitive(void); + String crypto_scalarmult_primitive(); + + // int crypto_scalarmult_base(unsigned char * q, const unsigned char * n); + int crypto_scalarmult_base(@Out Pointer q, @In Pointer n); + + // int crypto_scalarmult(unsigned char * q, const unsigned char * n, const unsigned char * p); + int crypto_scalarmult(@Out byte[] q, @In byte[] n, @In byte[] p); + + // size_t crypto_secretbox_xsalsa20poly1305_keybytes(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_keybytes(); + + // size_t crypto_secretbox_xsalsa20poly1305_noncebytes(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_noncebytes(); + + // size_t crypto_secretbox_xsalsa20poly1305_macbytes(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_macbytes(); + + // size_t crypto_secretbox_xsalsa20poly1305_messagebytes_max(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_messagebytes_max(); + + // int crypto_secretbox_xsalsa20poly1305(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xsalsa20poly1305( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] k); + + // int crypto_secretbox_xsalsa20poly1305_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xsalsa20poly1305_open( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] k); + + // void crypto_secretbox_xsalsa20poly1305_keygen(unsigned char[] k); + void crypto_secretbox_xsalsa20poly1305_keygen(@Out byte[] k); + + // size_t crypto_secretbox_xsalsa20poly1305_boxzerobytes(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_boxzerobytes(); + + // size_t crypto_secretbox_xsalsa20poly1305_zerobytes(void); + @ssize_t + long crypto_secretbox_xsalsa20poly1305_zerobytes(); + + // size_t crypto_secretbox_keybytes(void); + @ssize_t + long crypto_secretbox_keybytes(); + + // size_t crypto_secretbox_noncebytes(void); + @ssize_t + long crypto_secretbox_noncebytes(); + + // size_t crypto_secretbox_macbytes(void); + @ssize_t + long crypto_secretbox_macbytes(); + + // const char * crypto_secretbox_primitive(void); + String crypto_secretbox_primitive(); + + // size_t crypto_secretbox_messagebytes_max(void); + @ssize_t + long crypto_secretbox_messagebytes_max(); + + // int crypto_secretbox_easy(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_easy(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In Pointer n, @In Pointer k); + + // int crypto_secretbox_open_easy(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_open_easy(@Out byte[] m, @In byte[] c, @In @u_int64_t long clen, @In Pointer n, @In Pointer k); + + // int crypto_secretbox_detached(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_detached( + @Out byte[] c, + @Out byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In Pointer n, + @In Pointer k); + + // int crypto_secretbox_open_detached(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_open_detached( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In Pointer n, + @In Pointer k); + + // void crypto_secretbox_keygen(unsigned char[] k); + void crypto_secretbox_keygen(@Out Pointer k); + + // size_t crypto_secretbox_zerobytes(void); + @ssize_t + long crypto_secretbox_zerobytes(); + + // size_t crypto_secretbox_boxzerobytes(void); + @ssize_t + long crypto_secretbox_boxzerobytes(); + + // int crypto_secretbox(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // int crypto_secretbox_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_open(@Out byte[] m, @In byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // size_t crypto_stream_chacha20_keybytes(void); + @ssize_t + long crypto_stream_chacha20_keybytes(); + + // size_t crypto_stream_chacha20_noncebytes(void); + @ssize_t + long crypto_stream_chacha20_noncebytes(); + + // size_t crypto_stream_chacha20_messagebytes_max(void); + @ssize_t + long crypto_stream_chacha20_messagebytes_max(); + + // int crypto_stream_chacha20(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_chacha20(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_chacha20_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_chacha20_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // int crypto_stream_chacha20_xor_ic(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, uint64_t ic, const unsigned char * k); + int crypto_stream_chacha20_xor_ic( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In @u_int64_t long ic, + @In byte[] k); + + // void crypto_stream_chacha20_keygen(unsigned char[] k); + void crypto_stream_chacha20_keygen(@Out byte[] k); + + // size_t crypto_stream_chacha20_ietf_keybytes(void); + @ssize_t + long crypto_stream_chacha20_ietf_keybytes(); + + // size_t crypto_stream_chacha20_ietf_noncebytes(void); + @ssize_t + long crypto_stream_chacha20_ietf_noncebytes(); + + // size_t crypto_stream_chacha20_ietf_messagebytes_max(void); + @ssize_t + long crypto_stream_chacha20_ietf_messagebytes_max(); + + // int crypto_stream_chacha20_ietf(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_chacha20_ietf(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_chacha20_ietf_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_chacha20_ietf_xor( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] k); + + // int crypto_stream_chacha20_ietf_xor_ic(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, uint32_t ic, const unsigned char * k); + int crypto_stream_chacha20_ietf_xor_ic( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In @u_int32_t int ic, + @In byte[] k); + + // void crypto_stream_chacha20_ietf_keygen(unsigned char[] k); + void crypto_stream_chacha20_ietf_keygen(@Out byte[] k); + + // size_t crypto_secretstream_xchacha20poly1305_abytes(void); + @ssize_t + long crypto_secretstream_xchacha20poly1305_abytes(); + + // size_t crypto_secretstream_xchacha20poly1305_headerbytes(void); + @ssize_t + long crypto_secretstream_xchacha20poly1305_headerbytes(); + + // size_t crypto_secretstream_xchacha20poly1305_keybytes(void); + @ssize_t + long crypto_secretstream_xchacha20poly1305_keybytes(); + + // size_t crypto_secretstream_xchacha20poly1305_messagebytes_max(void); + @ssize_t + long crypto_secretstream_xchacha20poly1305_messagebytes_max(); + + // unsigned char crypto_secretstream_xchacha20poly1305_tag_message(void); + @u_int8_t + char crypto_secretstream_xchacha20poly1305_tag_message(); + + // unsigned char crypto_secretstream_xchacha20poly1305_tag_push(void); + @u_int8_t + char crypto_secretstream_xchacha20poly1305_tag_push(); + + // unsigned char crypto_secretstream_xchacha20poly1305_tag_rekey(void); + @u_int8_t + char crypto_secretstream_xchacha20poly1305_tag_rekey(); + + // unsigned char crypto_secretstream_xchacha20poly1305_tag_final(void); + @u_int8_t + char crypto_secretstream_xchacha20poly1305_tag_final(); + + // size_t crypto_secretstream_xchacha20poly1305_statebytes(void); + @ssize_t + long crypto_secretstream_xchacha20poly1305_statebytes(); + + // void crypto_secretstream_xchacha20poly1305_keygen(unsigned char[] k); + void crypto_secretstream_xchacha20poly1305_keygen(@Out Pointer k); + + // int crypto_secretstream_xchacha20poly1305_init_push(crypto_secretstream_xchacha20poly1305_state * state, unsigned char[] header, const unsigned char[] k); + int crypto_secretstream_xchacha20poly1305_init_push(/*both*/ Pointer state, @Out byte[] header, @In Pointer k); + + // int crypto_secretstream_xchacha20poly1305_push(crypto_secretstream_xchacha20poly1305_state * state, unsigned char * c, unsigned long long * clen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * ad, unsigned long long adlen, unsigned char tag); + int crypto_secretstream_xchacha20poly1305_push( + /*both*/ Pointer state, + @Out byte[] c, + @Out LongLongByReference clen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] ad, + @In @u_int64_t long adlen, + @In @u_int8_t byte tag); + + // int crypto_secretstream_xchacha20poly1305_init_pull(crypto_secretstream_xchacha20poly1305_state * state, const unsigned char[] header, const unsigned char[] k); + int crypto_secretstream_xchacha20poly1305_init_pull(/*both*/ Pointer state, @In byte[] header, @In Pointer k); + + // int crypto_secretstream_xchacha20poly1305_pull(crypto_secretstream_xchacha20poly1305_state * state, unsigned char * m, unsigned long long * mlen_p, unsigned char * tag_p, const unsigned char * c, unsigned long long clen, const unsigned char * ad, unsigned long long adlen); + int crypto_secretstream_xchacha20poly1305_pull( + /*both*/ Pointer state, + @Out byte[] m, + @Out LongLongByReference mlen_p, + @Out ByteByReference tag_p, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] ad, + @In @u_int64_t long adlen); + + // void crypto_secretstream_xchacha20poly1305_rekey(crypto_secretstream_xchacha20poly1305_state * state); + void crypto_secretstream_xchacha20poly1305_rekey(/*both*/ Pointer state); + + // size_t crypto_shorthash_siphash24_bytes(void); + @ssize_t + long crypto_shorthash_siphash24_bytes(); + + // size_t crypto_shorthash_siphash24_keybytes(void); + @ssize_t + long crypto_shorthash_siphash24_keybytes(); + + // int crypto_shorthash_siphash24(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_shorthash_siphash24(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // size_t crypto_shorthash_siphashx24_bytes(void); + @ssize_t + long crypto_shorthash_siphashx24_bytes(); + + // size_t crypto_shorthash_siphashx24_keybytes(void); + @ssize_t + long crypto_shorthash_siphashx24_keybytes(); + + // int crypto_shorthash_siphashx24(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_shorthash_siphashx24(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // size_t crypto_shorthash_bytes(void); + @ssize_t + long crypto_shorthash_bytes(); + + // size_t crypto_shorthash_keybytes(void); + @ssize_t + long crypto_shorthash_keybytes(); + + // const char * crypto_shorthash_primitive(void); + String crypto_shorthash_primitive(); + + // int crypto_shorthash(unsigned char * out, const unsigned char * in, unsigned long long inlen, const unsigned char * k); + int crypto_shorthash(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen, @In byte[] k); + + // void crypto_shorthash_keygen(unsigned char[] k); + void crypto_shorthash_keygen(@Out byte[] k); + + // size_t crypto_sign_ed25519ph_statebytes(void); + @ssize_t + long crypto_sign_ed25519ph_statebytes(); + + // size_t crypto_sign_ed25519_bytes(void); + @ssize_t + long crypto_sign_ed25519_bytes(); + + // size_t crypto_sign_ed25519_seedbytes(void); + @ssize_t + long crypto_sign_ed25519_seedbytes(); + + // size_t crypto_sign_ed25519_publickeybytes(void); + @ssize_t + long crypto_sign_ed25519_publickeybytes(); + + // size_t crypto_sign_ed25519_secretkeybytes(void); + @ssize_t + long crypto_sign_ed25519_secretkeybytes(); + + // size_t crypto_sign_ed25519_messagebytes_max(void); + @ssize_t + long crypto_sign_ed25519_messagebytes_max(); + + // int crypto_sign_ed25519(unsigned char * sm, unsigned long long * smlen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * sk); + int crypto_sign_ed25519( + @Out byte[] sm, + @Out LongLongByReference smlen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] sk); + + // int crypto_sign_ed25519_open(unsigned char * m, unsigned long long * mlen_p, const unsigned char * sm, unsigned long long smlen, const unsigned char * pk); + int crypto_sign_ed25519_open( + @Out byte[] m, + @Out LongLongByReference mlen_p, + @In byte[] sm, + @In @u_int64_t long smlen, + @In byte[] pk); + + // int crypto_sign_ed25519_detached(unsigned char * sig, unsigned long long * siglen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * sk); + int crypto_sign_ed25519_detached( + @Out byte[] sig, + @Out LongLongByReference siglen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] sk); + + // int crypto_sign_ed25519_verify_detached(const unsigned char * sig, const unsigned char * m, unsigned long long mlen, const unsigned char * pk); + int crypto_sign_ed25519_verify_detached(@In byte[] sig, @In byte[] m, @In @u_int64_t long mlen, @In byte[] pk); + + // int crypto_sign_ed25519_keypair(unsigned char * pk, unsigned char * sk); + int crypto_sign_ed25519_keypair(@Out byte[] pk, @Out byte[] sk); + + // int crypto_sign_ed25519_seed_keypair(unsigned char * pk, unsigned char * sk, const unsigned char * seed); + int crypto_sign_ed25519_seed_keypair(@Out byte[] pk, @Out byte[] sk, @In byte[] seed); + + // int crypto_sign_ed25519_pk_to_curve25519(unsigned char * curve25519_pk, const unsigned char * ed25519_pk); + int crypto_sign_ed25519_pk_to_curve25519(@Out byte[] curve25519_pk, @In byte[] ed25519_pk); + + // int crypto_sign_ed25519_sk_to_curve25519(unsigned char * curve25519_sk, const unsigned char * ed25519_sk); + int crypto_sign_ed25519_sk_to_curve25519(@Out byte[] curve25519_sk, @In byte[] ed25519_sk); + + // int crypto_sign_ed25519_sk_to_seed(unsigned char * seed, const unsigned char * sk); + int crypto_sign_ed25519_sk_to_seed(@Out byte[] seed, @In byte[] sk); + + // int crypto_sign_ed25519_sk_to_pk(unsigned char * pk, const unsigned char * sk); + int crypto_sign_ed25519_sk_to_pk(@Out byte[] pk, @In byte[] sk); + + // int crypto_sign_ed25519ph_init(crypto_sign_ed25519ph_state * state); + int crypto_sign_ed25519ph_init(@Out Pointer state); + + // int crypto_sign_ed25519ph_update(crypto_sign_ed25519ph_state * state, const unsigned char * m, unsigned long long mlen); + int crypto_sign_ed25519ph_update(/*both*/ Pointer state, @In byte[] m, @In @u_int64_t long mlen); + + // int crypto_sign_ed25519ph_final_create(crypto_sign_ed25519ph_state * state, unsigned char * sig, unsigned long long * siglen_p, const unsigned char * sk); + int crypto_sign_ed25519ph_final_create( + /*both*/ Pointer state, + @Out byte[] sig, + @Out LongLongByReference siglen_p, + @In byte[] sk); + + // int crypto_sign_ed25519ph_final_verify(crypto_sign_ed25519ph_state * state, unsigned char * sig, const unsigned char * pk); + int crypto_sign_ed25519ph_final_verify(/*both*/ Pointer state, @In byte[] sig, @In byte[] pk); + + // size_t crypto_sign_statebytes(void); + @ssize_t + long crypto_sign_statebytes(); + + // size_t crypto_sign_bytes(void); + @ssize_t + long crypto_sign_bytes(); + + // size_t crypto_sign_seedbytes(void); + @ssize_t + long crypto_sign_seedbytes(); + + // size_t crypto_sign_publickeybytes(void); + @ssize_t + long crypto_sign_publickeybytes(); + + // size_t crypto_sign_secretkeybytes(void); + @ssize_t + long crypto_sign_secretkeybytes(); + + // size_t crypto_sign_messagebytes_max(void); + @ssize_t + long crypto_sign_messagebytes_max(); + + // const char * crypto_sign_primitive(void); + String crypto_sign_primitive(); + + // int crypto_sign_seed_keypair(unsigned char * pk, unsigned char * sk, const unsigned char * seed); + int crypto_sign_seed_keypair(@Out byte[] pk, @Out byte[] sk, @In byte[] seed); + + // int crypto_sign_keypair(unsigned char * pk, unsigned char * sk); + int crypto_sign_keypair(@Out byte[] pk, @Out byte[] sk); + + // int crypto_sign(unsigned char * sm, unsigned long long * smlen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * sk); + int crypto_sign( + @Out byte[] sm, + @Out LongLongByReference smlen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] sk); + + // int crypto_sign_open(unsigned char * m, unsigned long long * mlen_p, const unsigned char * sm, unsigned long long smlen, const unsigned char * pk); + int crypto_sign_open( + @Out byte[] m, + @Out LongLongByReference mlen_p, + @In byte[] sm, + @In @u_int64_t long smlen, + @In byte[] pk); + + // int crypto_sign_detached(unsigned char * sig, unsigned long long * siglen_p, const unsigned char * m, unsigned long long mlen, const unsigned char * sk); + int crypto_sign_detached( + @Out byte[] sig, + @Out LongLongByReference siglen_p, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] sk); + + // int crypto_sign_verify_detached(const unsigned char * sig, const unsigned char * m, unsigned long long mlen, const unsigned char * pk); + int crypto_sign_verify_detached(@In byte[] sig, @In byte[] m, @In @u_int64_t long mlen, @In byte[] pk); + + // int crypto_sign_init(crypto_sign_state * state); + int crypto_sign_init(@Out Pointer state); + + // int crypto_sign_update(crypto_sign_state * state, const unsigned char * m, unsigned long long mlen); + int crypto_sign_update(/*both*/ Pointer state, @In byte[] m, @In @u_int64_t long mlen); + + // int crypto_sign_final_create(crypto_sign_state * state, unsigned char * sig, unsigned long long * siglen_p, const unsigned char * sk); + int crypto_sign_final_create( + /*both*/ Pointer state, + @Out byte[] sig, + @Out LongLongByReference siglen_p, + @In byte[] sk); + + // int crypto_sign_final_verify(crypto_sign_state * state, unsigned char * sig, const unsigned char * pk); + int crypto_sign_final_verify(/*both*/ Pointer state, @In byte[] sig, @In byte[] pk); + + // size_t crypto_stream_keybytes(void); + @ssize_t + long crypto_stream_keybytes(); + + // size_t crypto_stream_noncebytes(void); + @ssize_t + long crypto_stream_noncebytes(); + + // size_t crypto_stream_messagebytes_max(void); + @ssize_t + long crypto_stream_messagebytes_max(); + + // const char * crypto_stream_primitive(void); + String crypto_stream_primitive(); + + // int crypto_stream(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // void crypto_stream_keygen(unsigned char[] k); + void crypto_stream_keygen(@Out byte[] k); + + // size_t crypto_stream_salsa20_keybytes(void); + @ssize_t + long crypto_stream_salsa20_keybytes(); + + // size_t crypto_stream_salsa20_noncebytes(void); + @ssize_t + long crypto_stream_salsa20_noncebytes(); + + // size_t crypto_stream_salsa20_messagebytes_max(void); + @ssize_t + long crypto_stream_salsa20_messagebytes_max(); + + // int crypto_stream_salsa20(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa20(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_salsa20_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa20_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // int crypto_stream_salsa20_xor_ic(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, uint64_t ic, const unsigned char * k); + int crypto_stream_salsa20_xor_ic( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In @u_int64_t long ic, + @In byte[] k); + + // void crypto_stream_salsa20_keygen(unsigned char[] k); + void crypto_stream_salsa20_keygen(@Out byte[] k); + + // size_t crypto_verify_16_bytes(void); + @ssize_t + long crypto_verify_16_bytes(); + + // int crypto_verify_16(const unsigned char * x, const unsigned char * y); + int crypto_verify_16(@In byte[] x, @In byte[] y); + + // size_t crypto_verify_32_bytes(void); + @ssize_t + long crypto_verify_32_bytes(); + + // int crypto_verify_32(const unsigned char * x, const unsigned char * y); + int crypto_verify_32(@In byte[] x, @In byte[] y); + + // size_t crypto_verify_64_bytes(void); + @ssize_t + long crypto_verify_64_bytes(); + + // int crypto_verify_64(const unsigned char * x, const unsigned char * y); + int crypto_verify_64(@In byte[] x, @In byte[] y); + + // const char * implementation_name(void); + String implementation_name(); + + // uint32_t random(void); + @u_int32_t + int random(); + + // void stir(void); + void stir(); + + // uint32_t uniform(const uint32_t upper_bound); + @u_int32_t + int uniform(@In @u_int32_t int upper_bound); + + // void buf(void *const buf, const size_t size); + void buf(/*@In @Out*/ byte[] buf, @In @ssize_t long size); + + // int close(void); + int close(); + + // size_t randombytes_seedbytes(void); + @ssize_t + long randombytes_seedbytes(); + + // void randombytes_buf(void *const buf, const size_t size); + void randombytes_buf(@Out Pointer buf, @In @ssize_t long size); + + // void randombytes_buf_deterministic(void *const buf, const size_t size, const unsigned char[] seed); + void randombytes_buf_deterministic(@Out byte[] buf, @In @ssize_t long size, @In byte[] seed); + + // uint32_t randombytes_random(void); + @u_int32_t + int randombytes_random(); + + // uint32_t randombytes_uniform(const uint32_t upper_bound); + @u_int32_t + int randombytes_uniform(@In @u_int32_t int upper_bound); + + // void randombytes_stir(void); + void randombytes_stir(); + + // int randombytes_close(void); + int randombytes_close(); + + // int randombytes_set_implementation(randombytes_implementation * impl); + int randombytes_set_implementation(/*@In @Out*/ Pointer impl); + + // const char * randombytes_implementation_name(void); + String randombytes_implementation_name(); + + // void randombytes(unsigned char *const buf, const unsigned long long buf_len); + void randombytes(@Out byte[] buf, @In @u_int64_t long buf_len); + + // int sodium_runtime_has_neon(void); + int sodium_runtime_has_neon(); + + // int sodium_runtime_has_sse2(void); + int sodium_runtime_has_sse2(); + + // int sodium_runtime_has_sse3(void); + int sodium_runtime_has_sse3(); + + // int sodium_runtime_has_ssse3(void); + int sodium_runtime_has_ssse3(); + + // int sodium_runtime_has_sse41(void); + int sodium_runtime_has_sse41(); + + // int sodium_runtime_has_avx(void); + int sodium_runtime_has_avx(); + + // int sodium_runtime_has_avx2(void); + int sodium_runtime_has_avx2(); + + // int sodium_runtime_has_avx512f(void); + int sodium_runtime_has_avx512f(); + + // int sodium_runtime_has_pclmul(void); + int sodium_runtime_has_pclmul(); + + // int sodium_runtime_has_aesni(void); + int sodium_runtime_has_aesni(); + + // int sodium_runtime_has_rdrand(void); + int sodium_runtime_has_rdrand(); + + // void sodium_memzero(void *const pnt, const size_t len); + void sodium_memzero(/*@In @Out*/ Pointer pnt, @In @ssize_t long len); + + // void sodium_stackzero(const size_t len); + void sodium_stackzero(@In @ssize_t long len); + + // int sodium_memcmp(const void *const b1_, const void *const b2_, size_t len); + int sodium_memcmp(@In Pointer b1_, @In Pointer b2_, @In @ssize_t long len); + + // int sodium_compare(const unsigned char * b1_, const unsigned char * b2_, size_t len); + int sodium_compare(@In Pointer b1_, @In Pointer b2_, @In @ssize_t long len); + + // int sodium_is_zero(const unsigned char * n, const size_t nlen); + int sodium_is_zero(@In Pointer n, @In @ssize_t long nlen); + + // void sodium_increment(unsigned char * n, const size_t nlen); + void sodium_increment(/*@In @Out*/ Pointer n, @In @ssize_t long nlen); + + // void sodium_add(unsigned char * a, const unsigned char * b, const size_t len); + void sodium_add(/*@In @Out*/ Pointer a, @In Pointer b, @In @ssize_t long len); + + // char * sodium_bin2hex(char *const hex, const size_t hex_maxlen, const unsigned char *const bin, const size_t bin_len); + // FIXME: JNR-FFI code generation fails for this method + // byte[] sodium_bin2hex(@Out byte[] hex, @In @ssize_t long hex_maxlen, @In byte[] bin, @In @ssize_t long bin_len); + + // int sodium_hex2bin(unsigned char *const bin, const size_t bin_maxlen, const char *const hex, const size_t hex_len, const char *const ignore, size_t *const bin_len, const char * *const hex_end); + int sodium_hex2bin( + @Out byte[] bin, + @In @ssize_t long bin_maxlen, + @In byte[] hex, + @In @ssize_t long hex_len, + @In byte[] ignore, + @Out LongLongByReference bin_len, + /*@In @Out*/ Pointer hex_end); + + // size_t sodium_base64_encoded_len(const size_t bin_len, const int variant); + @ssize_t + long sodium_base64_encoded_len(@In @ssize_t long bin_len, @In int variant); + + // char * sodium_bin2base64(char *const b64, const size_t b64_maxlen, const unsigned char *const bin, const size_t bin_len, const int variant); + // FIXME: JNR-FFI code generation fails for this method + // byte[] sodium_bin2base64( + // @Out byte[] b64, + // @In @ssize_t long b64_maxlen, + // @In byte[] bin, + // @In @ssize_t long bin_len, + // @In int variant); + + // int sodium_base642bin(unsigned char *const bin, const size_t bin_maxlen, const char *const b64, const size_t b64_len, const char *const ignore, size_t *const bin_len, const char * *const b64_end, const int variant); + int sodium_base642bin( + @Out byte[] bin, + @In @ssize_t long bin_maxlen, + @In byte[] b64, + @In @ssize_t long b64_len, + @In byte[] ignore, + @Out LongLongByReference bin_len, + /*@In @Out*/ Pointer b64_end, + @In int variant); + + // int sodium_mlock(void *const addr, const size_t len); + int sodium_mlock(/*@In @Out*/ Pointer addr, @In @ssize_t long len); + + // int sodium_munlock(void *const addr, const size_t len); + int sodium_munlock(/*@In @Out*/ Pointer addr, @In @ssize_t long len); + + // void * sodium_malloc(const size_t size); + Pointer sodium_malloc(@In @ssize_t long size); + + // void * sodium_allocarray(size_t count, size_t size); + Pointer sodium_allocarray(@In @ssize_t long count, @In @ssize_t long size); + + // void sodium_free(void * ptr); + void sodium_free(/*@In @Out*/ Pointer ptr); + + // int sodium_mprotect_noaccess(void * ptr); + int sodium_mprotect_noaccess(/*@In @Out*/ Pointer ptr); + + // int sodium_mprotect_readonly(void * ptr); + int sodium_mprotect_readonly(/*@In @Out*/ Pointer ptr); + + // int sodium_mprotect_readwrite(void * ptr); + int sodium_mprotect_readwrite(/*@In @Out*/ Pointer ptr); + + // int sodium_pad(size_t * padded_buflen_p, unsigned char * buf, size_t unpadded_buflen, size_t blocksize, size_t max_buflen); + int sodium_pad( + @Out LongLongByReference padded_buflen_p, + /*@In @Out*/ byte[] buf, + @In @ssize_t long unpadded_buflen, + @In @ssize_t long blocksize, + @In @ssize_t long max_buflen); + + // int sodium_unpad(size_t * unpadded_buflen_p, const unsigned char * buf, size_t padded_buflen, size_t blocksize); + int sodium_unpad( + @Out LongLongByReference unpadded_buflen_p, + @In byte[] buf, + @In @ssize_t long padded_buflen, + @In @ssize_t long blocksize); + + // size_t crypto_stream_xchacha20_keybytes(void); + @ssize_t + long crypto_stream_xchacha20_keybytes(); + + // size_t crypto_stream_xchacha20_noncebytes(void); + @ssize_t + long crypto_stream_xchacha20_noncebytes(); + + // size_t crypto_stream_xchacha20_messagebytes_max(void); + @ssize_t + long crypto_stream_xchacha20_messagebytes_max(); + + // int crypto_stream_xchacha20(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_xchacha20(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_xchacha20_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_xchacha20_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // int crypto_stream_xchacha20_xor_ic(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, uint64_t ic, const unsigned char * k); + int crypto_stream_xchacha20_xor_ic( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In @u_int64_t long ic, + @In byte[] k); + + // void crypto_stream_xchacha20_keygen(unsigned char[] k); + void crypto_stream_xchacha20_keygen(@Out byte[] k); + + // size_t crypto_box_curve25519xchacha20poly1305_seedbytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_seedbytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_publickeybytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_publickeybytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_secretkeybytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_secretkeybytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_beforenmbytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_beforenmbytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_noncebytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_noncebytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_macbytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_macbytes(); + + // size_t crypto_box_curve25519xchacha20poly1305_messagebytes_max(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_messagebytes_max(); + + // int crypto_box_curve25519xchacha20poly1305_seed_keypair(unsigned char * pk, unsigned char * sk, const unsigned char * seed); + int crypto_box_curve25519xchacha20poly1305_seed_keypair(@Out byte[] pk, @Out byte[] sk, @In byte[] seed); + + // int crypto_box_curve25519xchacha20poly1305_keypair(unsigned char * pk, unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_keypair(@Out byte[] pk, @Out byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_easy(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_easy( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_open_easy(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_open_easy( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_detached(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_detached( + @Out byte[] c, + /*@In @Out*/ byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_open_detached(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_open_detached( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] pk, + @In byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_beforenm(unsigned char * k, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_beforenm(@Out Pointer k, @In byte[] pk, @In byte[] sk); + + // int crypto_box_curve25519xchacha20poly1305_easy_afternm(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xchacha20poly1305_easy_afternm( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In Pointer k); + + // int crypto_box_curve25519xchacha20poly1305_open_easy_afternm(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xchacha20poly1305_open_easy_afternm( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In Pointer k); + + // int crypto_box_curve25519xchacha20poly1305_detached_afternm(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xchacha20poly1305_detached_afternm( + @Out byte[] c, + /*@In @Out*/ byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In Pointer k); + + // int crypto_box_curve25519xchacha20poly1305_open_detached_afternm(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_box_curve25519xchacha20poly1305_open_detached_afternm( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In byte[] n, + @In Pointer k); + + // size_t crypto_box_curve25519xchacha20poly1305_sealbytes(void); + @ssize_t + long crypto_box_curve25519xchacha20poly1305_sealbytes(); + + // int crypto_box_curve25519xchacha20poly1305_seal(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * pk); + int crypto_box_curve25519xchacha20poly1305_seal(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] pk); + + // int crypto_box_curve25519xchacha20poly1305_seal_open(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * pk, const unsigned char * sk); + int crypto_box_curve25519xchacha20poly1305_seal_open( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] pk, + @In byte[] sk); + + // size_t crypto_core_ed25519_bytes(void); + @ssize_t + long crypto_core_ed25519_bytes(); + + // size_t crypto_core_ed25519_uniformbytes(void); + @ssize_t + long crypto_core_ed25519_uniformbytes(); + + // int crypto_core_ed25519_is_valid_point(const unsigned char * p); + int crypto_core_ed25519_is_valid_point(@In byte[] p); + + // int crypto_core_ed25519_add(unsigned char * r, const unsigned char * p, const unsigned char * q); + int crypto_core_ed25519_add(@Out byte[] r, @In byte[] p, @In byte[] q); + + // int crypto_core_ed25519_sub(unsigned char * r, const unsigned char * p, const unsigned char * q); + int crypto_core_ed25519_sub(@Out byte[] r, @In byte[] p, @In byte[] q); + + // int crypto_core_ed25519_from_uniform(unsigned char * p, const unsigned char * r); + int crypto_core_ed25519_from_uniform(@Out byte[] p, @In byte[] r); + + // size_t crypto_scalarmult_ed25519_bytes(void); + @ssize_t + long crypto_scalarmult_ed25519_bytes(); + + // size_t crypto_scalarmult_ed25519_scalarbytes(void); + @ssize_t + long crypto_scalarmult_ed25519_scalarbytes(); + + // int crypto_scalarmult_ed25519(unsigned char * q, const unsigned char * n, const unsigned char * p); + int crypto_scalarmult_ed25519(@Out byte[] q, @In byte[] n, @In byte[] p); + + // int crypto_scalarmult_ed25519_base(unsigned char * q, const unsigned char * n); + int crypto_scalarmult_ed25519_base(@Out byte[] q, @In byte[] n); + + // size_t crypto_secretbox_xchacha20poly1305_keybytes(void); + @ssize_t + long crypto_secretbox_xchacha20poly1305_keybytes(); + + // size_t crypto_secretbox_xchacha20poly1305_noncebytes(void); + @ssize_t + long crypto_secretbox_xchacha20poly1305_noncebytes(); + + // size_t crypto_secretbox_xchacha20poly1305_macbytes(void); + @ssize_t + long crypto_secretbox_xchacha20poly1305_macbytes(); + + // size_t crypto_secretbox_xchacha20poly1305_messagebytes_max(void); + @ssize_t + long crypto_secretbox_xchacha20poly1305_messagebytes_max(); + + // int crypto_secretbox_xchacha20poly1305_easy(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xchacha20poly1305_easy( + @Out byte[] c, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] k); + + // int crypto_secretbox_xchacha20poly1305_open_easy(unsigned char * m, const unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xchacha20poly1305_open_easy( + @Out byte[] m, + @In byte[] c, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] k); + + // int crypto_secretbox_xchacha20poly1305_detached(unsigned char * c, unsigned char * mac, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xchacha20poly1305_detached( + @Out byte[] c, + @Out byte[] mac, + @In byte[] m, + @In @u_int64_t long mlen, + @In byte[] n, + @In byte[] k); + + // int crypto_secretbox_xchacha20poly1305_open_detached(unsigned char * m, const unsigned char * c, const unsigned char * mac, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_secretbox_xchacha20poly1305_open_detached( + @Out byte[] m, + @In byte[] c, + @In byte[] mac, + @In @u_int64_t long clen, + @In byte[] n, + @In byte[] k); + + // size_t crypto_pwhash_scryptsalsa208sha256_bytes_min(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_bytes_min(); + + // size_t crypto_pwhash_scryptsalsa208sha256_bytes_max(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_bytes_max(); + + // size_t crypto_pwhash_scryptsalsa208sha256_passwd_min(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_passwd_min(); + + // size_t crypto_pwhash_scryptsalsa208sha256_passwd_max(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_passwd_max(); + + // size_t crypto_pwhash_scryptsalsa208sha256_saltbytes(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_saltbytes(); + + // size_t crypto_pwhash_scryptsalsa208sha256_strbytes(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_strbytes(); + + // const char * crypto_pwhash_scryptsalsa208sha256_strprefix(void); + String crypto_pwhash_scryptsalsa208sha256_strprefix(); + + // size_t crypto_pwhash_scryptsalsa208sha256_opslimit_min(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_opslimit_min(); + + // size_t crypto_pwhash_scryptsalsa208sha256_opslimit_max(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_opslimit_max(); + + // size_t crypto_pwhash_scryptsalsa208sha256_memlimit_min(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_memlimit_min(); + + // size_t crypto_pwhash_scryptsalsa208sha256_memlimit_max(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_memlimit_max(); + + // size_t crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(); + + // size_t crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(); + + // size_t crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(); + + // size_t crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(void); + @ssize_t + long crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(); + + // int crypto_pwhash_scryptsalsa208sha256(unsigned char *const out, unsigned long long outlen, const char *const passwd, unsigned long long passwdlen, const unsigned char *const salt, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_scryptsalsa208sha256( + @Out byte[] out, + @In @u_int64_t long outlen, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In byte[] salt, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // int crypto_pwhash_scryptsalsa208sha256_str(char[] out, const char *const passwd, unsigned long long passwdlen, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_scryptsalsa208sha256_str( + @Out byte[] out, + @In byte[] passwd, + @In @u_int64_t long passwdlen, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // int crypto_pwhash_scryptsalsa208sha256_str_verify(const char[] str, const char *const passwd, unsigned long long passwdlen); + int crypto_pwhash_scryptsalsa208sha256_str_verify(@In byte[] str, @In byte[] passwd, @In @u_int64_t long passwdlen); + + // int crypto_pwhash_scryptsalsa208sha256_ll(const uint8_t * passwd, size_t passwdlen, const uint8_t * salt, size_t saltlen, uint64_t N, uint32_t r, uint32_t p, uint8_t * buf, size_t buflen); + int crypto_pwhash_scryptsalsa208sha256_ll( + @In byte[] passwd, + @In @ssize_t long passwdlen, + @In byte[] salt, + @In @ssize_t long saltlen, + @In @u_int64_t long N, + @In @u_int32_t int r, + @In @u_int32_t int p, + @Out byte[] buf, + @In @ssize_t long buflen); + + // int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(const char[] str, unsigned long long opslimit, size_t memlimit); + int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash( + @In byte[] str, + @In @u_int64_t long opslimit, + @In @ssize_t long memlimit); + + // size_t crypto_stream_salsa2012_keybytes(void); + @ssize_t + long crypto_stream_salsa2012_keybytes(); + + // size_t crypto_stream_salsa2012_noncebytes(void); + @ssize_t + long crypto_stream_salsa2012_noncebytes(); + + // size_t crypto_stream_salsa2012_messagebytes_max(void); + @ssize_t + long crypto_stream_salsa2012_messagebytes_max(); + + // int crypto_stream_salsa2012(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa2012(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_salsa2012_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa2012_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // void crypto_stream_salsa2012_keygen(unsigned char[] k); + void crypto_stream_salsa2012_keygen(@Out byte[] k); + + // size_t crypto_stream_salsa208_keybytes(void); + @ssize_t + long crypto_stream_salsa208_keybytes(); + + // size_t crypto_stream_salsa208_noncebytes(void); + @ssize_t + long crypto_stream_salsa208_noncebytes(); + + // size_t crypto_stream_salsa208_messagebytes_max(void); + @ssize_t + long crypto_stream_salsa208_messagebytes_max(); + + // int crypto_stream_salsa208(unsigned char * c, unsigned long long clen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa208(@Out byte[] c, @In @u_int64_t long clen, @In byte[] n, @In byte[] k); + + // int crypto_stream_salsa208_xor(unsigned char * c, const unsigned char * m, unsigned long long mlen, const unsigned char * n, const unsigned char * k); + int crypto_stream_salsa208_xor(@Out byte[] c, @In byte[] m, @In @u_int64_t long mlen, @In byte[] n, @In byte[] k); + + // void crypto_stream_salsa208_keygen(unsigned char[] k); + void crypto_stream_salsa208_keygen(@Out byte[] k); +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/PasswordHash.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/PasswordHash.java new file mode 100644 index 00000000..1a0cc096 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/PasswordHash.java @@ -0,0 +1,657 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import com.google.common.base.Charsets; +import jnr.ffi.Pointer; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/password_hashing/the_argon2i_function.md + +/** + * The Argon2 memory-hard hashing function. + * + *

+ * Argon2 summarizes the state of the art in the design of memory-hard functions. + * + *

+ * It aims at the highest memory filling rate and effective use of multiple computing units, while still providing + * defense against tradeoff attacks. + * + *

+ * It prevents ASICs from having a significant advantage over software implementations. + * + *

Guidelines for choosing the parameters

+ * + *

+ * Start by determining how much memory the function can use. What will be the highest number of threads/processes + * evaluating the function simultaneously (ideally, no more than 1 per CPU core)? How much physical memory is guaranteed + * to be available? + * + *

+ * Set memlimit to the amount of memory you want to reserve for password hashing. + * + *

+ * Then, set opslimit to 3 and measure the time it takes to hash a password. + * + *

+ * If this it is way too long for your application, reduce memlimit, but keep opslimit set to 3. + * + *

+ * If the function is so fast that you can afford it to be more computationally intensive without any usability issues, + * increase opslimit. + * + *

+ * For online use (e.g. login in on a website), a 1 second computation is likely to be the acceptable maximum. + * + *

+ * For interactive use (e.g. a desktop application), a 5 second pause after having entered a password is acceptable if + * the password doesn't need to be entered more than once per session. + * + *

+ * For non-interactive use and infrequent use (e.g. restoring an encrypted backup), an even slower computation can be an + * option. + * + *

+ * This class depends upon the JNR-FFI library being available on the classpath, along with its dependencies. See + * https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +public final class PasswordHash { + + /** + * A PasswordHash salt. + */ + public static final class Salt { + private final Pointer ptr; + + private Salt(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Salt} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Salt forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Salt} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Salt forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_pwhash_saltbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_pwhash_saltbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Salt::new); + } + + /** + * Obtain the length of the salt in bytes (32). + * + * @return The length of the salt in bytes (32). + */ + public static int length() { + long saltbytes = Sodium.crypto_pwhash_saltbytes(); + if (saltbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_pwhash_saltbytes: " + saltbytes + " is too large"); + } + return (int) saltbytes; + } + + /** + * Generate a new salt using a random generator. + * + * @return A randomly generated salt. + */ + public static Salt random() { + return Sodium.randomBytes(length(), Salt::new); + } + + /** + * @return The bytes of this salt. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this salt. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A PasswordHash algorithm. + */ + public static final class Algorithm { + private final int id; + private final long minOps; + + private Algorithm(int id, long minOps) { + this.id = id; + this.minOps = minOps; + } + + /** + * @return The currently recommended algorithm. + */ + public static Algorithm recommended() { + return new Algorithm(Sodium.crypto_pwhash_alg_default(), 1); + } + + /** + * @return Version 1.3 of the Argon2i algorithm. + */ + public static Algorithm argon2i13() { + return new Algorithm(Sodium.crypto_pwhash_alg_argon2i13(), 3); + } + + /** + * @return Version 1.3 of the Argon2id algorithm. + */ + public static Algorithm argon2id13() { + return new Algorithm(Sodium.crypto_pwhash_alg_argon2id13(), 1); + } + + private long minOps() { + return minOps; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Algorithm)) { + return false; + } + Algorithm other = (Algorithm) obj; + return this.id == other.id; + } + + @Override + public int hashCode() { + return Integer.hashCode(id); + } + } + + /** + * Compute a specific length key from a password. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link #maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hash(String password, int length, Salt salt, long opsLimit, long memLimit, Algorithm algorithm) { + return Bytes.wrap(hashToArray(password, length, salt, opsLimit, memLimit, algorithm)); + } + + /** + * Compute a specific length key from a password. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link #maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static byte[] hashToArray( + String password, + int length, + Salt salt, + long opsLimit, + long memLimit, + Algorithm algorithm) { + assertHashLength(length); + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + if (opsLimit < algorithm.minOps()) { + throw new IllegalArgumentException("opsLimit too low for specified algorithm"); + } + byte[] out = new byte[length]; + + byte[] pwbytes = password.getBytes(Charsets.UTF_8); + int rc = Sodium.crypto_pwhash(out, length, pwbytes, pwbytes.length, salt.ptr, opsLimit, memLimit, algorithm.id); + if (rc != 0) { + throw new SodiumException("crypto_pwhash: failed with result " + rc); + } + return out; + } + + /** + * @return The minimum hash length (16). + */ + public static int minHashLength() { + long len = Sodium.crypto_pwhash_bytes_min(); + if (len > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_pwhash_bytes_min: " + len + " is too large"); + } + return (int) len; + } + + /** + * @return The maximum hash length. + */ + public static int maxHashLength() { + long len = Sodium.crypto_pwhash_bytes_max(); + if (len > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) len; + } + + private static void assertHashLength(int length) { + if (length < Sodium.crypto_pwhash_bytes_min() || length > Sodium.crypto_pwhash_bytes_max()) { + throw new IllegalArgumentException("length out of range"); + } + } + + /** + * Compute a hash from a password, using limits on operations and memory that are suitable for most use-cases. + * + *

+ * Equivalent to {@code hash(password, moderateOpsLimit(), moderateMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hash(String password) { + return hash(password, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Compute a hash from a password, using limits on operations and memory that are suitable for interactive use-cases. + * + *

+ * Equivalent to {@code hash(password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hashInteractive(String password) { + return hash(password, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Compute a hash using limits on operations and memory that are suitable for sensitive use-cases. + * + *

+ * Equivalent to {@code hash(password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hashSensitive(String password) { + return hash(password, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Compute a hash from a password. + * + * @param password The password to hash. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link #maxMemLimit()}. + * @return The hash string. + */ + public static String hash(String password, long opsLimit, long memLimit) { + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] out = new byte[hashStringLength()]; + + byte[] pwbytes = password.getBytes(Charsets.UTF_8); + int rc = Sodium.crypto_pwhash_str(out, pwbytes, pwbytes.length, opsLimit, memLimit); + if (rc != 0) { + throw new SodiumException("crypto_pwhash_str: failed with result " + rc); + } + + int i = 0; + while (i < out.length && out[i] != 0) { + ++i; + } + return new String(out, 0, i, Charsets.UTF_8); + } + + /** + * A hash verification result. + */ + public enum VerificationResult { + /** The hash verification failed. */ + FAILED, + /** The hash verification passed. */ + PASSED, + /** The hash verification passed, but the hash is out-of-date and should be regenerated. */ + NEEDS_REHASH; + + /** + * @return true if the verification passed. + */ + public boolean passed() { + return this != FAILED; + } + + /** + * @return true if the hash should be regenerated. + */ + public boolean needsRehash() { + return this == NEEDS_REHASH; + } + } + + /** + * Verify a password against a hash using limits on operations and memory that are suitable for most use-cases. + * + *

+ * Equivalent to {@code verify(hash, password, moderateOpsLimit(), moderateMemLimit())}. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult verify(String hash, String password) { + return verify(hash, password, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Verify a password against a hash using limits on operations and memory that are suitable for interactive use-cases. + * + *

+ * Equivalent to {@code verify(hash, password, interactiveOpsLimit(), interactiveMemLimit())}. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult verifyInteractive(String hash, String password) { + return verify(hash, password, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Verify a password against a hash using limits on operations and memory that are suitable for sensitive use-cases. + * + *

+ * Equivalent to {@code verify(hash, password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult verifySensitive(String hash, String password) { + return verify(hash, password, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Verify a password against a hash. + * + * @param hash The hash. + * @param password The password to verify. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link #maxMemLimit()}. + * @return The result of verification. + */ + public static VerificationResult verify(String hash, String password, long opsLimit, long memLimit) { + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] hashBytes = hash.getBytes(Charsets.UTF_8); + + int strbytes = hashStringLength(); + if (hashBytes.length >= strbytes) { + throw new IllegalArgumentException("hash is too long"); + } + + Pointer str = Sodium.malloc(strbytes); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + byte[] pwbytes = password.getBytes(Charsets.UTF_8); + int rc = Sodium.crypto_pwhash_str_verify(str, pwbytes, pwbytes.length); + if (rc != 0) { + return VerificationResult.FAILED; + } + + rc = Sodium.crypto_pwhash_str_needs_rehash(str, opsLimit, memLimit); + if (rc < 0) { + throw new SodiumException("crypto_pwhash_str_needs_rehash: failed with result " + rc); + } + return (rc == 0) ? VerificationResult.PASSED : VerificationResult.NEEDS_REHASH; + } finally { + Sodium.sodium_free(str); + } + } + + /** + * Verify a password against a hash. + * + * @param hash The hash. + * @param password The password to verify. + * @return true if the password matches the hash. + */ + public static boolean verifyOnly(String hash, String password) { + byte[] hashBytes = hash.getBytes(Charsets.UTF_8); + + int strbytes = hashStringLength(); + if (hashBytes.length >= strbytes) { + throw new IllegalArgumentException("hash is too long"); + } + + Pointer str = Sodium.malloc(strbytes); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + byte[] pwbytes = password.getBytes(Charsets.UTF_8); + int rc = Sodium.crypto_pwhash_str_verify(str, pwbytes, pwbytes.length); + return (rc == 0); + } finally { + Sodium.sodium_free(str); + } + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable for most use-cases. + * + *

+ * Equivalent to {@code needsRehash(hash, moderateOpsLimit(), moderateMemLimit())}. + * + * @param hash The hash. + * @return true if the hash should be regenerated. + */ + public static boolean needsRehash(String hash) { + return needsRehash(hash, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable for interactive + * use-cases. + * + *

+ * Equivalent to {@code needsRehash(hash, interactiveOpsLimit(), interactiveMemLimit())}. + * + * @param hash The hash. + * @return true if the hash should be regenerated. + */ + public static boolean needsRehashInteractive(String hash) { + return needsRehash(hash, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable for sensitive + * use-cases. + * + *

+ * Equivalent to {@code needsRehash(hash, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param hash The hash. + * @return true if the hash should be regenerated. + */ + public static boolean needsRehashSensitive(String hash) { + return needsRehash(hash, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Check if a hash needs to be regenerated. + * + *

+ * Check if a hash matches the parameters opslimit and memlimit, and the current default algorithm. + * + * @param hash The hash. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link #maxMemLimit()}. + * @return true if the hash should be regenerated. + */ + public static boolean needsRehash(String hash, long opsLimit, long memLimit) { + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] hashBytes = hash.getBytes(Charsets.UTF_8); + + int strbytes = hashStringLength(); + if (hashBytes.length >= strbytes) { + throw new IllegalArgumentException("hash is too long"); + } + + Pointer str = Sodium.malloc(strbytes); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + int rc = Sodium.crypto_pwhash_str_needs_rehash(str, opsLimit, memLimit); + if (rc < 0) { + throw new SodiumException("crypto_pwhash_str_needs_rehash: failed with result " + rc); + } + return (rc == 0); + } finally { + Sodium.sodium_free(str); + } + } + + private static int hashStringLength() { + long strbytes = Sodium.crypto_pwhash_strbytes(); + if (strbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_pwhash_strbytes: " + strbytes + " is too large"); + } + return (int) strbytes; + } + + /** + * @return The minimum operations limit (1). + */ + public static long minOpsLimit() { + return Sodium.crypto_pwhash_opslimit_min(); + } + + /** + * @return An operations limit suitable for interactive use-cases (2). + */ + public static long interactiveOpsLimit() { + return Sodium.crypto_pwhash_opslimit_interactive(); + } + + /** + * @return An operations limit suitable for most use-cases (3). + */ + public static long moderateOpsLimit() { + return Sodium.crypto_pwhash_opslimit_moderate(); + } + + /** + * @return An operations limit for sensitive use-cases (4). + */ + public static long sensitiveOpsLimit() { + return Sodium.crypto_pwhash_opslimit_sensitive(); + } + + /** + * @return The maximum operations limit (4294967295). + */ + public static long maxOpsLimit() { + return Sodium.crypto_pwhash_opslimit_max(); + } + + private static void assertOpsLimit(long opsLimit) { + if (opsLimit < minOpsLimit() || opsLimit > maxOpsLimit()) { + throw new IllegalArgumentException("opsLimit out of range"); + } + } + + /** + * @return The minimum memory limit (8192). + */ + public static long minMemLimit() { + return Sodium.crypto_pwhash_memlimit_min(); + } + + /** + * @return A memory limit suitable for interactive use-cases (67108864). + */ + public static long interactiveMemLimit() { + return Sodium.crypto_pwhash_memlimit_interactive(); + } + + /** + * @return A memory limit suitable for most use-cases (268435456). + */ + public static long moderateMemLimit() { + return Sodium.crypto_pwhash_memlimit_moderate(); + } + + /** + * @return A memory limit suitable for sensitive use-cases (1073741824). + */ + public static long sensitiveMemLimit() { + return Sodium.crypto_pwhash_memlimit_sensitive(); + } + + /** + * @return The maximum memory limit. + */ + public static long maxMemLimit() { + return Sodium.crypto_pwhash_memlimit_max(); + } + + private static void assertMemLimit(long memLimit) { + if (memLimit < minMemLimit() || memLimit > maxMemLimit()) { + throw new IllegalArgumentException("memLimit out of range"); + } + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretBox.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretBox.java new file mode 100644 index 00000000..cc91b302 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretBox.java @@ -0,0 +1,402 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import javax.annotation.Nullable; + +import jnr.ffi.Pointer; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/authenticated_encryption.md + +/** + * Secret-key authenticated encryption. + * + *

+ * Encrypts a message with a key and a nonce to keep it confidential, and computes an authentication tag. The tag is + * used to make sure that the message hasn't been tampered with before decrypting it. + * + *

+ * A single key is used both to encrypt/sign and verify/decrypt messages. For this reason, it is critical to keep the + * key confidential. + * + *

+ * The nonce doesn't have to be confidential, but it should never ever be reused with the same key. The easiest way to + * generate a nonce is to use randombytes_buf(). + * + *

+ * Messages encrypted are assumed to be independent. If multiple messages are sent using this API and random nonces, + * there will be no way to detect if a message has been received twice, or if messages have been reordered. + * + *

+ * This class depends upon the JNR-FFI library being available on the classpath, along with its dependencies. See + * https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +public final class SecretBox { + private SecretBox() {} + + /** + * A SecretBox key. + */ + public static final class Key { + private final Pointer ptr; + + private Key(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_secretbox_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_secretbox_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_secretbox_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_secretbox_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + Pointer ptr = Sodium.malloc(length()); + try { + Sodium.crypto_secretbox_keygen(ptr); + return new Key(ptr); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A SecretBox nonce. + */ + public static final class Nonce { + private final Pointer ptr; + + private Nonce(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_secretbox_noncebytes()) { + throw new IllegalArgumentException( + "nonce must be " + Sodium.crypto_secretbox_noncebytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + */ + public static int length() { + long noncebytes = Sodium.crypto_secretbox_noncebytes(); + if (noncebytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_secretbox_noncebytes: " + noncebytes + " is too large"); + } + return (int) noncebytes; + } + + /** + * Generate a new {@link Nonce} using a random generator. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

+ * Note that this is not synchronized. If multiple threads are creating encrypted messages and incrementing this + * nonce, then external synchronization is required to ensure no two encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(ptr, length(), Nonce::new); + } + + /** + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + byte[] cipherText = new byte[combinedCypherTextLength(message)]; + + int rc = Sodium.crypto_secretbox_easy(cipherText, message, message.length, nonce.ptr, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_secretbox_easy: failed with result " + rc); + } + + return cipherText; + } + + private static int combinedCypherTextLength(byte[] message) { + long macbytes = Sodium.crypto_secretbox_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_secretbox_macbytes: " + macbytes + " is too large"); + } + return (int) macbytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + byte[] cipherText = new byte[message.length]; + long macbytes = Sodium.crypto_secretbox_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_secretbox_macbytes: " + macbytes + " is too large"); + } + byte[] mac = new byte[(int) macbytes]; + + int rc = Sodium.crypto_secretbox_detached(cipherText, mac, message, message.length, nonce.ptr, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_secretbox_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + byte[] clearText = new byte[clearTextLength(cipherText)]; + + int rc = Sodium.crypto_secretbox_open_easy(clearText, cipherText, cipherText.length, nonce.ptr, key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_easy: failed with result " + rc); + } + + return clearText; + } + + private static int clearTextLength(byte[] cipherText) { + long macbytes = Sodium.crypto_secretbox_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_secretbox_macbytes: " + macbytes + " is too large"); + } + if (macbytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) macbytes); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + long macbytes = Sodium.crypto_secretbox_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_secretbox_macbytes: " + macbytes + " is too large"); + } + if (mac.length != macbytes) { + throw new IllegalArgumentException("mac must be " + macbytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium.crypto_secretbox_open_detached(clearText, cipherText, mac, cipherText.length, nonce.ptr, key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_detached: failed with result " + rc); + } + + return clearText; + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretDecryptionStream.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretDecryptionStream.java new file mode 100644 index 00000000..8774c707 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretDecryptionStream.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +/** + * Used to decrypt a sequence of messages, or a single message split into arbitrary chunks. + */ +public interface SecretDecryptionStream { + + /** + * Pull a message from this secret stream. + * + * @param cipherText The encrypted message. + * @return The clear text. + */ + default Bytes pull(Bytes cipherText) { + return Bytes.wrap(pull(cipherText.toArrayUnsafe())); + } + + /** + * Pull a message from this secret stream. + * + * @param cipherText The encrypted message. + * @return The clear text. + */ + byte[] pull(byte[] cipherText); + + /** @return true if no more messages should be decrypted by this stream */ + boolean isComplete(); +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretEncryptionStream.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretEncryptionStream.java new file mode 100644 index 00000000..72479726 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SecretEncryptionStream.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +/** + * Used to encrypt a sequence of messages, or a single message split into arbitrary chunks. + */ +public interface SecretEncryptionStream { + + /** @return The header for the stream. */ + default Bytes header() { + return Bytes.wrap(headerArray()); + } + + /** @return The header for the stream. */ + byte[] headerArray(); + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default Bytes push(Bytes clearText) { + return push(clearText, false); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default byte[] push(byte[] clearText) { + return push(clearText, false); + } + + /** + * Push the final message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default Bytes pushLast(Bytes clearText) { + return push(clearText, true); + } + + /** + * Push the final message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default byte[] pushLast(byte[] clearText) { + return push(clearText, true); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @param isFinal true if this is the final message that will be sent on this stream. + * @return The encrypted message. + */ + default Bytes push(Bytes clearText, boolean isFinal) { + return Bytes.wrap(push(clearText.toArrayUnsafe(), isFinal)); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @param isFinal true if this is the final message that will be sent on this stream. + * @return The encrypted message. + */ + byte[] push(byte[] clearText, boolean isFinal); +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/Sodium.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Sodium.java new file mode 100644 index 00000000..76ff883f --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/Sodium.java @@ -0,0 +1,2911 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nullable; + +import jnr.ffi.LibraryLoader; +import jnr.ffi.Platform; +import jnr.ffi.Pointer; +import jnr.ffi.byref.ByteByReference; +import jnr.ffi.byref.LongLongByReference; + +/** + * Access to the sodium native library. + * + *

+ * This class provides static methods for checking or loading the sodium native library. + */ +public final class Sodium { + private Sodium() {} + + private static final int SUPPORTED_MAJOR_VERSION = 10; + private static final int SUPPORTED_MIN_MINOR_VERSION = 1; + + /** + * @return The major version of the sodium native library that this binding is based on. + */ + public static int supportedMajorVersion() { + return SUPPORTED_MAJOR_VERSION; + } + + /** + * @return The minor version of the sodium native library that this binding is based on. + */ + public static int supportedMinMinorVersion() { + return SUPPORTED_MIN_MINOR_VERSION; + } + + private static final String LIBRARY_NAME; + static { + try { + Class.forName("jnr.ffi.Platform"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("JNR-FFI is not available on the classpath, see https://github.com/jnr/jnr-ffi"); + } + switch (Platform.getNativePlatform().getOS()) { + case WINDOWS: + LIBRARY_NAME = "libsodium"; + break; + default: + LIBRARY_NAME = "sodium"; + break; + } + } + + private static volatile LibSodium libSodium = null; + + /** + * Load and initialize the native libsodium shared library. + * + *

+ * If this method returns successfully (without throwing a {@link LinkageError}), then all future calls to methods + * provided by this class will use the loaded library. + * + * @param path The path to the shared library. + * @throws LinkageError If the library cannot be found, dependent libraries are missing, or cannot be initialized. + */ + public static void loadLibrary(Path path) { + requireNonNull(path); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Non-existent path"); + } + + Path dir = path.getParent(); + Path library = path.getFileName(); + + LibSodium lib = + LibraryLoader.create(LibSodium.class).search(dir.toFile().getAbsolutePath()).load(library.toString()); + initializeLibrary(lib); + + synchronized (LibSodium.class) { + Sodium.libSodium = lib; + } + } + + private static LibSodium libSodium() { + if (libSodium == null) { + synchronized (LibSodium.class) { + if (libSodium == null) { + LibSodium lib = LibraryLoader + .create(LibSodium.class) + .search("/usr/local/lib") + .search("/opt/local/lib") + .search("lib") + .load(LIBRARY_NAME); + libSodium = initializeLibrary(lib); + } + } + } + return libSodium; + } + + private static LibSodium initializeLibrary(LibSodium lib) { + if (SUPPORTED_MAJOR_VERSION != lib.sodium_library_version_major() + || SUPPORTED_MIN_MINOR_VERSION > lib.sodium_library_version_minor()) { + throw new LinkageError("Unsupported libsodium version " + lib.sodium_version_string()); + } + int result = lib.sodium_init(); + if (result == -1) { + throw new LinkageError("Failed to initialize libsodium: sodium_init returned " + result); + } + return lib; + } + + /** + * Check if the sodium library is available. + * + *

+ * If the sodium library has not already been loaded, this will attempt to load and initialize it before returning. + * + * @return true if the library is loaded and available. + */ + public static boolean isAvailable() { + try { + libSodium(); + } catch (LinkageError e) { + return false; + } + return true; + } + + static Pointer malloc(long length) { + Pointer ptr = sodium_malloc(length); + if (ptr == null) { + throw new OutOfMemoryError("Sodium.sodium_malloc failed allocating " + length); + } + return ptr; + } + + static Pointer dup(Pointer src, long length) { + Pointer ptr = malloc(length); + try { + ptr.transferFrom(0, src, 0, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T dup(Pointer src, long length, Function ctr) { + Pointer ptr = Sodium.dup(src, length); + try { + return ctr.apply(ptr); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static Pointer dupAndIncrement(Pointer src, long length) { + Pointer ptr = dup(src, length); + try { + sodium_increment(ptr, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T dupAndIncrement(Pointer src, long length, Function ctr) { + Pointer ptr = Sodium.dupAndIncrement(src, length); + try { + return ctr.apply(ptr); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static Pointer dup(byte[] bytes) { + Pointer ptr = malloc(bytes.length); + try { + ptr.put(0, bytes, 0, bytes.length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T dup(byte[] bytes, Function ctr) { + Pointer ptr = Sodium.dup(bytes); + try { + return ctr.apply(ptr); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static byte[] reify(Pointer ptr, int length) { + byte[] bytes = new byte[length]; + ptr.get(0, bytes, 0, bytes.length); + return bytes; + } + + static Pointer randomBytes(int length) { + Pointer ptr = malloc(length); + try { + randombytes_buf(ptr, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T randomBytes(int length, Function ctr) { + Pointer ptr = Sodium.randomBytes(length); + try { + return ctr.apply(ptr); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T scalarMultBase(Pointer src, long length, BiFunction ctr) { + if (length != Sodium.crypto_scalarmult_scalarbytes()) { + throw new IllegalArgumentException( + "key length is " + length + " but required " + Sodium.crypto_scalarmult_scalarbytes()); + } + long sbytes = Sodium.crypto_scalarmult_bytes(); + Pointer dst = malloc(Sodium.crypto_scalarmult_bytes()); + try { + int rc = Sodium.crypto_scalarmult_base(dst, src); + if (rc != 0) { + throw new SodiumException("crypto_scalarmult_base: failed with result " + rc); + } + return ctr.apply(dst, sbytes); + } catch (Throwable e) { + sodium_free(dst); + throw e; + } + } + + ///////// + // Generated with https://gist.github.com/cleishm/39fbad03378f5e1ad82521ad821cd065, then modified + + static String sodium_version_string() { + return libSodium().sodium_version_string(); + } + + static int sodium_library_version_major() { + return libSodium().sodium_library_version_major(); + } + + static int sodium_library_version_minor() { + return libSodium().sodium_library_version_minor(); + } + + static int sodium_library_minimal() { + return libSodium().sodium_library_minimal(); + } + + static int sodium_set_misuse_handler(Pointer handler) { + return libSodium().sodium_set_misuse_handler(handler); + } + + static void sodium_misuse() { + libSodium().sodium_misuse(); + } + + static int crypto_aead_aes256gcm_is_available() { + return libSodium().crypto_aead_aes256gcm_is_available(); + } + + static long crypto_aead_aes256gcm_keybytes() { + return libSodium().crypto_aead_aes256gcm_keybytes(); + } + + static long crypto_aead_aes256gcm_nsecbytes() { + return libSodium().crypto_aead_aes256gcm_nsecbytes(); + } + + static long crypto_aead_aes256gcm_npubbytes() { + return libSodium().crypto_aead_aes256gcm_npubbytes(); + } + + static long crypto_aead_aes256gcm_abytes() { + return libSodium().crypto_aead_aes256gcm_abytes(); + } + + static long crypto_aead_aes256gcm_messagebytes_max() { + return libSodium().crypto_aead_aes256gcm_messagebytes_max(); + } + + static long crypto_aead_aes256gcm_statebytes() { + return libSodium().crypto_aead_aes256gcm_statebytes(); + } + + static int crypto_aead_aes256gcm_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_aes256gcm_decrypt( + byte[] m, + LongLongByReference mlen_p, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_aes256gcm_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_encrypt_detached(c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_aes256gcm_decrypt_detached( + byte[] m, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static int crypto_aead_aes256gcm_beforenm(Pointer ctx_, Pointer k) { + return libSodium().crypto_aead_aes256gcm_beforenm(ctx_, k); + } + + static int crypto_aead_aes256gcm_encrypt_afternm( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer ctx_) { + return libSodium().crypto_aead_aes256gcm_encrypt_afternm(c, clen_p, m, mlen, ad, adlen, nsec, npub, ctx_); + } + + static int crypto_aead_aes256gcm_decrypt_afternm( + byte[] m, + LongLongByReference mlen_p, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer ctx_) { + return libSodium().crypto_aead_aes256gcm_decrypt_afternm(m, mlen_p, nsec, c, clen, ad, adlen, npub, ctx_); + } + + static int crypto_aead_aes256gcm_encrypt_detached_afternm( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer ctx_) { + return libSodium() + .crypto_aead_aes256gcm_encrypt_detached_afternm(c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, ctx_); + } + + static int crypto_aead_aes256gcm_decrypt_detached_afternm( + byte[] m, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer ctx_) { + return libSodium().crypto_aead_aes256gcm_decrypt_detached_afternm(m, nsec, c, clen, mac, ad, adlen, npub, ctx_); + } + + static void crypto_aead_aes256gcm_keygen(Pointer k) { + libSodium().crypto_aead_aes256gcm_keygen(k); + } + + static long crypto_aead_chacha20poly1305_ietf_keybytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_keybytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_nsecbytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_nsecbytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_npubbytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_npubbytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_abytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_abytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_messagebytes_max() { + return libSodium().crypto_aead_chacha20poly1305_ietf_messagebytes_max(); + } + + static int crypto_aead_chacha20poly1305_ietf_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_ietf_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_decrypt( + byte[] m, + LongLongByReference mlen_p, + byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_ietf_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_ietf_encrypt_detached(c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_decrypt_detached( + byte[] m, + byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_ietf_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_chacha20poly1305_ietf_keygen(byte[] k) { + libSodium().crypto_aead_chacha20poly1305_ietf_keygen(k); + } + + static long crypto_aead_chacha20poly1305_keybytes() { + return libSodium().crypto_aead_chacha20poly1305_keybytes(); + } + + static long crypto_aead_chacha20poly1305_nsecbytes() { + return libSodium().crypto_aead_chacha20poly1305_nsecbytes(); + } + + static long crypto_aead_chacha20poly1305_npubbytes() { + return libSodium().crypto_aead_chacha20poly1305_npubbytes(); + } + + static long crypto_aead_chacha20poly1305_abytes() { + return libSodium().crypto_aead_chacha20poly1305_abytes(); + } + + static long crypto_aead_chacha20poly1305_messagebytes_max() { + return libSodium().crypto_aead_chacha20poly1305_messagebytes_max(); + } + + static int crypto_aead_chacha20poly1305_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_decrypt( + byte[] m, + LongLongByReference mlen_p, + byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_chacha20poly1305_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_encrypt_detached(c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_decrypt_detached( + byte[] m, + byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium().crypto_aead_chacha20poly1305_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_chacha20poly1305_keygen(byte[] k) { + libSodium().crypto_aead_chacha20poly1305_keygen(k); + } + + static long crypto_aead_xchacha20poly1305_ietf_keybytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_keybytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_nsecbytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_nsecbytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_npubbytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_npubbytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_abytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_abytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_messagebytes_max() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_messagebytes_max(); + } + + static int crypto_aead_xchacha20poly1305_ietf_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable byte[] nsec, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_xchacha20poly1305_ietf_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_decrypt( + byte[] m, + LongLongByReference mlen_p, + @Nullable byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_xchacha20poly1305_ietf_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable byte[] nsec, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_xchacha20poly1305_ietf_encrypt_detached(c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + byte[] m, + @Nullable byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_xchacha20poly1305_ietf_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_xchacha20poly1305_ietf_keygen(Pointer k) { + libSodium().crypto_aead_xchacha20poly1305_ietf_keygen(k); + } + + static long crypto_hash_sha512_statebytes() { + return libSodium().crypto_hash_sha512_statebytes(); + } + + static long crypto_hash_sha512_bytes() { + return libSodium().crypto_hash_sha512_bytes(); + } + + static int crypto_hash_sha512(byte[] out, byte[] in, long inlen) { + return libSodium().crypto_hash_sha512(out, in, inlen); + } + + static int crypto_hash_sha512_init(Pointer state) { + return libSodium().crypto_hash_sha512_init(state); + } + + static int crypto_hash_sha512_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_hash_sha512_update(state, in, inlen); + } + + static int crypto_hash_sha512_final(Pointer state, byte[] out) { + return libSodium().crypto_hash_sha512_final(state, out); + } + + static long crypto_auth_hmacsha512_bytes() { + return libSodium().crypto_auth_hmacsha512_bytes(); + } + + static long crypto_auth_hmacsha512_keybytes() { + return libSodium().crypto_auth_hmacsha512_keybytes(); + } + + static int crypto_auth_hmacsha512(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha512(out, in, inlen, k); + } + + static int crypto_auth_hmacsha512_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha512_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha512_statebytes() { + return libSodium().crypto_auth_hmacsha512_statebytes(); + } + + static int crypto_auth_hmacsha512_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha512_init(state, key, keylen); + } + + static int crypto_auth_hmacsha512_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha512_update(state, in, inlen); + } + + static int crypto_auth_hmacsha512_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha512_final(state, out); + } + + static void crypto_auth_hmacsha512_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha512_keygen(k); + } + + static long crypto_auth_hmacsha512256_bytes() { + return libSodium().crypto_auth_hmacsha512256_bytes(); + } + + static long crypto_auth_hmacsha512256_keybytes() { + return libSodium().crypto_auth_hmacsha512256_keybytes(); + } + + static int crypto_auth_hmacsha512256(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha512256(out, in, inlen, k); + } + + static int crypto_auth_hmacsha512256_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha512256_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha512256_statebytes() { + return libSodium().crypto_auth_hmacsha512256_statebytes(); + } + + static int crypto_auth_hmacsha512256_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha512256_init(state, key, keylen); + } + + static int crypto_auth_hmacsha512256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha512256_update(state, in, inlen); + } + + static int crypto_auth_hmacsha512256_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha512256_final(state, out); + } + + static void crypto_auth_hmacsha512256_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha512256_keygen(k); + } + + static long crypto_auth_bytes() { + return libSodium().crypto_auth_bytes(); + } + + static long crypto_auth_keybytes() { + return libSodium().crypto_auth_keybytes(); + } + + static String crypto_auth_primitive() { + return libSodium().crypto_auth_primitive(); + } + + static int crypto_auth(byte[] out, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth(out, in, inlen, k); + } + + static int crypto_auth_verify(byte[] h, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_verify(h, in, inlen, k); + } + + static void crypto_auth_keygen(Pointer k) { + libSodium().crypto_auth_keygen(k); + } + + static long crypto_hash_sha256_statebytes() { + return libSodium().crypto_hash_sha256_statebytes(); + } + + static long crypto_hash_sha256_bytes() { + return libSodium().crypto_hash_sha256_bytes(); + } + + static int crypto_hash_sha256(byte[] out, byte[] in, long inlen) { + return libSodium().crypto_hash_sha256(out, in, inlen); + } + + static int crypto_hash_sha256_init(Pointer state) { + return libSodium().crypto_hash_sha256_init(state); + } + + static int crypto_hash_sha256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_hash_sha256_update(state, in, inlen); + } + + static int crypto_hash_sha256_final(Pointer state, byte[] out) { + return libSodium().crypto_hash_sha256_final(state, out); + } + + static long crypto_auth_hmacsha256_bytes() { + return libSodium().crypto_auth_hmacsha256_bytes(); + } + + static long crypto_auth_hmacsha256_keybytes() { + return libSodium().crypto_auth_hmacsha256_keybytes(); + } + + static int crypto_auth_hmacsha256(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha256(out, in, inlen, k); + } + + static int crypto_auth_hmacsha256_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_auth_hmacsha256_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha256_statebytes() { + return libSodium().crypto_auth_hmacsha256_statebytes(); + } + + static int crypto_auth_hmacsha256_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha256_init(state, key, keylen); + } + + static int crypto_auth_hmacsha256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha256_update(state, in, inlen); + } + + static int crypto_auth_hmacsha256_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha256_final(state, out); + } + + static void crypto_auth_hmacsha256_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha256_keygen(k); + } + + static long crypto_stream_xsalsa20_keybytes() { + return libSodium().crypto_stream_xsalsa20_keybytes(); + } + + static long crypto_stream_xsalsa20_noncebytes() { + return libSodium().crypto_stream_xsalsa20_noncebytes(); + } + + static long crypto_stream_xsalsa20_messagebytes_max() { + return libSodium().crypto_stream_xsalsa20_messagebytes_max(); + } + + static int crypto_stream_xsalsa20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xsalsa20(c, clen, n, k); + } + + static int crypto_stream_xsalsa20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xsalsa20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_xsalsa20_xor_ic(byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_xsalsa20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_xsalsa20_keygen(byte[] k) { + libSodium().crypto_stream_xsalsa20_keygen(k); + } + + static long crypto_box_curve25519xsalsa20poly1305_seedbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_seedbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_publickeybytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_publickeybytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_secretkeybytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_secretkeybytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_beforenmbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_beforenmbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_noncebytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_noncebytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_macbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_macbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_messagebytes_max() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_messagebytes_max(); + } + + static int crypto_box_curve25519xsalsa20poly1305_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_seed_keypair(pk, sk, seed); + } + + static int crypto_box_curve25519xsalsa20poly1305_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_keypair(pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_beforenm(Pointer k, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_beforenm(k, pk, sk); + } + + static long crypto_box_curve25519xsalsa20poly1305_boxzerobytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_boxzerobytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_zerobytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_zerobytes(); + } + + static int crypto_box_curve25519xsalsa20poly1305(byte[] c, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305(c, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_open(byte[] m, byte[] c, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_open(m, c, clen, n, pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_afternm(byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_afternm(c, m, mlen, n, k); + } + + static int crypto_box_curve25519xsalsa20poly1305_open_afternm(byte[] m, byte[] c, long clen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_open_afternm(m, c, clen, n, k); + } + + static long crypto_box_seedbytes() { + return libSodium().crypto_box_seedbytes(); + } + + static long crypto_box_publickeybytes() { + return libSodium().crypto_box_publickeybytes(); + } + + static long crypto_box_secretkeybytes() { + return libSodium().crypto_box_secretkeybytes(); + } + + static long crypto_box_noncebytes() { + return libSodium().crypto_box_noncebytes(); + } + + static long crypto_box_macbytes() { + return libSodium().crypto_box_macbytes(); + } + + static long crypto_box_messagebytes_max() { + return libSodium().crypto_box_messagebytes_max(); + } + + static String crypto_box_primitive() { + return libSodium().crypto_box_primitive(); + } + + static int crypto_box_seed_keypair(Pointer pk, Pointer sk, Pointer seed) { + return libSodium().crypto_box_seed_keypair(pk, sk, seed); + } + + static int crypto_box_keypair(Pointer pk, Pointer sk) { + return libSodium().crypto_box_keypair(pk, sk); + } + + static int crypto_box_easy(byte[] c, byte[] m, long mlen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_easy(c, m, mlen, n, pk, sk); + } + + static int crypto_box_open_easy(byte[] m, byte[] c, long clen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_open_easy(m, c, clen, n, pk, sk); + } + + static int crypto_box_detached(byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_detached(c, mac, m, mlen, n, pk, sk); + } + + static int crypto_box_open_detached(byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_open_detached(m, c, mac, clen, n, pk, sk); + } + + static long crypto_box_beforenmbytes() { + return libSodium().crypto_box_beforenmbytes(); + } + + static int crypto_box_beforenm(Pointer k, Pointer pk, Pointer sk) { + return libSodium().crypto_box_beforenm(k, pk, sk); + } + + static int crypto_box_easy_afternm(byte[] c, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_box_easy_afternm(c, m, mlen, n, k); + } + + static int crypto_box_open_easy_afternm(byte[] m, byte[] c, long clen, Pointer n, Pointer k) { + return libSodium().crypto_box_open_easy_afternm(m, c, clen, n, k); + } + + static int crypto_box_detached_afternm(byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_box_detached_afternm(c, mac, m, mlen, n, k); + } + + static int crypto_box_open_detached_afternm(byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer k) { + return libSodium().crypto_box_open_detached_afternm(m, c, mac, clen, n, k); + } + + static long crypto_box_sealbytes() { + return libSodium().crypto_box_sealbytes(); + } + + static int crypto_box_seal(byte[] c, byte[] m, long mlen, Pointer pk) { + return libSodium().crypto_box_seal(c, m, mlen, pk); + } + + static int crypto_box_seal_open(byte[] m, byte[] c, long clen, Pointer pk, Pointer sk) { + return libSodium().crypto_box_seal_open(m, c, clen, pk, sk); + } + + static long crypto_box_zerobytes() { + return libSodium().crypto_box_zerobytes(); + } + + static long crypto_box_boxzerobytes() { + return libSodium().crypto_box_boxzerobytes(); + } + + static int crypto_box(byte[] c, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box(c, m, mlen, n, pk, sk); + } + + static int crypto_box_open(byte[] m, byte[] c, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_open(m, c, clen, n, pk, sk); + } + + static int crypto_box_afternm(byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_afternm(c, m, mlen, n, k); + } + + static int crypto_box_open_afternm(byte[] m, byte[] c, long clen, byte[] n, Pointer k) { + return libSodium().crypto_box_open_afternm(m, c, clen, n, k); + } + + static long crypto_core_hsalsa20_outputbytes() { + return libSodium().crypto_core_hsalsa20_outputbytes(); + } + + static long crypto_core_hsalsa20_inputbytes() { + return libSodium().crypto_core_hsalsa20_inputbytes(); + } + + static long crypto_core_hsalsa20_keybytes() { + return libSodium().crypto_core_hsalsa20_keybytes(); + } + + static long crypto_core_hsalsa20_constbytes() { + return libSodium().crypto_core_hsalsa20_constbytes(); + } + + static int crypto_core_hsalsa20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_hsalsa20(out, in, k, c); + } + + static long crypto_core_hchacha20_outputbytes() { + return libSodium().crypto_core_hchacha20_outputbytes(); + } + + static long crypto_core_hchacha20_inputbytes() { + return libSodium().crypto_core_hchacha20_inputbytes(); + } + + static long crypto_core_hchacha20_keybytes() { + return libSodium().crypto_core_hchacha20_keybytes(); + } + + static long crypto_core_hchacha20_constbytes() { + return libSodium().crypto_core_hchacha20_constbytes(); + } + + static int crypto_core_hchacha20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_hchacha20(out, in, k, c); + } + + static long crypto_core_salsa20_outputbytes() { + return libSodium().crypto_core_salsa20_outputbytes(); + } + + static long crypto_core_salsa20_inputbytes() { + return libSodium().crypto_core_salsa20_inputbytes(); + } + + static long crypto_core_salsa20_keybytes() { + return libSodium().crypto_core_salsa20_keybytes(); + } + + static long crypto_core_salsa20_constbytes() { + return libSodium().crypto_core_salsa20_constbytes(); + } + + static int crypto_core_salsa20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa20(out, in, k, c); + } + + static long crypto_core_salsa2012_outputbytes() { + return libSodium().crypto_core_salsa2012_outputbytes(); + } + + static long crypto_core_salsa2012_inputbytes() { + return libSodium().crypto_core_salsa2012_inputbytes(); + } + + static long crypto_core_salsa2012_keybytes() { + return libSodium().crypto_core_salsa2012_keybytes(); + } + + static long crypto_core_salsa2012_constbytes() { + return libSodium().crypto_core_salsa2012_constbytes(); + } + + static int crypto_core_salsa2012(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa2012(out, in, k, c); + } + + static long crypto_core_salsa208_outputbytes() { + return libSodium().crypto_core_salsa208_outputbytes(); + } + + static long crypto_core_salsa208_inputbytes() { + return libSodium().crypto_core_salsa208_inputbytes(); + } + + static long crypto_core_salsa208_keybytes() { + return libSodium().crypto_core_salsa208_keybytes(); + } + + static long crypto_core_salsa208_constbytes() { + return libSodium().crypto_core_salsa208_constbytes(); + } + + static int crypto_core_salsa208(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa208(out, in, k, c); + } + + static long crypto_generichash_blake2b_bytes_min() { + return libSodium().crypto_generichash_blake2b_bytes_min(); + } + + static long crypto_generichash_blake2b_bytes_max() { + return libSodium().crypto_generichash_blake2b_bytes_max(); + } + + static long crypto_generichash_blake2b_bytes() { + return libSodium().crypto_generichash_blake2b_bytes(); + } + + static long crypto_generichash_blake2b_keybytes_min() { + return libSodium().crypto_generichash_blake2b_keybytes_min(); + } + + static long crypto_generichash_blake2b_keybytes_max() { + return libSodium().crypto_generichash_blake2b_keybytes_max(); + } + + static long crypto_generichash_blake2b_keybytes() { + return libSodium().crypto_generichash_blake2b_keybytes(); + } + + static long crypto_generichash_blake2b_saltbytes() { + return libSodium().crypto_generichash_blake2b_saltbytes(); + } + + static long crypto_generichash_blake2b_personalbytes() { + return libSodium().crypto_generichash_blake2b_personalbytes(); + } + + static long crypto_generichash_blake2b_statebytes() { + return libSodium().crypto_generichash_blake2b_statebytes(); + } + + static int crypto_generichash_blake2b(byte[] out, long outlen, byte[] in, long inlen, byte[] key, long keylen) { + return libSodium().crypto_generichash_blake2b(out, outlen, in, inlen, key, keylen); + } + + static int crypto_generichash_blake2b_salt_personal( + byte[] out, + long outlen, + byte[] in, + long inlen, + byte[] key, + long keylen, + byte[] salt, + byte[] personal) { + return libSodium().crypto_generichash_blake2b_salt_personal(out, outlen, in, inlen, key, keylen, salt, personal); + } + + static int crypto_generichash_blake2b_init(Pointer state, byte[] key, long keylen, long outlen) { + return libSodium().crypto_generichash_blake2b_init(state, key, keylen, outlen); + } + + static int crypto_generichash_blake2b_init_salt_personal( + Pointer state, + byte[] key, + long keylen, + long outlen, + byte[] salt, + byte[] personal) { + return libSodium().crypto_generichash_blake2b_init_salt_personal(state, key, keylen, outlen, salt, personal); + } + + static int crypto_generichash_blake2b_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_generichash_blake2b_update(state, in, inlen); + } + + static int crypto_generichash_blake2b_final(Pointer state, byte[] out, long outlen) { + return libSodium().crypto_generichash_blake2b_final(state, out, outlen); + } + + static void crypto_generichash_blake2b_keygen(byte[] k) { + libSodium().crypto_generichash_blake2b_keygen(k); + } + + static long crypto_generichash_bytes_min() { + return libSodium().crypto_generichash_bytes_min(); + } + + static long crypto_generichash_bytes_max() { + return libSodium().crypto_generichash_bytes_max(); + } + + static long crypto_generichash_bytes() { + return libSodium().crypto_generichash_bytes(); + } + + static long crypto_generichash_keybytes_min() { + return libSodium().crypto_generichash_keybytes_min(); + } + + static long crypto_generichash_keybytes_max() { + return libSodium().crypto_generichash_keybytes_max(); + } + + static long crypto_generichash_keybytes() { + return libSodium().crypto_generichash_keybytes(); + } + + static String crypto_generichash_primitive() { + return libSodium().crypto_generichash_primitive(); + } + + static long crypto_generichash_statebytes() { + return libSodium().crypto_generichash_statebytes(); + } + + static int crypto_generichash(byte[] out, long outlen, byte[] in, long inlen, byte[] key, long keylen) { + return libSodium().crypto_generichash(out, outlen, in, inlen, key, keylen); + } + + static int crypto_generichash_init(Pointer state, byte[] key, long keylen, long outlen) { + return libSodium().crypto_generichash_init(state, key, keylen, outlen); + } + + static int crypto_generichash_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_generichash_update(state, in, inlen); + } + + static int crypto_generichash_final(Pointer state, byte[] out, long outlen) { + return libSodium().crypto_generichash_final(state, out, outlen); + } + + static void crypto_generichash_keygen(byte[] k) { + libSodium().crypto_generichash_keygen(k); + } + + static long crypto_hash_bytes() { + return libSodium().crypto_hash_bytes(); + } + + static int crypto_hash(byte[] out, byte[] in, long inlen) { + return libSodium().crypto_hash(out, in, inlen); + } + + static String crypto_hash_primitive() { + return libSodium().crypto_hash_primitive(); + } + + static long crypto_kdf_blake2b_bytes_min() { + return libSodium().crypto_kdf_blake2b_bytes_min(); + } + + static long crypto_kdf_blake2b_bytes_max() { + return libSodium().crypto_kdf_blake2b_bytes_max(); + } + + static long crypto_kdf_blake2b_contextbytes() { + return libSodium().crypto_kdf_blake2b_contextbytes(); + } + + static long crypto_kdf_blake2b_keybytes() { + return libSodium().crypto_kdf_blake2b_keybytes(); + } + + static int crypto_kdf_blake2b_derive_from_key( + byte[] subkey, + long subkey_len, + long subkey_id, + byte[] ctx, + Pointer key) { + return libSodium().crypto_kdf_blake2b_derive_from_key(subkey, subkey_len, subkey_id, ctx, key); + } + + static long crypto_kdf_bytes_min() { + return libSodium().crypto_kdf_bytes_min(); + } + + static long crypto_kdf_bytes_max() { + return libSodium().crypto_kdf_bytes_max(); + } + + static long crypto_kdf_contextbytes() { + return libSodium().crypto_kdf_contextbytes(); + } + + static long crypto_kdf_keybytes() { + return libSodium().crypto_kdf_keybytes(); + } + + static String crypto_kdf_primitive() { + return libSodium().crypto_kdf_primitive(); + } + + static int crypto_kdf_derive_from_key(byte[] subkey, long subkey_len, long subkey_id, byte[] ctx, Pointer key) { + return libSodium().crypto_kdf_derive_from_key(subkey, subkey_len, subkey_id, ctx, key); + } + + static void crypto_kdf_keygen(Pointer k) { + libSodium().crypto_kdf_keygen(k); + } + + static long crypto_kx_publickeybytes() { + return libSodium().crypto_kx_publickeybytes(); + } + + static long crypto_kx_secretkeybytes() { + return libSodium().crypto_kx_secretkeybytes(); + } + + static long crypto_kx_seedbytes() { + return libSodium().crypto_kx_seedbytes(); + } + + static long crypto_kx_sessionkeybytes() { + return libSodium().crypto_kx_sessionkeybytes(); + } + + static String crypto_kx_primitive() { + return libSodium().crypto_kx_primitive(); + } + + static int crypto_kx_seed_keypair(Pointer pk, Pointer sk, Pointer seed) { + return libSodium().crypto_kx_seed_keypair(pk, sk, seed); + } + + static int crypto_kx_keypair(Pointer pk, Pointer sk) { + return libSodium().crypto_kx_keypair(pk, sk); + } + + static int crypto_kx_client_session_keys( + Pointer rx, + Pointer tx, + Pointer client_pk, + Pointer client_sk, + Pointer server_pk) { + return libSodium().crypto_kx_client_session_keys(rx, tx, client_pk, client_sk, server_pk); + } + + static int crypto_kx_server_session_keys( + Pointer rx, + Pointer tx, + Pointer server_pk, + Pointer server_sk, + Pointer client_pk) { + return libSodium().crypto_kx_server_session_keys(rx, tx, server_pk, server_sk, client_pk); + } + + static long crypto_onetimeauth_poly1305_statebytes() { + return libSodium().crypto_onetimeauth_poly1305_statebytes(); + } + + static long crypto_onetimeauth_poly1305_bytes() { + return libSodium().crypto_onetimeauth_poly1305_bytes(); + } + + static long crypto_onetimeauth_poly1305_keybytes() { + return libSodium().crypto_onetimeauth_poly1305_keybytes(); + } + + static int crypto_onetimeauth_poly1305(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_poly1305(out, in, inlen, k); + } + + static int crypto_onetimeauth_poly1305_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_poly1305_verify(h, in, inlen, k); + } + + static int crypto_onetimeauth_poly1305_init(Pointer state, byte[] key) { + return libSodium().crypto_onetimeauth_poly1305_init(state, key); + } + + static int crypto_onetimeauth_poly1305_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_onetimeauth_poly1305_update(state, in, inlen); + } + + static int crypto_onetimeauth_poly1305_final(Pointer state, byte[] out) { + return libSodium().crypto_onetimeauth_poly1305_final(state, out); + } + + static void crypto_onetimeauth_poly1305_keygen(byte[] k) { + libSodium().crypto_onetimeauth_poly1305_keygen(k); + } + + static long crypto_onetimeauth_statebytes() { + return libSodium().crypto_onetimeauth_statebytes(); + } + + static long crypto_onetimeauth_bytes() { + return libSodium().crypto_onetimeauth_bytes(); + } + + static long crypto_onetimeauth_keybytes() { + return libSodium().crypto_onetimeauth_keybytes(); + } + + static String crypto_onetimeauth_primitive() { + return libSodium().crypto_onetimeauth_primitive(); + } + + static int crypto_onetimeauth(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth(out, in, inlen, k); + } + + static int crypto_onetimeauth_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_verify(h, in, inlen, k); + } + + static int crypto_onetimeauth_init(Pointer state, byte[] key) { + return libSodium().crypto_onetimeauth_init(state, key); + } + + static int crypto_onetimeauth_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_onetimeauth_update(state, in, inlen); + } + + static int crypto_onetimeauth_final(Pointer state, byte[] out) { + return libSodium().crypto_onetimeauth_final(state, out); + } + + static void crypto_onetimeauth_keygen(byte[] k) { + libSodium().crypto_onetimeauth_keygen(k); + } + + static int crypto_pwhash_argon2i_alg_argon2i13() { + return libSodium().crypto_pwhash_argon2i_alg_argon2i13(); + } + + static long crypto_pwhash_argon2i_bytes_min() { + return libSodium().crypto_pwhash_argon2i_bytes_min(); + } + + static long crypto_pwhash_argon2i_bytes_max() { + return libSodium().crypto_pwhash_argon2i_bytes_max(); + } + + static long crypto_pwhash_argon2i_passwd_min() { + return libSodium().crypto_pwhash_argon2i_passwd_min(); + } + + static long crypto_pwhash_argon2i_passwd_max() { + return libSodium().crypto_pwhash_argon2i_passwd_max(); + } + + static long crypto_pwhash_argon2i_saltbytes() { + return libSodium().crypto_pwhash_argon2i_saltbytes(); + } + + static long crypto_pwhash_argon2i_strbytes() { + return libSodium().crypto_pwhash_argon2i_strbytes(); + } + + static String crypto_pwhash_argon2i_strprefix() { + return libSodium().crypto_pwhash_argon2i_strprefix(); + } + + static long crypto_pwhash_argon2i_opslimit_min() { + return libSodium().crypto_pwhash_argon2i_opslimit_min(); + } + + static long crypto_pwhash_argon2i_opslimit_max() { + return libSodium().crypto_pwhash_argon2i_opslimit_max(); + } + + static long crypto_pwhash_argon2i_memlimit_min() { + return libSodium().crypto_pwhash_argon2i_memlimit_min(); + } + + static long crypto_pwhash_argon2i_memlimit_max() { + return libSodium().crypto_pwhash_argon2i_memlimit_max(); + } + + static long crypto_pwhash_argon2i_opslimit_interactive() { + return libSodium().crypto_pwhash_argon2i_opslimit_interactive(); + } + + static long crypto_pwhash_argon2i_memlimit_interactive() { + return libSodium().crypto_pwhash_argon2i_memlimit_interactive(); + } + + static long crypto_pwhash_argon2i_opslimit_moderate() { + return libSodium().crypto_pwhash_argon2i_opslimit_moderate(); + } + + static long crypto_pwhash_argon2i_memlimit_moderate() { + return libSodium().crypto_pwhash_argon2i_memlimit_moderate(); + } + + static long crypto_pwhash_argon2i_opslimit_sensitive() { + return libSodium().crypto_pwhash_argon2i_opslimit_sensitive(); + } + + static long crypto_pwhash_argon2i_memlimit_sensitive() { + return libSodium().crypto_pwhash_argon2i_memlimit_sensitive(); + } + + static int crypto_pwhash_argon2i( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit, + int alg) { + return libSodium().crypto_pwhash_argon2i(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_argon2i_str(byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2i_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_argon2i_str_verify(byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_argon2i_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_argon2i_str_needs_rehash(byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2i_str_needs_rehash(str, opslimit, memlimit); + } + + static int crypto_pwhash_argon2id_alg_argon2id13() { + return libSodium().crypto_pwhash_argon2id_alg_argon2id13(); + } + + static long crypto_pwhash_argon2id_bytes_min() { + return libSodium().crypto_pwhash_argon2id_bytes_min(); + } + + static long crypto_pwhash_argon2id_bytes_max() { + return libSodium().crypto_pwhash_argon2id_bytes_max(); + } + + static long crypto_pwhash_argon2id_passwd_min() { + return libSodium().crypto_pwhash_argon2id_passwd_min(); + } + + static long crypto_pwhash_argon2id_passwd_max() { + return libSodium().crypto_pwhash_argon2id_passwd_max(); + } + + static long crypto_pwhash_argon2id_saltbytes() { + return libSodium().crypto_pwhash_argon2id_saltbytes(); + } + + static long crypto_pwhash_argon2id_strbytes() { + return libSodium().crypto_pwhash_argon2id_strbytes(); + } + + static String crypto_pwhash_argon2id_strprefix() { + return libSodium().crypto_pwhash_argon2id_strprefix(); + } + + static long crypto_pwhash_argon2id_opslimit_min() { + return libSodium().crypto_pwhash_argon2id_opslimit_min(); + } + + static long crypto_pwhash_argon2id_opslimit_max() { + return libSodium().crypto_pwhash_argon2id_opslimit_max(); + } + + static long crypto_pwhash_argon2id_memlimit_min() { + return libSodium().crypto_pwhash_argon2id_memlimit_min(); + } + + static long crypto_pwhash_argon2id_memlimit_max() { + return libSodium().crypto_pwhash_argon2id_memlimit_max(); + } + + static long crypto_pwhash_argon2id_opslimit_interactive() { + return libSodium().crypto_pwhash_argon2id_opslimit_interactive(); + } + + static long crypto_pwhash_argon2id_memlimit_interactive() { + return libSodium().crypto_pwhash_argon2id_memlimit_interactive(); + } + + static long crypto_pwhash_argon2id_opslimit_moderate() { + return libSodium().crypto_pwhash_argon2id_opslimit_moderate(); + } + + static long crypto_pwhash_argon2id_memlimit_moderate() { + return libSodium().crypto_pwhash_argon2id_memlimit_moderate(); + } + + static long crypto_pwhash_argon2id_opslimit_sensitive() { + return libSodium().crypto_pwhash_argon2id_opslimit_sensitive(); + } + + static long crypto_pwhash_argon2id_memlimit_sensitive() { + return libSodium().crypto_pwhash_argon2id_memlimit_sensitive(); + } + + static int crypto_pwhash_argon2id( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit, + int alg) { + return libSodium().crypto_pwhash_argon2id(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_argon2id_str(byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2id_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_argon2id_str_verify(byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_argon2id_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_argon2id_str_needs_rehash(byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2id_str_needs_rehash(str, opslimit, memlimit); + } + + static int crypto_pwhash_alg_argon2i13() { + return libSodium().crypto_pwhash_alg_argon2i13(); + } + + static int crypto_pwhash_alg_argon2id13() { + return libSodium().crypto_pwhash_alg_argon2id13(); + } + + static int crypto_pwhash_alg_default() { + return libSodium().crypto_pwhash_alg_default(); + } + + static long crypto_pwhash_bytes_min() { + return libSodium().crypto_pwhash_bytes_min(); + } + + static long crypto_pwhash_bytes_max() { + return libSodium().crypto_pwhash_bytes_max(); + } + + static long crypto_pwhash_passwd_min() { + return libSodium().crypto_pwhash_passwd_min(); + } + + static long crypto_pwhash_passwd_max() { + return libSodium().crypto_pwhash_passwd_max(); + } + + static long crypto_pwhash_saltbytes() { + return libSodium().crypto_pwhash_saltbytes(); + } + + static long crypto_pwhash_strbytes() { + return libSodium().crypto_pwhash_strbytes(); + } + + static String crypto_pwhash_strprefix() { + return libSodium().crypto_pwhash_strprefix(); + } + + static long crypto_pwhash_opslimit_min() { + return libSodium().crypto_pwhash_opslimit_min(); + } + + static long crypto_pwhash_opslimit_max() { + return libSodium().crypto_pwhash_opslimit_max(); + } + + static long crypto_pwhash_memlimit_min() { + return libSodium().crypto_pwhash_memlimit_min(); + } + + static long crypto_pwhash_memlimit_max() { + return libSodium().crypto_pwhash_memlimit_max(); + } + + static long crypto_pwhash_opslimit_interactive() { + return libSodium().crypto_pwhash_opslimit_interactive(); + } + + static long crypto_pwhash_memlimit_interactive() { + return libSodium().crypto_pwhash_memlimit_interactive(); + } + + static long crypto_pwhash_opslimit_moderate() { + return libSodium().crypto_pwhash_opslimit_moderate(); + } + + static long crypto_pwhash_memlimit_moderate() { + return libSodium().crypto_pwhash_memlimit_moderate(); + } + + static long crypto_pwhash_opslimit_sensitive() { + return libSodium().crypto_pwhash_opslimit_sensitive(); + } + + static long crypto_pwhash_memlimit_sensitive() { + return libSodium().crypto_pwhash_memlimit_sensitive(); + } + + static int crypto_pwhash( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + Pointer salt, + long opslimit, + long memlimit, + int alg) { + return libSodium().crypto_pwhash(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_str(byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_str_alg(byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit, int alg) { + return libSodium().crypto_pwhash_str_alg(out, passwd, passwdlen, opslimit, memlimit, alg); + } + + static int crypto_pwhash_str_verify(Pointer str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_str_needs_rehash(Pointer str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_str_needs_rehash(str, opslimit, memlimit); + } + + static String crypto_pwhash_primitive() { + return libSodium().crypto_pwhash_primitive(); + } + + static long crypto_scalarmult_curve25519_bytes() { + return libSodium().crypto_scalarmult_curve25519_bytes(); + } + + static long crypto_scalarmult_curve25519_scalarbytes() { + return libSodium().crypto_scalarmult_curve25519_scalarbytes(); + } + + static int crypto_scalarmult_curve25519(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_curve25519(q, n, p); + } + + static int crypto_scalarmult_curve25519_base(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_curve25519_base(q, n); + } + + static long crypto_scalarmult_bytes() { + return libSodium().crypto_scalarmult_bytes(); + } + + static long crypto_scalarmult_scalarbytes() { + return libSodium().crypto_scalarmult_scalarbytes(); + } + + static String crypto_scalarmult_primitive() { + return libSodium().crypto_scalarmult_primitive(); + } + + static int crypto_scalarmult_base(Pointer q, Pointer n) { + return libSodium().crypto_scalarmult_base(q, n); + } + + static int crypto_scalarmult(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult(q, n, p); + } + + static long crypto_secretbox_xsalsa20poly1305_keybytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_keybytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_noncebytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_noncebytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_macbytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_macbytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_messagebytes_max() { + return libSodium().crypto_secretbox_xsalsa20poly1305_messagebytes_max(); + } + + static int crypto_secretbox_xsalsa20poly1305(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xsalsa20poly1305(c, m, mlen, n, k); + } + + static int crypto_secretbox_xsalsa20poly1305_open(byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xsalsa20poly1305_open(m, c, clen, n, k); + } + + static void crypto_secretbox_xsalsa20poly1305_keygen(byte[] k) { + libSodium().crypto_secretbox_xsalsa20poly1305_keygen(k); + } + + static long crypto_secretbox_xsalsa20poly1305_boxzerobytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_boxzerobytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_zerobytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_zerobytes(); + } + + static long crypto_secretbox_keybytes() { + return libSodium().crypto_secretbox_keybytes(); + } + + static long crypto_secretbox_noncebytes() { + return libSodium().crypto_secretbox_noncebytes(); + } + + static long crypto_secretbox_macbytes() { + return libSodium().crypto_secretbox_macbytes(); + } + + static String crypto_secretbox_primitive() { + return libSodium().crypto_secretbox_primitive(); + } + + static long crypto_secretbox_messagebytes_max() { + return libSodium().crypto_secretbox_messagebytes_max(); + } + + static int crypto_secretbox_easy(byte[] c, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_easy(c, m, mlen, n, k); + } + + static int crypto_secretbox_open_easy(byte[] m, byte[] c, long clen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_open_easy(m, c, clen, n, k); + } + + static int crypto_secretbox_detached(byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_detached(c, mac, m, mlen, n, k); + } + + static int crypto_secretbox_open_detached(byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_open_detached(m, c, mac, clen, n, k); + } + + static void crypto_secretbox_keygen(Pointer k) { + libSodium().crypto_secretbox_keygen(k); + } + + static long crypto_secretbox_zerobytes() { + return libSodium().crypto_secretbox_zerobytes(); + } + + static long crypto_secretbox_boxzerobytes() { + return libSodium().crypto_secretbox_boxzerobytes(); + } + + static int crypto_secretbox(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox(c, m, mlen, n, k); + } + + static int crypto_secretbox_open(byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_open(m, c, clen, n, k); + } + + static long crypto_stream_chacha20_keybytes() { + return libSodium().crypto_stream_chacha20_keybytes(); + } + + static long crypto_stream_chacha20_noncebytes() { + return libSodium().crypto_stream_chacha20_noncebytes(); + } + + static long crypto_stream_chacha20_messagebytes_max() { + return libSodium().crypto_stream_chacha20_messagebytes_max(); + } + + static int crypto_stream_chacha20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20(c, clen, n, k); + } + + static int crypto_stream_chacha20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_chacha20_xor_ic(byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_chacha20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_chacha20_keygen(byte[] k) { + libSodium().crypto_stream_chacha20_keygen(k); + } + + static long crypto_stream_chacha20_ietf_keybytes() { + return libSodium().crypto_stream_chacha20_ietf_keybytes(); + } + + static long crypto_stream_chacha20_ietf_noncebytes() { + return libSodium().crypto_stream_chacha20_ietf_noncebytes(); + } + + static long crypto_stream_chacha20_ietf_messagebytes_max() { + return libSodium().crypto_stream_chacha20_ietf_messagebytes_max(); + } + + static int crypto_stream_chacha20_ietf(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf(c, clen, n, k); + } + + static int crypto_stream_chacha20_ietf_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf_xor(c, m, mlen, n, k); + } + + static int crypto_stream_chacha20_ietf_xor_ic(byte[] c, byte[] m, long mlen, byte[] n, int ic, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_chacha20_ietf_keygen(byte[] k) { + libSodium().crypto_stream_chacha20_ietf_keygen(k); + } + + static long crypto_secretstream_xchacha20poly1305_abytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_abytes(); + } + + static long crypto_secretstream_xchacha20poly1305_headerbytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_headerbytes(); + } + + static long crypto_secretstream_xchacha20poly1305_keybytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_keybytes(); + } + + static long crypto_secretstream_xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_secretstream_xchacha20poly1305_messagebytes_max(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_message() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_message(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_push() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_push(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_rekey() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_rekey(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_final() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_final(); + } + + static long crypto_secretstream_xchacha20poly1305_statebytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_statebytes(); + } + + static void crypto_secretstream_xchacha20poly1305_keygen(Pointer k) { + libSodium().crypto_secretstream_xchacha20poly1305_keygen(k); + } + + static int crypto_secretstream_xchacha20poly1305_init_push(Pointer state, byte[] header, Pointer k) { + return libSodium().crypto_secretstream_xchacha20poly1305_init_push(state, header, k); + } + + static int crypto_secretstream_xchacha20poly1305_push( + Pointer state, + byte[] c, + @Nullable LongLongByReference clen_p, + byte[] m, + long mlen, + @Nullable byte[] ad, + long adlen, + byte tag) { + return libSodium().crypto_secretstream_xchacha20poly1305_push(state, c, clen_p, m, mlen, ad, adlen, tag); + } + + static int crypto_secretstream_xchacha20poly1305_init_pull(Pointer state, byte[] header, Pointer k) { + return libSodium().crypto_secretstream_xchacha20poly1305_init_pull(state, header, k); + } + + static int crypto_secretstream_xchacha20poly1305_pull( + Pointer state, + byte[] m, + @Nullable LongLongByReference mlen_p, + ByteByReference tag_p, + byte[] c, + long clen, + @Nullable byte[] ad, + long adlen) { + return libSodium().crypto_secretstream_xchacha20poly1305_pull(state, m, mlen_p, tag_p, c, clen, ad, adlen); + } + + static void crypto_secretstream_xchacha20poly1305_rekey(Pointer state) { + libSodium().crypto_secretstream_xchacha20poly1305_rekey(state); + } + + static long crypto_shorthash_siphash24_bytes() { + return libSodium().crypto_shorthash_siphash24_bytes(); + } + + static long crypto_shorthash_siphash24_keybytes() { + return libSodium().crypto_shorthash_siphash24_keybytes(); + } + + static int crypto_shorthash_siphash24(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash_siphash24(out, in, inlen, k); + } + + static long crypto_shorthash_siphashx24_bytes() { + return libSodium().crypto_shorthash_siphashx24_bytes(); + } + + static long crypto_shorthash_siphashx24_keybytes() { + return libSodium().crypto_shorthash_siphashx24_keybytes(); + } + + static int crypto_shorthash_siphashx24(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash_siphashx24(out, in, inlen, k); + } + + static long crypto_shorthash_bytes() { + return libSodium().crypto_shorthash_bytes(); + } + + static long crypto_shorthash_keybytes() { + return libSodium().crypto_shorthash_keybytes(); + } + + static String crypto_shorthash_primitive() { + return libSodium().crypto_shorthash_primitive(); + } + + static int crypto_shorthash(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash(out, in, inlen, k); + } + + static void crypto_shorthash_keygen(byte[] k) { + libSodium().crypto_shorthash_keygen(k); + } + + static long crypto_sign_ed25519ph_statebytes() { + return libSodium().crypto_sign_ed25519ph_statebytes(); + } + + static long crypto_sign_ed25519_bytes() { + return libSodium().crypto_sign_ed25519_bytes(); + } + + static long crypto_sign_ed25519_seedbytes() { + return libSodium().crypto_sign_ed25519_seedbytes(); + } + + static long crypto_sign_ed25519_publickeybytes() { + return libSodium().crypto_sign_ed25519_publickeybytes(); + } + + static long crypto_sign_ed25519_secretkeybytes() { + return libSodium().crypto_sign_ed25519_secretkeybytes(); + } + + static long crypto_sign_ed25519_messagebytes_max() { + return libSodium().crypto_sign_ed25519_messagebytes_max(); + } + + static int crypto_sign_ed25519(byte[] sm, LongLongByReference smlen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign_ed25519(sm, smlen_p, m, mlen, sk); + } + + static int crypto_sign_ed25519_open(byte[] m, LongLongByReference mlen_p, byte[] sm, long smlen, byte[] pk) { + return libSodium().crypto_sign_ed25519_open(m, mlen_p, sm, smlen, pk); + } + + static int crypto_sign_ed25519_detached(byte[] sig, LongLongByReference siglen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign_ed25519_detached(sig, siglen_p, m, mlen, sk); + } + + static int crypto_sign_ed25519_verify_detached(byte[] sig, byte[] m, long mlen, byte[] pk) { + return libSodium().crypto_sign_ed25519_verify_detached(sig, m, mlen, pk); + } + + static int crypto_sign_ed25519_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_sign_ed25519_keypair(pk, sk); + } + + static int crypto_sign_ed25519_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_sign_ed25519_seed_keypair(pk, sk, seed); + } + + static int crypto_sign_ed25519_pk_to_curve25519(byte[] curve25519_pk, byte[] ed25519_pk) { + return libSodium().crypto_sign_ed25519_pk_to_curve25519(curve25519_pk, ed25519_pk); + } + + static int crypto_sign_ed25519_sk_to_curve25519(byte[] curve25519_sk, byte[] ed25519_sk) { + return libSodium().crypto_sign_ed25519_sk_to_curve25519(curve25519_sk, ed25519_sk); + } + + static int crypto_sign_ed25519_sk_to_seed(byte[] seed, byte[] sk) { + return libSodium().crypto_sign_ed25519_sk_to_seed(seed, sk); + } + + static int crypto_sign_ed25519_sk_to_pk(byte[] pk, byte[] sk) { + return libSodium().crypto_sign_ed25519_sk_to_pk(pk, sk); + } + + static int crypto_sign_ed25519ph_init(Pointer state) { + return libSodium().crypto_sign_ed25519ph_init(state); + } + + static int crypto_sign_ed25519ph_update(Pointer state, byte[] m, long mlen) { + return libSodium().crypto_sign_ed25519ph_update(state, m, mlen); + } + + static int crypto_sign_ed25519ph_final_create(Pointer state, byte[] sig, LongLongByReference siglen_p, byte[] sk) { + return libSodium().crypto_sign_ed25519ph_final_create(state, sig, siglen_p, sk); + } + + static int crypto_sign_ed25519ph_final_verify(Pointer state, byte[] sig, byte[] pk) { + return libSodium().crypto_sign_ed25519ph_final_verify(state, sig, pk); + } + + static long crypto_sign_statebytes() { + return libSodium().crypto_sign_statebytes(); + } + + static long crypto_sign_bytes() { + return libSodium().crypto_sign_bytes(); + } + + static long crypto_sign_seedbytes() { + return libSodium().crypto_sign_seedbytes(); + } + + static long crypto_sign_publickeybytes() { + return libSodium().crypto_sign_publickeybytes(); + } + + static long crypto_sign_secretkeybytes() { + return libSodium().crypto_sign_secretkeybytes(); + } + + static long crypto_sign_messagebytes_max() { + return libSodium().crypto_sign_messagebytes_max(); + } + + static String crypto_sign_primitive() { + return libSodium().crypto_sign_primitive(); + } + + static int crypto_sign_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_sign_seed_keypair(pk, sk, seed); + } + + static int crypto_sign_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_sign_keypair(pk, sk); + } + + static int crypto_sign(byte[] sm, LongLongByReference smlen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign(sm, smlen_p, m, mlen, sk); + } + + static int crypto_sign_open(byte[] m, LongLongByReference mlen_p, byte[] sm, long smlen, byte[] pk) { + return libSodium().crypto_sign_open(m, mlen_p, sm, smlen, pk); + } + + static int crypto_sign_detached(byte[] sig, LongLongByReference siglen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign_detached(sig, siglen_p, m, mlen, sk); + } + + static int crypto_sign_verify_detached(byte[] sig, byte[] m, long mlen, byte[] pk) { + return libSodium().crypto_sign_verify_detached(sig, m, mlen, pk); + } + + static int crypto_sign_init(Pointer state) { + return libSodium().crypto_sign_init(state); + } + + static int crypto_sign_update(Pointer state, byte[] m, long mlen) { + return libSodium().crypto_sign_update(state, m, mlen); + } + + static int crypto_sign_final_create(Pointer state, byte[] sig, LongLongByReference siglen_p, byte[] sk) { + return libSodium().crypto_sign_final_create(state, sig, siglen_p, sk); + } + + static int crypto_sign_final_verify(Pointer state, byte[] sig, byte[] pk) { + return libSodium().crypto_sign_final_verify(state, sig, pk); + } + + static long crypto_stream_keybytes() { + return libSodium().crypto_stream_keybytes(); + } + + static long crypto_stream_noncebytes() { + return libSodium().crypto_stream_noncebytes(); + } + + static long crypto_stream_messagebytes_max() { + return libSodium().crypto_stream_messagebytes_max(); + } + + static String crypto_stream_primitive() { + return libSodium().crypto_stream_primitive(); + } + + static int crypto_stream(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream(c, clen, n, k); + } + + static int crypto_stream_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xor(c, m, mlen, n, k); + } + + static void crypto_stream_keygen(byte[] k) { + libSodium().crypto_stream_keygen(k); + } + + static long crypto_stream_salsa20_keybytes() { + return libSodium().crypto_stream_salsa20_keybytes(); + } + + static long crypto_stream_salsa20_noncebytes() { + return libSodium().crypto_stream_salsa20_noncebytes(); + } + + static long crypto_stream_salsa20_messagebytes_max() { + return libSodium().crypto_stream_salsa20_messagebytes_max(); + } + + static int crypto_stream_salsa20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa20(c, clen, n, k); + } + + static int crypto_stream_salsa20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_salsa20_xor_ic(byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_salsa20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_salsa20_keygen(byte[] k) { + libSodium().crypto_stream_salsa20_keygen(k); + } + + static long crypto_verify_16_bytes() { + return libSodium().crypto_verify_16_bytes(); + } + + static int crypto_verify_16(byte[] x, byte[] y) { + return libSodium().crypto_verify_16(x, y); + } + + static long crypto_verify_32_bytes() { + return libSodium().crypto_verify_32_bytes(); + } + + static int crypto_verify_32(byte[] x, byte[] y) { + return libSodium().crypto_verify_32(x, y); + } + + static long crypto_verify_64_bytes() { + return libSodium().crypto_verify_64_bytes(); + } + + static int crypto_verify_64(byte[] x, byte[] y) { + return libSodium().crypto_verify_64(x, y); + } + + static String implementation_name() { + return libSodium().implementation_name(); + } + + static int random() { + return libSodium().random(); + } + + static void stir() { + libSodium().stir(); + } + + static int uniform(int upper_bound) { + return libSodium().uniform(upper_bound); + } + + static void buf(byte[] buf, long size) { + libSodium().buf(buf, size); + } + + static int close() { + return libSodium().close(); + } + + static long randombytes_seedbytes() { + return libSodium().randombytes_seedbytes(); + } + + static void randombytes_buf(Pointer buf, long size) { + libSodium().randombytes_buf(buf, size); + } + + static void randombytes_buf_deterministic(byte[] buf, long size, byte[] seed) { + libSodium().randombytes_buf_deterministic(buf, size, seed); + } + + static int randombytes_random() { + return libSodium().randombytes_random(); + } + + static int randombytes_uniform(int upper_bound) { + return libSodium().randombytes_uniform(upper_bound); + } + + static void randombytes_stir() { + libSodium().randombytes_stir(); + } + + static int randombytes_close() { + return libSodium().randombytes_close(); + } + + static int randombytes_set_implementation(Pointer impl) { + return libSodium().randombytes_set_implementation(impl); + } + + static String randombytes_implementation_name() { + return libSodium().randombytes_implementation_name(); + } + + static void randombytes(byte[] buf, long buf_len) { + libSodium().randombytes(buf, buf_len); + } + + static int sodium_runtime_has_neon() { + return libSodium().sodium_runtime_has_neon(); + } + + static int sodium_runtime_has_sse2() { + return libSodium().sodium_runtime_has_sse2(); + } + + static int sodium_runtime_has_sse3() { + return libSodium().sodium_runtime_has_sse3(); + } + + static int sodium_runtime_has_ssse3() { + return libSodium().sodium_runtime_has_ssse3(); + } + + static int sodium_runtime_has_sse41() { + return libSodium().sodium_runtime_has_sse41(); + } + + static int sodium_runtime_has_avx() { + return libSodium().sodium_runtime_has_avx(); + } + + static int sodium_runtime_has_avx2() { + return libSodium().sodium_runtime_has_avx2(); + } + + static int sodium_runtime_has_avx512f() { + return libSodium().sodium_runtime_has_avx512f(); + } + + static int sodium_runtime_has_pclmul() { + return libSodium().sodium_runtime_has_pclmul(); + } + + static int sodium_runtime_has_aesni() { + return libSodium().sodium_runtime_has_aesni(); + } + + static int sodium_runtime_has_rdrand() { + return libSodium().sodium_runtime_has_rdrand(); + } + + static void sodium_memzero(Pointer pnt, long len) { + libSodium().sodium_memzero(pnt, len); + } + + // static void sodium_stackzero(long len) { + // libSodium().sodium_stackzero(len); + // } + + static int sodium_memcmp(Pointer b1_, Pointer b2_, long len) { + return libSodium().sodium_memcmp(b1_, b2_, len); + } + + static int sodium_compare(Pointer b1_, Pointer b2_, long len) { + return libSodium().sodium_compare(b1_, b2_, len); + } + + static int sodium_is_zero(Pointer n, long nlen) { + return libSodium().sodium_is_zero(n, nlen); + } + + static void sodium_increment(Pointer n, long nlen) { + libSodium().sodium_increment(n, nlen); + } + + static void sodium_add(Pointer a, Pointer b, long len) { + libSodium().sodium_add(a, b, len); + } + + // FIXME: not available due to issue with LibSodium#sodium_bin2hex + // static byte[] sodium_bin2hex(byte[] hex, long hex_maxlen, byte[] bin, long bin_len) { + // return libSodium().sodium_bin2hex(hex, hex_maxlen, bin, bin_len); + // } + + static int sodium_hex2bin( + byte[] bin, + long bin_maxlen, + byte[] hex, + long hex_len, + byte[] ignore, + LongLongByReference bin_len, + Pointer hex_end) { + return libSodium().sodium_hex2bin(bin, bin_maxlen, hex, hex_len, ignore, bin_len, hex_end); + } + + static long sodium_base64_encoded_len(long bin_len, int variant) { + return libSodium().sodium_base64_encoded_len(bin_len, variant); + } + + // FIXME: not available due to issue with LibSodium#sodium_bin2base64 + // static byte[] sodium_bin2base64(byte[] b64, long b64_maxlen, byte[] bin, long bin_len, int variant) { + // return libSodium().sodium_bin2base64(b64, b64_maxlen, bin, bin_len, variant); + // } + + static int sodium_base642bin( + byte[] bin, + long bin_maxlen, + byte[] b64, + long b64_len, + byte[] ignore, + LongLongByReference bin_len, + Pointer b64_end, + int variant) { + return libSodium().sodium_base642bin(bin, bin_maxlen, b64, b64_len, ignore, bin_len, b64_end, variant); + } + + static int sodium_mlock(Pointer addr, long len) { + return libSodium().sodium_mlock(addr, len); + } + + static int sodium_munlock(Pointer addr, long len) { + return libSodium().sodium_munlock(addr, len); + } + + static Pointer sodium_malloc(long size) { + return libSodium().sodium_malloc(size); + } + + static Pointer sodium_allocarray(long count, long size) { + return libSodium().sodium_allocarray(count, size); + } + + static void sodium_free(Pointer ptr) { + libSodium().sodium_free(ptr); + } + + static int sodium_mprotect_noaccess(Pointer ptr) { + return libSodium().sodium_mprotect_noaccess(ptr); + } + + static int sodium_mprotect_readonly(Pointer ptr) { + return libSodium().sodium_mprotect_readonly(ptr); + } + + static int sodium_mprotect_readwrite(Pointer ptr) { + return libSodium().sodium_mprotect_readwrite(ptr); + } + + static int sodium_pad( + LongLongByReference padded_buflen_p, + byte[] buf, + long unpadded_buflen, + long blocksize, + long max_buflen) { + return libSodium().sodium_pad(padded_buflen_p, buf, unpadded_buflen, blocksize, max_buflen); + } + + static int sodium_unpad(LongLongByReference unpadded_buflen_p, byte[] buf, long padded_buflen, long blocksize) { + return libSodium().sodium_unpad(unpadded_buflen_p, buf, padded_buflen, blocksize); + } + + static long crypto_stream_xchacha20_keybytes() { + return libSodium().crypto_stream_xchacha20_keybytes(); + } + + static long crypto_stream_xchacha20_noncebytes() { + return libSodium().crypto_stream_xchacha20_noncebytes(); + } + + static long crypto_stream_xchacha20_messagebytes_max() { + return libSodium().crypto_stream_xchacha20_messagebytes_max(); + } + + static int crypto_stream_xchacha20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xchacha20(c, clen, n, k); + } + + static int crypto_stream_xchacha20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xchacha20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_xchacha20_xor_ic(byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_xchacha20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_xchacha20_keygen(byte[] k) { + libSodium().crypto_stream_xchacha20_keygen(k); + } + + static long crypto_box_curve25519xchacha20poly1305_seedbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_seedbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_publickeybytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_publickeybytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_secretkeybytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_secretkeybytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_beforenmbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_beforenmbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_noncebytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_noncebytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_macbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_macbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_box_curve25519xchacha20poly1305_messagebytes_max(); + } + + static int crypto_box_curve25519xchacha20poly1305_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seed_keypair(pk, sk, seed); + } + + static int crypto_box_curve25519xchacha20poly1305_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_keypair(pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_easy( + byte[] c, + byte[] m, + long mlen, + byte[] n, + byte[] pk, + byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_easy(c, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_open_easy( + byte[] m, + byte[] c, + long clen, + byte[] n, + byte[] pk, + byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_easy(m, c, clen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_detached( + byte[] c, + byte[] mac, + byte[] m, + long mlen, + byte[] n, + byte[] pk, + byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_detached(c, mac, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_open_detached( + byte[] m, + byte[] c, + byte[] mac, + long clen, + byte[] n, + byte[] pk, + byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_detached(m, c, mac, clen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_beforenm(Pointer k, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_beforenm(k, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_easy_afternm(byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_easy_afternm(c, m, mlen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_open_easy_afternm( + byte[] m, + byte[] c, + long clen, + byte[] n, + Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_easy_afternm(m, c, clen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_detached_afternm( + byte[] c, + byte[] mac, + byte[] m, + long mlen, + byte[] n, + Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_detached_afternm(c, mac, m, mlen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_open_detached_afternm( + byte[] m, + byte[] c, + byte[] mac, + long clen, + byte[] n, + Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_detached_afternm(m, c, mac, clen, n, k); + } + + static long crypto_box_curve25519xchacha20poly1305_sealbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_sealbytes(); + } + + static int crypto_box_curve25519xchacha20poly1305_seal(byte[] c, byte[] m, long mlen, byte[] pk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seal(c, m, mlen, pk); + } + + static int crypto_box_curve25519xchacha20poly1305_seal_open(byte[] m, byte[] c, long clen, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seal_open(m, c, clen, pk, sk); + } + + static long crypto_core_ed25519_bytes() { + return libSodium().crypto_core_ed25519_bytes(); + } + + static long crypto_core_ed25519_uniformbytes() { + return libSodium().crypto_core_ed25519_uniformbytes(); + } + + static int crypto_core_ed25519_is_valid_point(byte[] p) { + return libSodium().crypto_core_ed25519_is_valid_point(p); + } + + static int crypto_core_ed25519_add(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ed25519_add(r, p, q); + } + + static int crypto_core_ed25519_sub(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ed25519_sub(r, p, q); + } + + static int crypto_core_ed25519_from_uniform(byte[] p, byte[] r) { + return libSodium().crypto_core_ed25519_from_uniform(p, r); + } + + static long crypto_scalarmult_ed25519_bytes() { + return libSodium().crypto_scalarmult_ed25519_bytes(); + } + + static long crypto_scalarmult_ed25519_scalarbytes() { + return libSodium().crypto_scalarmult_ed25519_scalarbytes(); + } + + static int crypto_scalarmult_ed25519(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_ed25519(q, n, p); + } + + static int crypto_scalarmult_ed25519_base(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_ed25519_base(q, n); + } + + static long crypto_secretbox_xchacha20poly1305_keybytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_keybytes(); + } + + static long crypto_secretbox_xchacha20poly1305_noncebytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_noncebytes(); + } + + static long crypto_secretbox_xchacha20poly1305_macbytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_macbytes(); + } + + static long crypto_secretbox_xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_secretbox_xchacha20poly1305_messagebytes_max(); + } + + static int crypto_secretbox_xchacha20poly1305_easy(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_easy(c, m, mlen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_open_easy(byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_open_easy(m, c, clen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_detached( + byte[] c, + byte[] mac, + byte[] m, + long mlen, + byte[] n, + byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_detached(c, mac, m, mlen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_open_detached( + byte[] m, + byte[] c, + byte[] mac, + long clen, + byte[] n, + byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_open_detached(m, c, mac, clen, n, k); + } + + static long crypto_pwhash_scryptsalsa208sha256_bytes_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_bytes_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_bytes_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_bytes_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_passwd_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_passwd_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_passwd_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_passwd_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_saltbytes() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_saltbytes(); + } + + static long crypto_pwhash_scryptsalsa208sha256_strbytes() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_strbytes(); + } + + static String crypto_pwhash_scryptsalsa208sha256_strprefix() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_strprefix(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_interactive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_interactive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(); + } + + static int crypto_pwhash_scryptsalsa208sha256( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit) { + return libSodium().crypto_pwhash_scryptsalsa208sha256(out, outlen, passwd, passwdlen, salt, opslimit, memlimit); + } + + static int crypto_pwhash_scryptsalsa208sha256_str( + byte[] out, + byte[] passwd, + long passwdlen, + long opslimit, + long memlimit) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_scryptsalsa208sha256_str_verify(byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_scryptsalsa208sha256_ll( + byte[] passwd, + long passwdlen, + byte[] salt, + long saltlen, + long N, + int r, + int p, + byte[] buf, + long buflen) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_ll(passwd, passwdlen, salt, saltlen, N, r, p, buf, buflen); + } + + static int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(str, opslimit, memlimit); + } + + static long crypto_stream_salsa2012_keybytes() { + return libSodium().crypto_stream_salsa2012_keybytes(); + } + + static long crypto_stream_salsa2012_noncebytes() { + return libSodium().crypto_stream_salsa2012_noncebytes(); + } + + static long crypto_stream_salsa2012_messagebytes_max() { + return libSodium().crypto_stream_salsa2012_messagebytes_max(); + } + + static int crypto_stream_salsa2012(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa2012(c, clen, n, k); + } + + static int crypto_stream_salsa2012_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa2012_xor(c, m, mlen, n, k); + } + + static void crypto_stream_salsa2012_keygen(byte[] k) { + libSodium().crypto_stream_salsa2012_keygen(k); + } + + static long crypto_stream_salsa208_keybytes() { + return libSodium().crypto_stream_salsa208_keybytes(); + } + + static long crypto_stream_salsa208_noncebytes() { + return libSodium().crypto_stream_salsa208_noncebytes(); + } + + static long crypto_stream_salsa208_messagebytes_max() { + return libSodium().crypto_stream_salsa208_messagebytes_max(); + } + + static int crypto_stream_salsa208(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa208(c, clen, n, k); + } + + static int crypto_stream_salsa208_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa208_xor(c, m, mlen, n, k); + } + + static void crypto_stream_salsa208_keygen(byte[] k) { + libSodium().crypto_stream_salsa208_keygen(k); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/SodiumException.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SodiumException.java new file mode 100644 index 00000000..65e78a4f --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/SodiumException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +/** + * An exception that is thrown when an error occurs using the native sodium library. + */ +public final class SodiumException extends RuntimeException { + + /** + * @param message The exception message. + */ + public SodiumException(String message) { + super(message); + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305.java new file mode 100644 index 00000000..7d4beb46 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305.java @@ -0,0 +1,742 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import net.consensys.cava.bytes.Bytes; + +import javax.annotation.Nullable; + +import jnr.ffi.Pointer; +import jnr.ffi.byref.ByteByReference; +import jnr.ffi.byref.LongLongByReference; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/xchacha20-poly1305_construction.md + +/** + * Authenticated Encryption with Additional Data using XChaCha20-Poly1305. + * + *

+ * The XChaCha20-Poly1305 construction can safely encrypt a practically unlimited number of messages with the same key, + * without any practical limit to the size of a message (up to ~ 2^64 bytes). + * + *

+ * As an alternative to counters, its large nonce size (192-bit) allows random nonces to be safely used. + * + *

+ * For this reason, and if interoperability with other libraries is not a concern, this is the recommended AEAD + * construction. + * + *

+ * This class depends upon the JNR-FFI library being available on the classpath, along with its dependencies. See + * https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +public final class XChaCha20Poly1305 { + private XChaCha20Poly1305() {} + + private static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * A XChaCha20-Poly1305 key. + */ + public static final class Key { + private final Pointer ptr; + + private Key(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + Pointer ptr = Sodium.malloc(length()); + try { + Sodium.crypto_aead_xchacha20poly1305_ietf_keygen(ptr); + return new Key(ptr); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * A XChaCha20-Poly1305 nonce. + */ + public static final class Nonce { + private final Pointer ptr; + + private Nonce(Pointer ptr) { + this.ptr = ptr; + } + + @Override + protected void finalize() { + Sodium.sodium_free(ptr); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(Bytes bytes) { + return forBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

+ * The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce forBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes()) { + throw new IllegalArgumentException( + "nonce must be " + Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + */ + public static int length() { + long npubbytes = Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_npubbytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Generate a new {@link Nonce} using a random generator. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

+ * Note that this is not synchronized. If multiple threads are creating encrypted messages and incrementing this + * nonce, then external synchronization is required to ensure no two encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(ptr, length(), Nonce::new); + } + + /** + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return Sodium.reify(ptr, length()); + } + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Bytes data, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, byte[] data, Key key, Nonce nonce) { + byte[] cipherText = new byte[maxCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_encrypt: failed with result " + rc); + } + + return maybeSliceResult(cipherText, cipherTextLen, "crypto_aead_xchacha20poly1305_ietf_encrypt"); + } + + private static int maxCypherTextLength(byte[] message) { + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + return (int) abytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Bytes data, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, byte[] data, Key key, Nonce nonce) { + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.ptr, + key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_encrypt_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, + maybeSliceResult(mac, macLen, "crypto_aead_xchacha20poly1305_ietf_encrypt_detached")); + } + + private static final byte TAG_FINAL = (0x01 | 0x02); + + private static final class SSEncrypt implements SecretEncryptionStream { + private final int abytes; + private final byte[] header; + private Pointer state; + private boolean complete = false; + + private SSEncrypt(Key key) { + long abytes = Sodium.crypto_secretstream_xchacha20poly1305_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + this.abytes = (int) abytes; + + long headerbytes = Sodium.crypto_secretstream_xchacha20poly1305_headerbytes(); + if (headerbytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_secretstream_xchacha20poly1305_headerbytes: " + abytes + " is too large"); + } + this.header = new byte[(int) headerbytes]; + + Pointer state = Sodium.malloc(Sodium.crypto_secretstream_xchacha20poly1305_statebytes()); + try { + int rc = Sodium.crypto_secretstream_xchacha20poly1305_init_push(state, header, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_secretstream_xchacha20poly1305_init_push: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(state); + throw e; + } + this.state = state; + } + + @Override + protected void finalize() { + if (state != null) { + Sodium.sodium_free(this.state); + this.state = null; + } + } + + @Override + public byte[] headerArray() { + return header; + } + + @Override + public byte[] push(byte[] clearText, boolean isFinal) { + if (complete) { + throw new IllegalStateException("stream already completed"); + } + byte[] cipherText = new byte[abytes + clearText.length]; + byte tag = isFinal ? TAG_FINAL : 0; + int rc = Sodium.crypto_secretstream_xchacha20poly1305_push( + state, + cipherText, + null, + clearText, + clearText.length, + null, + 0, + tag); + if (rc != 0) { + throw new SodiumException("crypto_secretstream_xchacha20poly1305_push: failed with result " + rc); + } + if (isFinal) { + complete = true; + // free state before finalization, as it will not be re-used + Sodium.sodium_free(this.state); + this.state = null; + } + return cipherText; + } + } + + /** + * Open an encryption stream. + * + * @param key The key to encrypt for. + * @return The input stream. + */ + public static SecretEncryptionStream openEncryptionStream(Key key) { + return new SSEncrypt(key); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, byte[] data, Key key, Nonce nonce) { + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.ptr, + key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_decrypt: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_xchacha20poly1305_ietf_decrypt"); + } + + private static int maxClearTextLength(byte[] cipherText) { + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) abytes); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or null if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, byte[] data, Key key, Nonce nonce) { + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.ptr, + key.ptr); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_xchacha20poly1305_ietf_decrypt_detached: failed with result " + rc); + } + + return clearText; + } + + private static final class SSDecrypt implements SecretDecryptionStream { + private final int abytes; + private Pointer state; + private boolean complete = false; + + private SSDecrypt(Key key, byte[] header) { + if (header.length != Sodium.crypto_secretstream_xchacha20poly1305_headerbytes()) { + throw new IllegalArgumentException( + "header must be " + + Sodium.crypto_secretstream_xchacha20poly1305_headerbytes() + + " bytes, got " + + header.length); + } + + long abytes = Sodium.crypto_secretstream_xchacha20poly1305_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + this.abytes = (int) abytes; + + Pointer state = Sodium.malloc(Sodium.crypto_secretstream_xchacha20poly1305_statebytes()); + try { + int rc = Sodium.crypto_secretstream_xchacha20poly1305_init_pull(state, header, key.ptr); + if (rc != 0) { + throw new SodiumException("crypto_secretstream_xchacha20poly1305_init_push: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(state); + throw e; + } + this.state = state; + } + + @Override + protected void finalize() { + if (state != null) { + Sodium.sodium_free(this.state); + this.state = null; + } + } + + @Override + public byte[] pull(byte[] cipherText) { + if (complete) { + throw new IllegalStateException("stream already completed"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + byte[] clearText = new byte[cipherText.length - abytes]; + ByteByReference tag = new ByteByReference(); + int rc = Sodium.crypto_secretstream_xchacha20poly1305_pull( + state, + clearText, + null, + tag, + cipherText, + cipherText.length, + null, + 0); + if (rc != 0) { + throw new SodiumException("crypto_secretstream_xchacha20poly1305_push: failed with result " + rc); + } + if (tag.byteValue() == TAG_FINAL) { + complete = true; + // free state before finalization, as it will not be re-used + Sodium.sodium_free(this.state); + this.state = null; + } + return clearText; + } + + @Override + public boolean isComplete() { + return complete; + } + } + + /** + * Open an decryption stream. + * + * @param key The key to use for decryption. + * @param header The header for the stream. + * @return The input stream. + */ + public static SecretDecryptionStream openDecryptionStream(Key key, byte[] header) { + return new SSDecrypt(key, header); + } + + private static byte[] maybeSliceResult(byte[] bytes, LongLongByReference actualLength, String methodName) { + if (actualLength.longValue() == bytes.length) { + return bytes; + } + if (actualLength.longValue() > Integer.MAX_VALUE) { + throw new SodiumException(methodName + ": result of length " + actualLength.longValue() + " is too large"); + } + byte[] result = new byte[actualLength.intValue()]; + System.arraycopy(bytes, 0, result, 0, result.length); + return result; + } +} diff --git a/crypto/src/main/java/net/consensys/cava/crypto/sodium/package-info.java b/crypto/src/main/java/net/consensys/cava/crypto/sodium/package-info.java new file mode 100644 index 00000000..b7f6fbf9 --- /dev/null +++ b/crypto/src/main/java/net/consensys/cava/crypto/sodium/package-info.java @@ -0,0 +1,18 @@ +/** + * 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 + * {@link net.consensys.cava.crypto.sodium.Sodium#loadLibrary(java.nio.file.Path)}. + * + *

+ * Classes in this package also depend upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle dependency + * 'com.github.jnr:jnr-ffi'. + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.crypto.sodium; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/crypto/src/test/java/net/consensys/cava/crypto/HashTest.java b/crypto/src/test/java/net/consensys/cava/crypto/HashTest.java new file mode 100644 index 00000000..4f5ab879 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/HashTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.junit.BouncyCastleExtension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class HashTest { + + @Test + void sha2_256() { + String horseSha2 = "fd62862b6dc213bee77c2badd6311528253c6cb3107e03c16051aa15584eca1c"; + String cowSha2 = "beb134754910a4b4790c69ab17d3975221f4c534b70c8d6e82b30c165e8c0c09"; + + Bytes resultHorse = Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha2), resultHorse); + + byte[] resultHorse2 = Hash.sha2_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha2).toArray(), resultHorse2); + + Bytes resultCow = Hash.sha2_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha2), resultCow); + + byte[] resultCow2 = Hash.sha2_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha2).toArray(), resultCow2); + } + + @Test + void sha2_512_256() { + String horseSha2 = "6d64886cd066b81cf2dcf16ae70e97017d35f2f4ab73c5c5810aaa9ab573dab3"; + String cowSha2 = "7d26bad15e2f266cb4cbe9b1913978cb8a8bd08d92ee157b6be87c92dfce2d3e"; + + Bytes resultHorse = Hash.sha2_512_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha2), resultHorse); + + byte[] resultHorse2 = Hash.sha2_512_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha2).toArray(), resultHorse2); + + Bytes resultCow = Hash.sha2_512_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha2), resultCow); + + byte[] resultCow2 = Hash.sha2_512_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha2).toArray(), resultCow2); + } + + @Test + void keccak256() { + String horseKeccak256 = "c87f65ff3f271bf5dc8643484f66b200109caffe4bf98c4cb393dc35740b28c0"; + String cowKeccak256 = "c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"; + + Bytes resultHorse = Hash.keccak256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseKeccak256), resultHorse); + + byte[] resultHorse2 = Hash.keccak256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseKeccak256).toArray(), resultHorse2); + + Bytes resultCow = Hash.keccak256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowKeccak256), resultCow); + + byte[] resultCow2 = Hash.keccak256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowKeccak256).toArray(), resultCow2); + } + + @Test + void sha3_256() { + String horseSha3 = "d8137088d21c7c0d69107cd51d1c32440a57aa5c59f73ed7310522ea491000ac"; + String cowSha3 = "fba26f1556b8c7b473d01e3eae218318f752e808407794fc0b6490988a33a82d"; + + Bytes resultHorse = Hash.sha3_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha3), resultHorse); + + byte[] resultHorse2 = Hash.sha3_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha3).toArray(), resultHorse2); + + Bytes resultCow = Hash.sha3_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha3), resultCow); + + byte[] resultCow2 = Hash.sha3_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha3).toArray(), resultCow2); + } + + @Test + void sha3_512() { + String horseSha3 = + "d78700def5dd85a9f5a1f8cce8614889e696d4dc82b17189e4974acc050659b49494f03cd0bfbb13a32132b4b4af5e16efd8b0643a5453c87e8e6dfb086b3568"; + String cowSha3 = + "14accdcf3380cd31674aa5edcd2a53f1b1dad3922eb335e89399321e17a8be5ea315b5346a4c45f6a2595b8e2e24bb345daeb97c7ddd2e970b9e53c9ae439f23"; + + Bytes resultHorse = Hash.sha3_512(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha3), resultHorse); + + byte[] resultHorse2 = Hash.sha3_512("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha3).toArray(), resultHorse2); + + Bytes resultCow = Hash.sha3_512(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha3), resultCow); + + byte[] resultCow2 = Hash.sha3_512("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha3).toArray(), resultCow2); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/SECP256K1Test.java b/crypto/src/test/java/net/consensys/cava/crypto/SECP256K1Test.java new file mode 100644 index 00000000..bdeaac40 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/SECP256K1Test.java @@ -0,0 +1,316 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static net.consensys.cava.bytes.Bytes.fromHexString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; +import net.consensys.cava.crypto.SECP256K1.PublicKey; +import net.consensys.cava.crypto.SECP256K1.Signature; +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; + +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(BouncyCastleExtension.class) +class SECP256K1Test { + + @Test + void testCreatePrivateKey_NullEncoding() { + assertThrows(NullPointerException.class, () -> SECP256K1.PrivateKey.create((Bytes32) null)); + } + + @Test + void testPrivateKeyEquals() { + SECP256K1.PrivateKey privateKey1 = SECP256K1.PrivateKey.create(BigInteger.TEN); + SECP256K1.PrivateKey privateKey2 = SECP256K1.PrivateKey.create(BigInteger.TEN); + assertEquals(privateKey1, privateKey2); + } + + @Test + void testPrivateHashCode() { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + assertNotEquals(0, privateKey.hashCode()); + } + + @Test + void testCreatePublicKey_NullEncoding() { + assertThrows(NullPointerException.class, () -> SECP256K1.PublicKey.create((Bytes) null)); + } + + @Test + void testCreatePublicKey_EncodingTooShort() { + assertThrows(IllegalArgumentException.class, () -> SECP256K1.PublicKey.create(Bytes.wrap(new byte[63]))); + } + + @Test + void testCreatePublicKey_EncodingTooLong() { + assertThrows(IllegalArgumentException.class, () -> SECP256K1.PublicKey.create(Bytes.wrap(new byte[65]))); + } + + @Test + void testPublicKeyEquals() { + SECP256K1.PublicKey publicKey1 = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + SECP256K1.PublicKey publicKey2 = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + assertEquals(publicKey1, publicKey2); + } + + @Test + void testPublicHashCode() { + SECP256K1.PublicKey publicKey = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + assertNotEquals(0, publicKey.hashCode()); + } + + @Test + void testCreateKeyPair_PublicKeyNull() { + assertThrows( + NullPointerException.class, + () -> SECP256K1.KeyPair.create(null, SECP256K1.PublicKey.create(Bytes.wrap(new byte[64])))); + } + + @Test + void testCreateKeyPair_PrivateKeyNull() { + assertThrows( + NullPointerException.class, + () -> SECP256K1.KeyPair.create(SECP256K1.PrivateKey.create(Bytes32.wrap(new byte[32])), null)); + } + + @Test + void testKeyPairGeneration() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertNotNull(keyPair); + assertNotNull(keyPair.getPrivateKey()); + assertNotNull(keyPair.getPublicKey()); + } + + @Test + void testKeyPairEquals() { + SECP256K1.PrivateKey privateKey1 = SECP256K1.PrivateKey.create(BigInteger.TEN); + SECP256K1.PrivateKey privateKey2 = SECP256K1.PrivateKey.create(BigInteger.TEN); + SECP256K1.PublicKey publicKey1 = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + SECP256K1.PublicKey publicKey2 = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + SECP256K1.KeyPair keyPair1 = SECP256K1.KeyPair.create(privateKey1, publicKey1); + SECP256K1.KeyPair keyPair2 = SECP256K1.KeyPair.create(privateKey2, publicKey2); + + assertEquals(keyPair1, keyPair2); + } + + @Test + void testKeyPairHashCode() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertNotEquals(0, keyPair.hashCode()); + } + + @Test + void testKeyPairGeneration_PublicKeyRecovery() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertEquals(keyPair.getPublicKey(), SECP256K1.PublicKey.create(keyPair.getPrivateKey())); + } + + @Test + void testPublicKeyRecovery() { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + SECP256K1.PublicKey expectedPublicKey = SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + SECP256K1.PublicKey publicKey = SECP256K1.PublicKey.create(privateKey); + assertEquals(expectedPublicKey, publicKey); + } + + @Test + void testCreateSignature() { + SECP256K1.Signature signature = new SECP256K1.Signature(BigInteger.ONE, BigInteger.TEN, (byte) 7); + assertEquals(BigInteger.ONE, signature.r()); + assertEquals(BigInteger.TEN, signature.s()); + assertEquals((byte) 7, signature.v()); + } + + @Test + void testEncodeSignature() { + SECP256K1.Signature signature = new SECP256K1.Signature(BigInteger.ONE, BigInteger.TEN, (byte) 7); + assertEquals( + "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000A07", + signature.encodedBytes().toString()); + } + + @Test + void testCreateSignatureFromEncoding() { + SECP256K1.Signature signature = SECP256K1.Signature.create( + fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000A07")); + assertEquals(BigInteger.ONE, signature.r()); + assertEquals(BigInteger.TEN, signature.s()); + assertEquals((byte) 7, signature.v()); + } + + @Test + void testCreateSignatureWithNullR() { + assertThrows(NullPointerException.class, () -> SECP256K1.Signature.create((byte) 12, null, BigInteger.ONE)); + } + + @Test + void testCreateSignatureWithNullS() { + assertThrows(NullPointerException.class, () -> SECP256K1.Signature.create((byte) 12, BigInteger.ONE, null)); + } + + @Test + void testCreateSignatureWithZeroR() { + Exception throwable = assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 12, BigInteger.ZERO, BigInteger.ONE)); + assertEquals("Invalid 'r' value, should be >= 1 but got 0", throwable.getMessage()); + } + + @Test + void testCreateSignatureWithZeroS() { + Exception throwable = assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 12, BigInteger.ONE, BigInteger.ZERO)); + assertEquals("Invalid 's' value, should be >= 1 but got 0", throwable.getMessage()); + } + + @Test + void testCreateSignatureWithRHigherThanCurve() { + BigInteger curveN = SECP256K1.Parameters.CURVE.getN(); + Exception throwable = assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 12, curveN.add(BigInteger.ONE), BigInteger.ONE)); + assertEquals( + "Invalid 'r' value, should be < " + curveN.toString() + " but got " + curveN.add(BigInteger.ONE), + throwable.getMessage()); + } + + @Test + void testCreateSignatureWithSHigherThanCurve() { + BigInteger curveN = SECP256K1.Parameters.CURVE.getN(); + Exception throwable = assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 12, BigInteger.ONE, curveN.add(BigInteger.ONE))); + assertEquals( + "Invalid 's' value, should be < " + curveN.toString() + " but got " + curveN.add(BigInteger.ONE), + throwable.getMessage()); + } + + @Test + void testRecoverPublicKeyFromSignature() { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey + .create(new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + SECP256K1.Signature signature = SECP256K1.sign(data, keyPair); + + PublicKey recoveredPublicKey = SECP256K1.PublicKey.recoverFromSignature(data, signature); + assertEquals(keyPair.getPublicKey().toString(), recoveredPublicKey.toString()); + } + + @Test + void testCannotRecoverPublicKeyFromSignature() { + SECP256K1.Signature signature = new Signature(BigInteger.ONE, BigInteger.valueOf(10), (byte) 3); + + SECP256K1KeyRecoveryException exception = assertThrows(SECP256K1KeyRecoveryException.class, () -> { + SECP256K1.PublicKey.recoverFromSignature(Bytes.of("This is not matching data".getBytes("UTF-8")), signature); + }); + assertEquals("Public key cannot be recovered: Invalid point compression", exception.getMessage()); + } + + @Test + void testSignatureGeneration() { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey + .create(new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + SECP256K1.Signature expectedSignature = new SECP256K1.Signature( + new BigInteger("d2ce488f4da29e68f22cb05cac1b19b75df170a12b4ad1bdd4531b8e9115c6fb", 16), + new BigInteger("75c1fe50a95e8ccffcbb5482a1e42fbbdd6324131dfe75c3b3b7f9a7c721eccb", 16), + (byte) 28); + + SECP256K1.Signature actualSignature = SECP256K1.sign(data, keyPair); + assertEquals(expectedSignature, actualSignature); + } + + @Test + void testSignatureVerification() { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey + .create(new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + + SECP256K1.Signature signature = SECP256K1.sign(data, keyPair); + assertTrue(SECP256K1.verify(data, signature, keyPair.getPublicKey())); + } + + + @Test + void testFileContainsValidPrivateKey(@TempDirectory Path tempDir) throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write(tempFile, "000000000000000000000000000000000000000000000000000000000000000A".getBytes(UTF_8)); + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.load(tempFile); + assertEquals( + fromHexString("000000000000000000000000000000000000000000000000000000000000000A"), + privateKey.encodedBytes()); + } + + @Test + void testReadWritePrivateKeyString(@TempDirectory Path tempDir) throws Exception { + SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + SECP256K1.KeyPair keyPair1 = SECP256K1.KeyPair.create(privateKey); + Path tempFile = tempDir.resolve("tempId"); + keyPair1.store(tempFile); + SECP256K1.KeyPair keyPair2 = SECP256K1.KeyPair.load(tempFile); + assertEquals(keyPair1, keyPair2); + } + + @Test + void testInvalidFileThrowsInvalidKeyPairException(@TempDirectory Path tempDir) throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write(tempFile, "not valid".getBytes(UTF_8)); + assertThrows(InvalidSEC256K1PrivateKeyStoreException.class, () -> SECP256K1.PrivateKey.load(tempFile)); + } + + @Test + void testInvalidMultiLineFileThrowsInvalidIdException(@TempDirectory Path tempDir) throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write(tempFile, "not\n\nvalid".getBytes(UTF_8)); + assertThrows(InvalidSEC256K1PrivateKeyStoreException.class, () -> SECP256K1.PrivateKey.load(tempFile)); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/AES256GCMTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/AES256GCMTest.java new file mode 100644 index 00000000..8a6d8d99 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/AES256GCMTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AES256GCMTest { + + private static AES256GCM.Nonce nonce; + + @BeforeAll + static void checkAvailable() { + assumeTrue(AES256GCM.isAvailable()); + nonce = AES256GCM.Nonce.random(); + } + + @BeforeEach + void incrementNonce() { + nonce = nonce.increment(); + } + + @Test + void checkCombinedEncryptDecrypt() { + AES256GCM.Key key = AES256GCM.Key.random(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + byte[] cipherText = AES256GCM.encrypt(message, data, key, nonce); + byte[] clearText = AES256GCM.decrypt(cipherText, data, key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(AES256GCM.decrypt(cipherText, data, key, nonce.increment())); + } + + @Test + void checkCombinedPrecomputedEncryptDecrypt() { + try (AES256GCM precomputed = AES256GCM.forKey(AES256GCM.Key.random())) { + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + byte[] cipherText = precomputed.encrypt(message, data, nonce); + byte[] clearText = precomputed.decrypt(cipherText, data, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(precomputed.decrypt(cipherText, data, nonce.increment())); + } + } + + @Test + void checkDetachedEncryptDecrypt() { + AES256GCM.Key key = AES256GCM.Key.random(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + DetachedEncryptionResult result = AES256GCM.encryptDetached(message, data, key, nonce); + byte[] clearText = AES256GCM.decryptDetached(result.cipherTextArray(), result.macArray(), data, key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = AES256GCM.decryptDetached(result.cipherTextArray(), result.macArray(), data, key, nonce.increment()); + assertNull(clearText); + } + + @Test + void checkDetachedPrecomputedEncryptDecrypt() { + try (AES256GCM precomputed = AES256GCM.forKey(AES256GCM.Key.random())) { + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + DetachedEncryptionResult result = precomputed.encryptDetached(message, data, nonce); + byte[] clearText = precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), data, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), data, nonce.increment()); + assertNull(clearText); + } + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/AuthTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/AuthTest.java new file mode 100644 index 00000000..543a9460 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/AuthTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class AuthTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void checkAuthenticateAndVerify() { + Auth.Key key = Auth.Key.random(); + + byte[] input = "An input to authenticate".getBytes(Charsets.UTF_8); + byte[] tag = Auth.auth(input, key); + + assertTrue(Auth.verify(tag, input, key)); + assertFalse(Auth.verify(new byte[tag.length], input, key)); + assertFalse(Auth.verify(tag, "An invalid input".getBytes(Charsets.UTF_8), key)); + assertFalse(Auth.verify(tag, input, Auth.Key.random())); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/BoxTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/BoxTest.java new file mode 100644 index 00000000..62c3971f --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/BoxTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BoxTest { + + private static Box.Seed seed; + private static Box.Nonce nonce; + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + nonce = Box.Nonce.random(); + // @formatter:off + seed = Box.Seed.forBytes(new byte[] { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + }); + // @formatter:on + } + + @BeforeEach + void incrementNonce() { + nonce = nonce.increment(); + } + + @Test + void checkCombinedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.fromSeed(seed); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + + byte[] cipherText = Box.encrypt(message, aliceKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + byte[] clearText = Box.decrypt(cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = Box.decrypt(cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce.increment()); + assertNull(clearText); + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + clearText = Box.decrypt(cipherText, otherKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + assertNull(clearText); + } + + @Test + void checkCombinedPrecomputedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] cipherText; + + try (Box precomputed = Box.forKeys(aliceKeyPair.publicKey(), bobKeyPair.secretKey())) { + cipherText = precomputed.encrypt(message, nonce); + } + + byte[] clearText = Box.decrypt(cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + try (Box precomputed = Box.forKeys(bobKeyPair.publicKey(), aliceKeyPair.secretKey())) { + clearText = precomputed.decrypt(cipherText, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(precomputed.decrypt(cipherText, nonce.increment())); + } + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + try (Box precomputed = Box.forKeys(otherKeyPair.publicKey(), bobKeyPair.secretKey())) { + assertNull(precomputed.decrypt(cipherText, nonce)); + } + } + + @Test + void checkDetachedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + + DetachedEncryptionResult result = + Box.encryptDetached(message, aliceKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + byte[] clearText = Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce.increment()); + assertNull(clearText); + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + clearText = Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + otherKeyPair.publicKey(), + bobKeyPair.secretKey(), + nonce); + assertNull(clearText); + } + + @Test + void checkDetachedPrecomputedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + DetachedEncryptionResult result; + + try (Box precomputed = Box.forKeys(aliceKeyPair.publicKey(), bobKeyPair.secretKey())) { + result = precomputed.encryptDetached(message, nonce); + } + + byte[] clearText = Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + try (Box precomputed = Box.forKeys(bobKeyPair.publicKey(), aliceKeyPair.secretKey())) { + clearText = precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), nonce.increment())); + } + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + try (Box precomputed = Box.forKeys(otherKeyPair.publicKey(), bobKeyPair.secretKey())) { + assertNull(precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), nonce)); + } + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/KeyDerivationTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/KeyDerivationTest.java new file mode 100644 index 00000000..55b26326 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/KeyDerivationTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import net.consensys.cava.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class KeyDerivationTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void differentIdsShouldGenerateDifferentKeys() { + KeyDerivation.Key masterKey = KeyDerivation.Key.random(); + + Bytes subKey1 = KeyDerivation.deriveKey(40, 1, "abcdefg", masterKey); + assertEquals(subKey1, KeyDerivation.deriveKey(40, 1, "abcdefg", masterKey)); + + assertNotEquals(subKey1, KeyDerivation.deriveKey(40, 2, "abcdefg", masterKey)); + assertNotEquals(subKey1, KeyDerivation.deriveKey(40, 1, new byte[KeyDerivation.contextLength()], masterKey)); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/PasswordHashTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/PasswordHashTest.java new file mode 100644 index 00000000..20a22766 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/PasswordHashTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +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 static org.junit.jupiter.api.Assumptions.assumeTrue; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.crypto.sodium.PasswordHash.Algorithm; +import net.consensys.cava.crypto.sodium.PasswordHash.Salt; +import net.consensys.cava.crypto.sodium.PasswordHash.VerificationResult; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class PasswordHashTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void shouldGenerateSameKeyForSameParameters() { + String password = "A very insecure password"; + Salt salt = Salt.random(); + + Bytes hash = PasswordHash.hash( + password, + 20, + salt, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertEquals(20, hash.size()); + + Bytes generated = PasswordHash.hash( + password, + 20, + salt, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertEquals(hash, generated); + + generated = PasswordHash.hash( + password, + 20, + Salt.random(), + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertNotEquals(hash, generated); + + generated = PasswordHash.hash( + password, + 20, + salt, + PasswordHash.moderateOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertNotEquals(hash, generated); + } + + @Test + void shouldThrowForLowOpsLimitWithArgon2i() { + assertThrows(IllegalArgumentException.class, () -> { + PasswordHash.hash( + "A very insecure password", + 20, + Salt.random(), + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.argon2i13()); + }); + } + + @Test + void checkHashAndVerify() { + String password = "A very insecure password"; + + String hash = PasswordHash.hashInteractive(password); + assertTrue(PasswordHash.verifyOnly(hash, password)); + VerificationResult result = PasswordHash.verifyInteractive(hash, password); + assertEquals(VerificationResult.PASSED, result); + assertTrue(result.passed()); + + assertFalse(PasswordHash.verifyOnly(hash, "Bad password")); + result = PasswordHash.verifyInteractive(hash, "Bad password"); + assertEquals(VerificationResult.FAILED, result); + assertFalse(result.passed()); + + result = PasswordHash.verify(hash, password); + assertEquals(VerificationResult.NEEDS_REHASH, result); + assertTrue(result.passed()); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/SecretBoxTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/SecretBoxTest.java new file mode 100644 index 00000000..dccae269 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/SecretBoxTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SecretBoxTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void checkCombinedEncryptDecrypt() { + SecretBox.Key key = SecretBox.Key.random(); + SecretBox.Nonce nonce = SecretBox.Nonce.random().increment(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + + byte[] cipherText = SecretBox.encrypt(message, key, nonce); + byte[] clearText = SecretBox.decrypt(cipherText, key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(SecretBox.decrypt(cipherText, key, nonce.increment())); + SecretBox.Key otherKey = SecretBox.Key.random(); + assertNull(SecretBox.decrypt(cipherText, otherKey, nonce)); + } + + @Test + void checkDetachedEncryptDecrypt() { + SecretBox.Key key = SecretBox.Key.random(); + SecretBox.Nonce nonce = SecretBox.Nonce.random().increment(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + + DetachedEncryptionResult result = SecretBox.encryptDetached(message, key, nonce); + byte[] clearText = SecretBox.decryptDetached(result.cipherTextArray(), result.macArray(), key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(SecretBox.decryptDetached(result.cipherTextArray(), result.macArray(), key, nonce.increment())); + SecretBox.Key otherKey = SecretBox.Key.random(); + assertNull(SecretBox.decryptDetached(result.cipherTextArray(), result.macArray(), otherKey, nonce)); + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/SodiumTest.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/SodiumTest.java new file mode 100644 index 00000000..a6c14a52 --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/SodiumTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import jnr.ffi.Pointer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SodiumTest { + + @BeforeAll + static void checkSodium() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void checkBasicConstants() { + assertEquals(12, Sodium.crypto_aead_aes256gcm_npubbytes()); + assertEquals(64, Sodium.crypto_auth_hmacsha512_bytes()); + assertEquals(32, Sodium.crypto_generichash_bytes()); + } + + @Test + void checkCryptoHashSha512MultiPart() { + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] hash = new byte[(int) Sodium.crypto_hash_sha512_bytes()]; + int rc = Sodium.crypto_hash_sha512(hash, message, message.length); + assertEquals(0, rc); + + Pointer state = Sodium.sodium_malloc(Sodium.crypto_hash_sha512_statebytes()); + try { + rc = Sodium.crypto_hash_sha512_init(state); + assertEquals(0, rc); + + byte[] message1 = "This is ".getBytes(Charsets.UTF_8); + Sodium.crypto_hash_sha512_update(state, message1, message1.length); + byte[] message2 = "a test message".getBytes(Charsets.UTF_8); + Sodium.crypto_hash_sha512_update(state, message2, message2.length); + + byte[] hash2 = new byte[(int) Sodium.crypto_hash_sha512_bytes()]; + Sodium.crypto_hash_sha512_final(state, hash2); + + assertArrayEquals(hash, hash2); + } finally { + Sodium.sodium_free(state); + } + } + + @Test + void checkCryptoHashSha256MultiPart() { + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] hash = new byte[(int) Sodium.crypto_hash_sha256_bytes()]; + int rc = Sodium.crypto_hash_sha256(hash, message, message.length); + assertEquals(0, rc); + + Pointer state = Sodium.sodium_malloc(Sodium.crypto_hash_sha256_statebytes()); + try { + rc = Sodium.crypto_hash_sha256_init(state); + assertEquals(0, rc); + + byte[] message1 = "This is ".getBytes(Charsets.UTF_8); + Sodium.crypto_hash_sha256_update(state, message1, message1.length); + byte[] message2 = "a test message".getBytes(Charsets.UTF_8); + Sodium.crypto_hash_sha256_update(state, message2, message2.length); + + byte[] hash2 = new byte[(int) Sodium.crypto_hash_sha256_bytes()]; + Sodium.crypto_hash_sha256_final(state, hash2); + + assertArrayEquals(hash, hash2); + } finally { + Sodium.sodium_free(state); + } + } +} diff --git a/crypto/src/test/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305Test.java b/crypto/src/test/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305Test.java new file mode 100644 index 00000000..140e7aeb --- /dev/null +++ b/crypto/src/test/java/net/consensys/cava/crypto/sodium/XChaCha20Poly1305Test.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class XChaCha20Poly1305Test { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable()); + } + + @Test + void checkCombinedEncryptDecrypt() { + XChaCha20Poly1305.Key key = XChaCha20Poly1305.Key.random(); + XChaCha20Poly1305.Nonce nonce = XChaCha20Poly1305.Nonce.random().increment(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + byte[] cipherText = XChaCha20Poly1305.encrypt(message, data, key, nonce); + byte[] clearText = XChaCha20Poly1305.decrypt(cipherText, data, key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(XChaCha20Poly1305.decrypt(cipherText, data, key, nonce.increment())); + } + + @Test + void checkDetachedEncryptDecrypt() { + XChaCha20Poly1305.Key key = XChaCha20Poly1305.Key.random(); + XChaCha20Poly1305.Nonce nonce = XChaCha20Poly1305.Nonce.random().increment(); + + byte[] message = "This is a test message".getBytes(Charsets.UTF_8); + byte[] data = "123456".getBytes(Charsets.UTF_8); + + DetachedEncryptionResult result = XChaCha20Poly1305.encryptDetached(message, data, key, nonce); + byte[] clearText = XChaCha20Poly1305.decryptDetached(result.cipherTextArray(), result.macArray(), data, key, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = + XChaCha20Poly1305.decryptDetached(result.cipherTextArray(), result.macArray(), data, key, nonce.increment()); + assertNull(clearText); + } + + @Test + void checkStreamEncryptDecrypt() { + XChaCha20Poly1305.Key key = XChaCha20Poly1305.Key.random(); + + byte[] message1 = "This is the first message".getBytes(Charsets.UTF_8); + byte[] message2 = "This is the second message".getBytes(Charsets.UTF_8); + byte[] message3 = "This is the third message".getBytes(Charsets.UTF_8); + + SecretEncryptionStream ses = XChaCha20Poly1305.openEncryptionStream(key); + byte[] header = ses.headerArray(); + byte[] cipher1 = ses.push(message1); + byte[] cipher2 = ses.push(message2); + byte[] cipher3 = ses.pushLast(message3); + + SecretDecryptionStream sds = XChaCha20Poly1305.openDecryptionStream(key, header); + assertArrayEquals(message1, sds.pull(cipher1)); + assertFalse(sds.isComplete()); + assertArrayEquals(message2, sds.pull(cipher2)); + assertFalse(sds.isComplete()); + assertArrayEquals(message3, sds.pull(cipher3)); + assertTrue(sds.isComplete()); + } +} diff --git a/dependency-versions.gradle b/dependency-versions.gradle new file mode 100644 index 00000000..1c6b7de9 --- /dev/null +++ b/dependency-versions.gradle @@ -0,0 +1,25 @@ +dependencyManagement { + dependencies { + dependency('com.github.jnr:jnr-ffi:2.1.8') + dependency('com.google.errorprone:error_prone_core:2.3.1') + dependency('com.google.guava:guava:25.1-jre') + dependency('com.winterbe:expekt:0.5.0') + dependency('io.vertx:vertx-core:3.5.2') + dependency('com.google.code.findbugs:jsr305:3.0.2') + dependency('org.antlr:antlr4:4.7.1') + dependency('org.assertj:assertj-core:3.10.0') + dependency('org.bouncycastle:bcpkix-jdk15on:1.59') + dependency('org.bouncycastle:bcprov-jdk15on:1.59') + dependency('org.fusesource.leveldbjni:leveldbjni-all:1.8') + dependency('org.junit.jupiter:junit-jupiter-api:5.2.0') + dependency('org.junit.jupiter:junit-jupiter-engine:5.2.0') + dependency('org.junit.jupiter:junit-jupiter-params:5.2.0') + dependency('org.jetbrains:annotations:16.0.2') + dependency('org.jetbrains.kotlin:kotlin-reflect:1.2.50') + dependency('org.jetbrains.kotlin:kotlin-stdlib:1.2.50') + dependency('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.50') + dependency('org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3') + dependency('org.jetbrains.spek:spek-api:1.1.5') + dependency('org.jetbrains.spek:spek-junit-platform-engine:1.1.5') + } +} diff --git a/eth-domain/build.gradle b/eth-domain/build.gradle new file mode 100644 index 00000000..d5b94fcf --- /dev/null +++ b/eth-domain/build.gradle @@ -0,0 +1,15 @@ +description = 'Classes and utilities for working with Ethereum domain objects.' + +dependencies { + compile project(':bytes') + compile project(':crypto') + compile project(':rlp') + compile project(':units') + + testCompile project(':junit') + testCompile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/Address.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Address.java new file mode 100644 index 00000000..1c61d98d --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Address.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; + +import com.google.common.base.Objects; + +/** + * An Ethereum account address. + */ +public final class Address { + + /** + * Create an address from Bytes. + * + *

+ * The address must be exactly 20 bytes. + * + * @param bytes The bytes for this address. + * @return An address. + * @throws IllegalArgumentException If {@code bytes.size() != 20}. + */ + public static Address fromBytes(Bytes bytes) { + requireNonNull(bytes); + checkArgument(bytes.size() == SIZE, "Expected %s bytes but got %s", SIZE, bytes.size()); + return new Address(bytes); + } + + private static final int SIZE = 20; + + private final Bytes delegate; + + private Address(Bytes value) { + this.delegate = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Address)) { + return false; + } + Address other = (Address) obj; + return delegate.equals(other.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } + + @Override + public String toString() { + return "Address{" + delegate.toHexString() + '}'; + } + + /** + * @return A hex-encoded version of the address. + */ + public String toHexString() { + return delegate.toHexString(); + } + + /** + * @return The bytes for this address. + */ + public Bytes toBytes() { + return delegate; + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/Block.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Block.java new file mode 100644 index 00000000..7ce1a66b --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Block.java @@ -0,0 +1,120 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.rlp.RLP; +import net.consensys.cava.rlp.RLPReader; +import net.consensys.cava.rlp.RLPWriter; + +import com.google.common.base.Objects; + +/** + * An Ethereum block. + */ +public final class Block { + + /** + * Deserialize a block from RLP encoded bytes. + * + * @param encoded The RLP encoded block. + * @return The deserialized block. + */ + public static Block fromBytes(Bytes encoded) { + requireNonNull(encoded); + return RLP.decodeList(encoded, Block::readFrom); + } + + /** + * Deserialize a block from an RLP input. + * + * @param reader The RLP reader. + * @return The deserialized block. + */ + public static Block readFrom(RLPReader reader) { + BlockHeader header = reader.readList(BlockHeader::readFrom); + BlockBody body = BlockBody.readFrom(reader); + return new Block(header, body); + } + + private final BlockHeader header; + private final BlockBody body; + + /** + * Creates a block. + * + * @param header the header of the block. + * @param body the body of the block. + */ + public Block(BlockHeader header, BlockBody body) { + requireNonNull(header); + requireNonNull(body); + this.header = header; + this.body = body; + } + + /** + * @return the block body. + */ + public BlockBody body() { + return body; + } + + /** + * @return the block header. + */ + public BlockHeader header() { + return header; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Block)) { + return false; + } + Block other = (Block) obj; + return Objects.equal(header, other.header) && Objects.equal(body, other.body); + } + + @Override + public int hashCode() { + return Objects.hashCode(header, body); + } + + @Override + public String toString() { + return "Block{" + "header=" + header + ", body=" + body + '}'; + } + + /** + * @return The RLP serialized form of this block. + */ + public Bytes toBytes() { + return RLP.encodeList(this::writeTo); + } + + /** + * Write this block to an RLP output. + * + * @param writer The RLP writer. + */ + public void writeTo(RLPWriter writer) { + writer.writeList(header::writeTo); + body.writeTo(writer); + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockBody.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockBody.java new file mode 100644 index 00000000..6f98acb0 --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockBody.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.rlp.RLP; +import net.consensys.cava.rlp.RLPReader; +import net.consensys.cava.rlp.RLPWriter; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.base.Objects; + +/** + * An Ethereum block body. + */ +public final class BlockBody { + + /** + * Deserialize a block body from RLP encoded bytes. + * + * @param encoded The RLP encoded block. + * @return The deserialized block body. + */ + public static BlockBody fromBytes(Bytes encoded) { + requireNonNull(encoded); + return RLP.decodeList(encoded, BlockBody::readFrom); + } + + static BlockBody readFrom(RLPReader reader) { + List txs = new ArrayList<>(); + reader.readList((listReader, l) -> { + while (!listReader.isComplete()) { + txs.add(listReader.readList(Transaction::readFrom)); + } + }); + List ommers = new ArrayList<>(); + reader.readList((listReader, l) -> { + while (!listReader.isComplete()) { + ommers.add(listReader.readList(BlockHeader::readFrom)); + } + }); + + return new BlockBody(txs, ommers); + } + + private final List transactions; + private final List ommers; + + /** + * Creates a new block body. + * + * @param transactions the list of transactions in this block. + * @param ommers the list of ommers for this block. + */ + public BlockBody(List transactions, List ommers) { + requireNonNull(transactions); + requireNonNull(ommers); + this.transactions = transactions; + this.ommers = ommers; + } + + /** + * @return the transactions of the block. + */ + public List transactions() { + return transactions; + } + + /** + * @return the list of ommers for this block. + */ + public List ommers() { + return ommers; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BlockBody)) { + return false; + } + BlockBody other = (BlockBody) obj; + return transactions.equals(other.transactions) && ommers.equals(other.ommers); + } + + @Override + public int hashCode() { + return Objects.hashCode(transactions, ommers); + } + + /** + * @return The RLP serialized form of this block body. + */ + public Bytes toBytes() { + return RLP.encodeList(this::writeTo); + } + + @Override + public String toString() { + return "BlockBody{" + "transactions=" + transactions + ", ommers=" + ommers + '}'; + } + + void writeTo(RLPWriter writer) { + writer.writeList(listWriter -> { + for (Transaction tx : transactions) { + listWriter.writeList(tx::writeTo); + } + }); + writer.writeList(listWriter -> { + for (BlockHeader ommer : ommers) { + listWriter.writeList(ommer::writeTo); + } + }); + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockHeader.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockHeader.java new file mode 100644 index 00000000..798da96d --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/BlockHeader.java @@ -0,0 +1,383 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.rlp.RLP; +import net.consensys.cava.rlp.RLPReader; +import net.consensys.cava.rlp.RLPWriter; +import net.consensys.cava.units.bigints.UInt256; +import net.consensys.cava.units.ethereum.Gas; + +import java.time.Instant; +import javax.annotation.Nullable; + +import com.google.common.base.Objects; + +/** + * An Ethereum block header. + */ +public final class BlockHeader { + + /** + * Deserialize a block header from RLP encoded bytes. + * + * @param encoded The RLP encoded block. + * @return The deserialized block header. + */ + public static BlockHeader fromBytes(Bytes encoded) { + requireNonNull(encoded); + return RLP.decodeList(encoded, BlockHeader::readFrom); + } + + /** + * Deserialize a block header from an RLP input. + * + * @param reader The RLP reader. + * @return The deserialized block header. + */ + static BlockHeader readFrom(RLPReader reader) { + Bytes parentHashBytes = reader.readValue(); + return new BlockHeader( + parentHashBytes.isEmpty() ? null : Hash.fromBytes(parentHashBytes), + Hash.fromBytes(reader.readValue()), + Address.fromBytes(reader.readValue()), + Hash.fromBytes(reader.readValue()), + Hash.fromBytes(reader.readValue()), + Hash.fromBytes(reader.readValue()), + reader.readValue(), + UInt256.fromBytes(reader.readValue()), + UInt256.fromBytes(reader.readValue()), + Gas.valueOf(reader.readUInt256()), + Gas.valueOf(reader.readUInt256()), + Instant.ofEpochSecond(reader.readLong()), + reader.readValue(), + Hash.fromBytes(reader.readValue()), + reader.readValue()); + } + + @Nullable + private final Hash parentHash; + private final Hash ommersHash; + private final Address coinbase; + private final Hash stateRoot; + private final Hash transactionsRoot; + private final Hash receiptsRoot; + private final Bytes logsBloom; + private final UInt256 difficulty; + private final UInt256 number; + private final Gas gasLimit; + private final Gas gasUsed; + private final Instant timestamp; + private final Bytes extraData; + private final Hash mixHash; + private final Bytes nonce; + private Hash hash; + + /** + * Creates a new block header. + * + * @param parentHash the parent hash, or null. + * @param ommersHash the ommers hash. + * @param coinbase the block's beneficiary address. + * @param stateRoot the hash associated with the state tree. + * @param transactionsRoot the hash associated with the transactions tree. + * @param receiptsRoot the hash associated with the transaction receipts tree. + * @param logsBloom the bloom filter of the logs of the block. + * @param difficulty the difficulty of the block. + * @param number the number of the block. + * @param gasLimit the gas limit of the block. + * @param gasUsed the gas used for the block. + * @param timestamp the timestamp of the block. + * @param extraData the extra data stored with the block. + * @param mixHash the hash associated with computional work on the block. + * @param nonce the nonce of the block. + */ + public BlockHeader( + @Nullable Hash parentHash, + Hash ommersHash, + Address coinbase, + Hash stateRoot, + Hash transactionsRoot, + Hash receiptsRoot, + Bytes logsBloom, + UInt256 difficulty, + UInt256 number, + Gas gasLimit, + Gas gasUsed, + Instant timestamp, + Bytes extraData, + Hash mixHash, + Bytes nonce) { + requireNonNull(ommersHash); + requireNonNull(coinbase); + requireNonNull(stateRoot); + requireNonNull(transactionsRoot); + requireNonNull(receiptsRoot); + requireNonNull(logsBloom); + requireNonNull(difficulty); + requireNonNull(number); + requireNonNull(gasLimit); + requireNonNull(gasUsed); + requireNonNull(timestamp); + requireNonNull(extraData); + requireNonNull(mixHash); + requireNonNull(nonce); + this.parentHash = parentHash; + this.ommersHash = ommersHash; + this.coinbase = coinbase; + this.stateRoot = stateRoot; + this.transactionsRoot = transactionsRoot; + this.receiptsRoot = receiptsRoot; + this.logsBloom = logsBloom; + this.difficulty = difficulty; + this.number = number; + this.gasLimit = gasLimit; + this.gasUsed = gasUsed; + this.timestamp = timestamp; + this.extraData = extraData; + this.mixHash = mixHash; + this.nonce = nonce; + } + + /** + * @return the block's beneficiary's address. + */ + public Address coinbase() { + return coinbase; + } + + /** + * @return the difficulty of the block. + */ + public UInt256 difficulty() { + return difficulty; + } + + /** + * @return the extra data stored with the block. + */ + public Bytes extraData() { + return extraData; + } + + /** + * @return the gas limit of the block. + */ + public Gas gasLimit() { + return gasLimit; + } + + /** + * @return the gas used for the block. + */ + public Gas gasUsed() { + return gasUsed; + } + + /** + * @return the hash of the block header. + */ + public Hash hash() { + if (hash == null) { + Bytes rlp = toBytes(); + hash = Hash.hash(rlp); + } + return hash; + } + + /** + * @return the bloom filter of the logs of the block. + */ + public Bytes logsBloom() { + return logsBloom; + } + + /** + * @return the hash associated with computional work on the block. + */ + public Hash mixHash() { + return mixHash; + } + + /** + * @return the nonce of the block. + */ + public Bytes nonce() { + return nonce; + } + + /** + * @return the number of the block. + */ + public UInt256 number() { + return number; + } + + /** + * @return the ommer hash. + */ + public Hash ommersHash() { + return ommersHash; + } + + /** + * @return the parent hash, or null if none was available. + */ + @Nullable + public Hash parentHash() { + return parentHash; + } + + /** + * @return the hash associated with the transaction receipts tree. + */ + public Hash receiptsRoot() { + return receiptsRoot; + } + + /** + * @return the hash associated with the state tree. + */ + public Hash stateRoot() { + return stateRoot; + } + + /** + * @return the timestamp of the block. + */ + public Instant timestamp() { + return timestamp; + } + + /** + * @return the hash associated with the transactions tree. + */ + public Hash transactionsRoot() { + return transactionsRoot; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BlockHeader)) { + return false; + } + BlockHeader other = (BlockHeader) obj; + return Objects.equal(parentHash, other.parentHash) + && ommersHash.equals(other.ommersHash) + && coinbase.equals(other.coinbase) + && stateRoot.equals(other.stateRoot) + && transactionsRoot.equals(other.transactionsRoot) + && receiptsRoot.equals(other.receiptsRoot) + && logsBloom.equals(other.logsBloom) + && difficulty.equals(other.difficulty) + && number.equals(other.number) + && gasLimit.equals(other.gasLimit) + && gasUsed.equals(other.gasUsed) + && timestamp.equals(other.timestamp) + && extraData.equals(other.extraData) + && mixHash.equals(other.mixHash) + && nonce.equals(other.nonce); + } + + @Override + public int hashCode() { + return Objects.hashCode( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp, + extraData, + mixHash, + nonce); + } + + @Override + public String toString() { + return "BlockHeader{" + + "parentHash=" + + parentHash + + ", ommersHash=" + + ommersHash + + ", coinbase=" + + coinbase + + ", stateRoot=" + + stateRoot + + ", transactionsRoot=" + + transactionsRoot + + ", receiptsRoot=" + + receiptsRoot + + ", logsBloom=" + + logsBloom + + ", difficulty=" + + difficulty + + ", number=" + + number + + ", gasLimit=" + + gasLimit + + ", gasUsed=" + + gasUsed + + ", timestamp=" + + timestamp + + ", extraData=" + + extraData + + ", mixHash=" + + mixHash + + ", nonce=" + + nonce + + '}'; + } + + /** + * @return The RLP serialized form of this block header. + */ + public Bytes toBytes() { + return RLP.encodeList(this::writeTo); + } + + /** + * Write this block header to an RLP output. + * + * @param writer The RLP writer. + */ + void writeTo(RLPWriter writer) { + writer.writeValue((parentHash != null) ? parentHash.toBytes() : Bytes.EMPTY); + writer.writeValue(ommersHash.toBytes()); + writer.writeValue(coinbase.toBytes()); + writer.writeValue(stateRoot.toBytes()); + writer.writeValue(transactionsRoot.toBytes()); + writer.writeValue(receiptsRoot.toBytes()); + writer.writeValue(logsBloom); + writer.writeValue(difficulty.toMinimalBytes()); + writer.writeValue(number.toMinimalBytes()); + writer.writeValue(gasLimit.toMinimalBytes()); + writer.writeValue(gasUsed.toMinimalBytes()); + writer.writeLong(timestamp.getEpochSecond()); + writer.writeValue(extraData); + writer.writeValue(mixHash.toBytes()); + writer.writeValue(nonce); + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/Hash.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Hash.java new file mode 100644 index 00000000..2e4b0f54 --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Hash.java @@ -0,0 +1,104 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.crypto.Hash.keccak256; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; + +import com.google.common.base.Objects; + +/** + * An Ethereum hash. + */ +public final class Hash { + + /** + * Create a Hash from Bytes. + * + *

+ * The hash must be exactly 32 bytes. + * + * @param bytes The bytes for this hash. + * @return A hash. + * @throws IllegalArgumentException If {@code bytes.size() != 32}. + */ + public static Hash fromBytes(Bytes bytes) { + requireNonNull(bytes); + checkArgument(bytes.size() == SIZE, "Expected %s bytes but got %s", SIZE, bytes.size()); + return new Hash(Bytes32.wrap(bytes)); + } + + /** + * Create a Hash from Bytes32. + * + * @param bytes The bytes for this hash. + * @return A hash. + */ + public static Hash fromBytes(Bytes32 bytes) { + requireNonNull(bytes); + return new Hash(bytes); + } + + public static Hash hash(Bytes value) { + return new Hash(keccak256(value)); + } + + private static final int SIZE = 32; + + private final Bytes32 delegate; + + private Hash(Bytes32 value) { + requireNonNull(value); + this.delegate = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Hash)) { + return false; + } + Hash hash = (Hash) obj; + return delegate.equals(hash.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } + + @Override + public String toString() { + return "Hash{" + delegate.toHexString() + '}'; + } + + /** + * @return A hex-encoded version of the hash. + */ + public String toHexString() { + return delegate.toHexString(); + } + + /** + * @return The bytes for this hash. + */ + public Bytes toBytes() { + return delegate; + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/Transaction.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Transaction.java new file mode 100644 index 00000000..41da59aa --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/Transaction.java @@ -0,0 +1,306 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.crypto.SECP256K1.PublicKey; +import net.consensys.cava.crypto.SECP256K1.Signature; +import net.consensys.cava.rlp.InvalidRLPEncodingException; +import net.consensys.cava.rlp.InvalidRLPTypeException; +import net.consensys.cava.rlp.RLP; +import net.consensys.cava.rlp.RLPReader; +import net.consensys.cava.rlp.RLPWriter; +import net.consensys.cava.units.bigints.UInt256; +import net.consensys.cava.units.ethereum.Gas; +import net.consensys.cava.units.ethereum.Wei; + +import java.lang.ref.SoftReference; +import java.math.BigInteger; +import javax.annotation.Nullable; + +import com.google.common.base.Objects; + +/** + * An Ethereum transaction. + */ +public final class Transaction { + + /** + * Deserialize a transaction from RLP encoded bytes. + * + * @param encoded The RLP encoded transaction. + * @return The deserialized transaction. + */ + public static Transaction fromBytes(Bytes encoded) { + requireNonNull(encoded); + return RLP.decode(encoded, (reader) -> { + Transaction tx = reader.readList(Transaction::readFrom); + if (!reader.isComplete()) { + throw new InvalidRLPTypeException("Additional bytes present at the end of the encoded transaction"); + } + return tx; + }); + } + + /** + * Deserialize a transaction from an RLP input. + * + * @param reader The RLP reader. + * @return The deserialized transaction. + */ + public static Transaction readFrom(RLPReader reader) { + UInt256 nonce = fromMinimalBytes(reader.readValue(), "nonce"); + Wei gasPrice = Wei.valueOf(fromMinimalBytes(reader.readValue(), "gasPrice")); + Gas gasLimit = Gas.valueOf(fromMinimalBytes(reader.readValue(), "gasLimit")); + Bytes addressBytes = reader.readValue(); + Address address = addressBytes.isEmpty() ? null : Address.fromBytes(addressBytes); + Wei value = Wei.valueOf(fromMinimalBytes(reader.readValue(), "wei")); + Bytes payload = reader.readValue(); + Bytes vbytes = reader.readValue(); + if (vbytes.size() != 1) { + throw new IllegalArgumentException( + "The 'v' portion of the signature should be exactly 1 byte, it is " + vbytes.size() + " instead"); + } + byte v = vbytes.get(0); + Bytes rbytes = reader.readValue(); + if (rbytes.hasLeadingZeroByte()) { + throw new IllegalArgumentException("The 'r' portion of the signature contains leading zero-byte values"); + } + if (rbytes.size() > 32) { + throw new IllegalArgumentException( + "The length of the 'r' portion of the signature is " + rbytes.size() + ", it should be at most 32 bytes"); + } + BigInteger r = rbytes.unsignedBigIntegerValue(); + Bytes sbytes = reader.readValue(); + if (sbytes.hasLeadingZeroByte()) { + throw new IllegalArgumentException("The 's' portion of the signature contains leading zero-byte values"); + } + if (sbytes.size() > 32) { + throw new IllegalArgumentException( + "The length of the 's' portion of the signature is " + sbytes.size() + ", it should be at most 32 bytes"); + } + BigInteger s = sbytes.unsignedBigIntegerValue(); + if (!reader.isComplete()) { + throw new InvalidRLPTypeException("Additional bytes present at the end of the encoded transaction list"); + } + return new Transaction(nonce, gasPrice, gasLimit, address, value, payload, Signature.create(v, r, s)); + } + + private final UInt256 nonce; + private final Wei gasPrice; + private final Gas gasLimit; + @Nullable + private final Address to; + private final Wei value; + private final Signature signature; + private final Bytes payload; + private SoftReference hash; + + /** + * Create a transaction. + * + * @param nonce The transaction nonce. + * @param gasPrice The transaction gas price. + * @param gasLimit The transaction gas limit. + * @param to The target contract address, if any. + * @param value The amount of Eth to transfer. + * @param payload The transaction payload. + * @param signature The transaction signature. + */ + public Transaction( + UInt256 nonce, + Wei gasPrice, + Gas gasLimit, + @Nullable Address to, + Wei value, + Bytes payload, + Signature signature) { + requireNonNull(nonce); + checkArgument(nonce.compareTo(UInt256.ZERO) >= 0, "Nonce less than zero"); + requireNonNull(gasPrice); + requireNonNull(value); + requireNonNull(signature); + requireNonNull(payload); + this.nonce = nonce; + this.gasPrice = gasPrice; + this.gasLimit = gasLimit; + this.to = to; + this.value = value; + this.signature = signature; + this.payload = payload; + } + + /** + * @return The transaction nonce. + */ + public UInt256 nonce() { + return nonce; + } + + /** + * @return The transaction gas price. + */ + public Wei gasPrice() { + return gasPrice; + } + + /** + * @return The transaction gas limit. + */ + public Gas gasLimit() { + return gasLimit; + } + + /** + * @return The target contract address, or null if not present. + */ + @Nullable + public Address to() { + return to; + } + + /** + * @return The amount of Eth to transfer. + */ + public Wei value() { + return value; + } + + /** + * @return The transaction signature. + */ + public Signature signature() { + return signature; + } + + /** + * @return The transaction payload. + */ + public Bytes payload() { + return payload; + } + + /** + * Calculate and return the hash for this transaction. + * + *

+ * Note: the hash is calculated lazily and stored (as a {@link SoftReference} for future access. + * + * @return The hash. + */ + public Hash hash() { + if (hash != null) { + Hash hashed = hash.get(); + if (hashed != null) { + return hashed; + } + } + Bytes rlp = toBytes(); + Hash hashed = Hash.hash(rlp); + hash = new SoftReference<>(hashed); + return hashed; + } + + /** + * @return The sender of the transaction. + */ + public Address sender() { + PublicKey publicKey = PublicKey.recoverFromSignature(RLP.encodeList(writer -> { + writer.writeValue(nonce().toMinimalBytes()); + writer.writeValue(gasPrice().toMinimalBytes()); + writer.writeValue(gasLimit().toMinimalBytes()); + Address to = to(); + writer.writeValue((to != null) ? to.toBytes() : Bytes.EMPTY); + writer.writeValue(value().toMinimalBytes()); + writer.writeValue(payload()); + }), signature()); + return Address.fromBytes(Hash.hash(publicKey.encodedBytes()).toBytes().slice(12, 20)); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Transaction)) { + return false; + } + Transaction that = (Transaction) obj; + return nonce.equals(that.nonce) + && gasPrice.equals(that.gasPrice) + && gasLimit.equals(that.gasLimit) + && Objects.equal(to, that.to) + && value.equals(that.value) + && signature.equals(that.signature) + && payload.equals(that.payload); + } + + @Override + public int hashCode() { + return Objects.hashCode(nonce, gasPrice, gasLimit, to, value, signature, payload); + } + + @Override + public String toString() { + return "Transaction{" + + "nonce=" + + nonce + + ", gasPrice=" + + gasPrice + + ", gasLimit=" + + gasLimit + + ", to=" + + to + + ", value=" + + value + + ", signature=" + + signature + + ", payload=" + + payload + + '}'; + } + + /** + * @return The RLP serialized form of this transaction. + */ + public Bytes toBytes() { + return RLP.encodeList(this::writeTo); + } + + /** + * Write this transaction to an RLP output. + * + * @param writer The RLP writer. + */ + public void writeTo(RLPWriter writer) { + writer.writeValue(nonce.toMinimalBytes()); + writer.writeValue(gasPrice.toMinimalBytes()); + writer.writeValue(gasLimit.toMinimalBytes()); + writer.writeValue((to != null) ? to.toBytes() : Bytes.EMPTY); + writer.writeValue(value.toMinimalBytes()); + writer.writeValue(payload); + writer.writeValue(Bytes.of(signature.v())); + writer.writeBigInteger(signature.r()); + writer.writeBigInteger(signature.s()); + } + + private static UInt256 fromMinimalBytes(Bytes bytes, String fieldName) { + if (bytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("Unexpected leading zero byte in encoding of " + fieldName); + } + return UInt256.fromBytes(bytes); + } +} diff --git a/eth-domain/src/main/java/net/consensys/cava/eth/domain/package-info.java b/eth-domain/src/main/java/net/consensys/cava/eth/domain/package-info.java new file mode 100644 index 00000000..5be510de --- /dev/null +++ b/eth-domain/src/main/java/net/consensys/cava/eth/domain/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes and utilities for working with Ethereum domain objects. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-eth-domain' (cava-eth-domain.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.eth.domain; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockBodyTest.java b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockBodyTest.java new file mode 100644 index 00000000..4494d8de --- /dev/null +++ b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockBodyTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static net.consensys.cava.eth.domain.BlockHeaderTest.generateBlockHeader; +import static net.consensys.cava.eth.domain.TransactionTest.generateTransaction; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.junit.BouncyCastleExtension; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class BlockBodyTest { + + @Test + void testRLPRoundtrip() { + BlockBody blockBody = new BlockBody( + Arrays.asList(generateTransaction(), generateTransaction(), generateTransaction(), generateTransaction()), + Arrays.asList( + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader())); + Bytes encoded = blockBody.toBytes(); + BlockBody read = BlockBody.fromBytes(encoded); + assertEquals(blockBody, read); + } + +} diff --git a/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockHeaderTest.java b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockHeaderTest.java new file mode 100644 index 00000000..ed3cfb81 --- /dev/null +++ b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockHeaderTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static net.consensys.cava.eth.domain.TransactionTest.randomBytes; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.units.bigints.UInt256; +import net.consensys.cava.units.ethereum.Gas; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class BlockHeaderTest { + + static BlockHeader generateBlockHeader() { + return new BlockHeader( + Hash.fromBytes(randomBytes(32)), + Hash.fromBytes(randomBytes(32)), + Address.fromBytes(Bytes.fromHexString("0x0102030405060708091011121314151617181920")), + Hash.fromBytes(randomBytes(32)), + Hash.fromBytes(randomBytes(32)), + Hash.fromBytes(randomBytes(32)), + randomBytes(8), + UInt256.fromBytes(randomBytes(32)), + UInt256.fromBytes(randomBytes(32)), + Gas.valueOf(UInt256.fromBytes(randomBytes(6))), + Gas.valueOf(UInt256.fromBytes(randomBytes(6))), + Instant.now().truncatedTo(ChronoUnit.SECONDS), + randomBytes(22), + Hash.fromBytes(randomBytes(32)), + randomBytes(8)); + } + + @Test + void rlpRoundtrip() { + BlockHeader blockHeader = generateBlockHeader(); + BlockHeader read = BlockHeader.fromBytes(blockHeader.toBytes()); + assertEquals(blockHeader, read); + } + +} diff --git a/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockTest.java b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockTest.java new file mode 100644 index 00000000..d6d8e9b2 --- /dev/null +++ b/eth-domain/src/test/java/net/consensys/cava/eth/domain/BlockTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static net.consensys.cava.eth.domain.BlockHeaderTest.generateBlockHeader; +import static net.consensys.cava.eth.domain.TransactionTest.generateTransaction; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.junit.BouncyCastleExtension; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class BlockTest { + + @Test + void testRoundtripRLP() { + Block block = new Block( + generateBlockHeader(), + new BlockBody( + Arrays.asList(generateTransaction(), generateTransaction(), generateTransaction(), generateTransaction()), + Arrays.asList( + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader(), + generateBlockHeader()))); + Bytes encoded = block.toBytes(); + Block read = Block.fromBytes(encoded); + assertEquals(block, read); + } +} diff --git a/eth-domain/src/test/java/net/consensys/cava/eth/domain/TransactionTest.java b/eth-domain/src/test/java/net/consensys/cava/eth/domain/TransactionTest.java new file mode 100644 index 00000000..f1d02e3c --- /dev/null +++ b/eth-domain/src/test/java/net/consensys/cava/eth/domain/TransactionTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.crypto.SECP256K1.Signature; +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.units.bigints.UInt256; +import net.consensys.cava.units.ethereum.Gas; +import net.consensys.cava.units.ethereum.Wei; + +import java.math.BigInteger; +import java.security.SecureRandom; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class TransactionTest { + + static Bytes randomBytes(int length) { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + return Bytes.wrap(bytes); + } + + static Transaction generateTransaction() { + return new Transaction( + UInt256.valueOf(0), + Wei.valueOf(BigInteger.valueOf(5L)), + Gas.valueOf(10L), + Address.fromBytes(Bytes.fromHexString("0x0102030405060708091011121314151617181920")), + Wei.valueOf(10L), + Bytes.of(1, 2, 3, 4), + Signature.create(randomBytes(65))); + } + + @Test + void testRLPRoundtrip() { + Transaction tx = generateTransaction(); + Bytes encoded = tx.toBytes(); + Transaction read = Transaction.fromBytes(encoded); + assertEquals(tx, read); + } +} diff --git a/eth-reference-tests/build.gradle b/eth-reference-tests/build.gradle new file mode 100644 index 00000000..f17b0519 --- /dev/null +++ b/eth-reference-tests/build.gradle @@ -0,0 +1,15 @@ +jar { enabled = false } + +dependencies { + testCompile project(':eth-domain') + testCompile project(':merkle-trie') + testCompile project(':rlp') + + testCompile project(':junit') + testCompile 'com.fasterxml.jackson.core:jackson-databind:2.9.5' + testCompile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/BlockRLPTestSuite.java b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/BlockRLPTestSuite.java new file mode 100644 index 00000000..ae4d17e9 --- /dev/null +++ b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/BlockRLPTestSuite.java @@ -0,0 +1,168 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.reference; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.crypto.SECP256K1.Signature; +import net.consensys.cava.eth.domain.Address; +import net.consensys.cava.eth.domain.Block; +import net.consensys.cava.eth.domain.BlockBody; +import net.consensys.cava.eth.domain.BlockHeader; +import net.consensys.cava.eth.domain.Hash; +import net.consensys.cava.eth.domain.Transaction; +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.units.bigints.UInt256; +import net.consensys.cava.units.ethereum.Gas; +import net.consensys.cava.units.ethereum.Wei; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Streams; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BouncyCastleExtension.class) +class BlockRLPTestSuite { + + private static ObjectMapper mapper = new ObjectMapper(); + + @ParameterizedTest(name = "{index}. block {0}/{1}/{2}[{3}]") + @MethodSource("readBlockChainTests") + void testBlockRLP( + String folder, + String fileName, + String name, + long blockIndex, + Block block, + String rlp, + String hash) { + Block rlpBlock = Block.fromBytes(Bytes.fromHexString(rlp)); + assertEquals(block, rlpBlock); + assertEquals(Bytes.fromHexString(rlp), block.toBytes()); + assertEquals(Hash.fromBytes(Bytes.fromHexString(hash)), block.header().hash()); + assertEquals(Hash.fromBytes(Bytes.fromHexString(hash)), rlpBlock.header().hash()); + } + + private static Stream readBlockChainTests() throws IOException { + URL testFolder = MerkleTrieTestSuite.class.getClassLoader().getResource("tests"); + if (testFolder == null) { + throw new RuntimeException("Tests folder missing. Please run git submodule --init"); + } + Path folderPath = Paths.get(testFolder.getFile(), "BlockchainTests"); + + List testCases = new ArrayList<>(); + try (Stream walker = Files.walk(folderPath)) { + walker.filter(path -> path.toString().endsWith(".json")).forEach( + file -> testCases.addAll(readTestCase(file).collect(Collectors.toList()))); + testCases.sort(Comparator.comparing(a -> ((String) a.get()[0]))); + return testCases.stream(); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Stream readTestCase(Path testFile) { + try { + Map test = mapper.readerFor(Map.class).readValue(testFile.toFile()); + String name = (String) test.keySet().iterator().next(); + Map testData = (Map) test.get(name); + List blocks = (List) testData.get("blocks"); + return Streams.mapWithIndex( + blocks.stream().filter(block -> ((Map) block).containsKey("blockHeader")), + (block, index) -> Arguments.of( + testFile.getName(testFile.getNameCount() - 2).toString(), + testFile.getName(testFile.getNameCount() - 1).toString(), + name, + index, + createBlock((Map) block), + ((Map) block).get("rlp"), + ((Map) ((Map) block).get("blockHeader")).get("hash"))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static BlockHeader createBlockHeader(Map headerData) { + checkNotNull(headerData, "" + headerData); + return new BlockHeader( + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("parentHash"))), + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("uncleHash"))), + Address.fromBytes(Bytes.fromHexString((String) headerData.get("coinbase"))), + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("stateRoot"))), + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("transactionsTrie"))), + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("receiptTrie"))), + Bytes.fromHexString((String) headerData.get("bloom")), + UInt256.fromBytes(Bytes.fromHexString((String) headerData.get("difficulty"))), + UInt256.fromBytes(Bytes.fromHexString((String) headerData.get("number"))), + Gas.valueOf(UInt256.fromBytes(Bytes.fromHexString((String) headerData.get("gasLimit")))), + Gas.valueOf(UInt256.fromBytes(Bytes.fromHexString((String) headerData.get("gasUsed")))), + Instant.ofEpochSecond(Bytes.fromHexString((String) headerData.get("timestamp")).longValue()), + Bytes.fromHexString((String) headerData.get("extraData")), + Hash.fromBytes(Bytes.fromHexString((String) headerData.get("mixHash"))), + Bytes.fromHexString((String) headerData.get("nonce"))); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Block createBlock(Map blockData) { + Map headerData = (Map) blockData.get("blockHeader"); + BlockHeader header = createBlockHeader(headerData); + List transactions = new ArrayList<>(); + for (Object txDataObj : (List) blockData.get("transactions")) { + Map txData = (Map) txDataObj; + + Address toAddress = null; + String toAddressString = (String) txData.get("to"); + if (toAddressString != null) { + Bytes toAddressBytes = Bytes.fromHexString(toAddressString); + toAddress = toAddressBytes.isEmpty() ? null : Address.fromBytes(toAddressBytes); + } + + transactions.add( + new Transaction( + UInt256.fromBytes(Bytes.fromHexString((String) txData.get("nonce"))), + Wei.valueOf(UInt256.fromBytes(Bytes.fromHexString((String) txData.get("gasPrice")))), + Gas.valueOf(UInt256.fromBytes(Bytes.fromHexString((String) txData.get("gasLimit")))), + toAddress, + Wei.valueOf(UInt256.fromBytes(Bytes.fromHexString((String) txData.get("value")))), + Bytes.fromHexString((String) txData.get("data")), + Signature.create( + Bytes.fromHexString((String) txData.get("v")).get(0), + Bytes.fromHexString((String) txData.get("r")).unsignedBigIntegerValue(), + Bytes.fromHexString((String) txData.get("s")).unsignedBigIntegerValue()))); + } + List ommers = new ArrayList<>(); + for (Object ommerDataObj : (List) blockData.get("uncleHeaders")) { + ommers.add(createBlockHeader((Map) ommerDataObj)); + } + + return new Block(header, new BlockBody(transactions, ommers)); + } +} diff --git a/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/MerkleTrieTestSuite.java b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/MerkleTrieTestSuite.java new file mode 100644 index 00000000..a3b5441b --- /dev/null +++ b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/MerkleTrieTestSuite.java @@ -0,0 +1,101 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.reference; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.trie.experimental.MerklePatriciaTrie; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BouncyCastleExtension.class) +class MerkleTrieTestSuite { + + private Bytes readFromString(String value) { + if (value.startsWith("0x")) { + return Bytes.fromHexString(value); + } else { + return Bytes.wrap(value.getBytes(UTF_8)); + } + } + + @ParameterizedTest(name = "{index}. {0}") + @MethodSource("readAnyOrderTrieTests") + @SuppressWarnings({"unchecked", "rawtypes"}) + void testAnyOrderTrieTrees(String name, Map input, String root) throws Exception { + MerklePatriciaTrie trie = new MerklePatriciaTrie<>((Function) this::readFromString); + for (Object entry : input.entrySet()) { + Map.Entry keyValue = (Map.Entry) entry; + trie.putAsync(readFromString((String) keyValue.getKey()), (String) keyValue.getValue()).join(); + } + assertEquals(Bytes.fromHexString(root), trie.rootHash()); + } + + @ParameterizedTest(name = "{index}. {0}") + @MethodSource("readTrieTests") + @SuppressWarnings({"unchecked", "rawtypes"}) + void testTrieTrees(String name, List input, String root) throws Exception { + MerklePatriciaTrie trie = new MerklePatriciaTrie<>((Function) this::readFromString); + for (Object entry : input) { + List keyValue = (List) entry; + trie.putAsync(readFromString((String) keyValue.get(0)), (String) keyValue.get(1)).join(); + } + assertEquals(Bytes.fromHexString(root), trie.rootHash()); + } + + private static Stream readTrieTests() throws IOException { + return prepareTests(Paths.get("TrieTests", "trietest.json")); + } + + private static Stream readAnyOrderTrieTests() throws IOException { + return prepareTests(Paths.get("TrieTests", "trieanyorder.json")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Stream prepareTests(Path path) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + Map allTests = mapper.readerFor(Map.class).readValue(findTests(path)); + return allTests.entrySet().stream().map(entry -> { + String name = (String) ((Map.Entry) entry).getKey(); + return Arguments.of( + name, + ((Map) ((Map.Entry) entry).getValue()).get("in"), + ((Map) ((Map.Entry) entry).getValue()).get("root")); + }); + } + + private static File findTests(Path testsPath) { + URL testFolder = MerkleTrieTestSuite.class.getClassLoader().getResource("tests"); + if (testFolder == null) { + throw new RuntimeException("Tests folder missing. Please run git submodule --init"); + } + return Paths.get(testFolder.getFile()).resolve(testsPath).toFile(); + } +} diff --git a/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/RLPReferenceTestSuite.java b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/RLPReferenceTestSuite.java new file mode 100644 index 00000000..c255b0d2 --- /dev/null +++ b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/RLPReferenceTestSuite.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.reference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.rlp.RLP; +import net.consensys.cava.rlp.RLPException; +import net.consensys.cava.rlp.RLPReader; +import net.consensys.cava.rlp.RLPWriter; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class RLPReferenceTestSuite { + private static void writePayload(RLPWriter writer, Object in) { + if (in instanceof String && ((String) in).startsWith("#")) { + writer.writeBigInteger(new BigInteger(((String) in).substring(1))); + } else if (in instanceof String) { + writer.writeString((String) in); + } else if (in instanceof BigInteger) { + writer.writeBigInteger((BigInteger) in); + } else if (in instanceof Integer) { + writer.writeValue(Bytes.minimalBytes((Integer) in)); + } else if (in instanceof List) { + writer.writeList((listWriter) -> { + for (Object elt : (List) in) { + writePayload(listWriter, elt); + } + }); + } else { + throw new UnsupportedOperationException(); + } + } + + private static Object readPayload(RLPReader reader, Object in) { + if (in instanceof List) { + return reader.readList((listReader, list) -> { + for (Object elt : ((List) in)) { + list.add(readPayload(listReader, elt)); + } + }); + } else if (in instanceof BigInteger) { + return reader.readBigInteger(); + } else if (in instanceof String) { + return reader.readString(); + } else if (in instanceof Integer) { + return reader.readInt(); + } else { + throw new UnsupportedOperationException(); + } + } + + @ParameterizedTest(name = "{index}. write {0}") + @MethodSource("readRLPTests") + void testWriteRLP(String name, Object in, String out) throws IOException { + Bytes encoded = RLP.encode((writer) -> writePayload(writer, in)); + assertEquals(Bytes.fromHexString(out), encoded, "Input was of type " + in.getClass()); + } + + @ParameterizedTest(name = "{index}. read {0}") + @MethodSource("readRLPTests") + void testReadRLP(String name, Object in, String out) { + if (in instanceof String && ((String) in).startsWith("#")) { + in = new BigInteger(((String) in).substring(1)); + } + Object payload = in; + Object decoded = RLP.decode(Bytes.fromHexString(out), (reader) -> readPayload(reader, payload)); + assertEquals(in, decoded); + } + + @ParameterizedTest(name = "{index}. invalid {0}") + @MethodSource("readInvalidRLPTests") + void testReadInvalidRLP(String name, Object in, String out) { + assertThrows(RLPException.class, () -> { + if ("incorrectLengthInArray".equals(name)) { + RLP.decodeList(Bytes.fromHexString(out), (reader, list) -> { + }); + } else { + RLP.decodeValue(Bytes.fromHexString(out)); + } + }); + } + + private static Stream readRLPTests() throws IOException { + return prepareRLPTests(Paths.get("RLPTests", "rlptest.json")); + } + + private static Stream readInvalidRLPTests() throws IOException { + return prepareRLPTests(Paths.get("RLPTests", "invalidRLPTest.json")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Stream prepareRLPTests(Path testsPath) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + Map allTests = mapper.readerFor(Map.class).readValue(findTests(testsPath)); + return allTests.entrySet().stream().map(entry -> { + String name = (String) ((Map.Entry) entry).getKey(); + return Arguments.of( + name, + ((Map) ((Map.Entry) entry).getValue()).get("in"), + ((Map) ((Map.Entry) entry).getValue()).get("out")); + }); + } + + private static File findTests(Path testsPath) { + URL testFolder = RLPReferenceTestSuite.class.getClassLoader().getResource("tests"); + if (testFolder == null) { + throw new RuntimeException("Tests folder missing. Please run git submodule --init"); + } + return Paths.get(testFolder.getFile()).resolve(testsPath).toFile(); + } +} diff --git a/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/TransactionTestSuite.java b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/TransactionTestSuite.java new file mode 100644 index 00000000..919467ac --- /dev/null +++ b/eth-reference-tests/src/test/java/net/consensys/cava/eth/reference/TransactionTestSuite.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.eth.reference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.eth.domain.Address; +import net.consensys.cava.eth.domain.Transaction; +import net.consensys.cava.junit.BouncyCastleExtension; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BouncyCastleExtension.class) +class TransactionTestSuite { + + private static ObjectMapper mapper = new ObjectMapper(); + + @ParameterizedTest(name = "{index}. tx {0}/{1}/{2}") + @MethodSource("readTransactionTests") + void testTransaction(String folder, String name, String milestone, String rlp, String hash, String sender) { + if (hash == null && sender == null) { + assertThrows(Throwable.class, () -> { + Transaction tx = Transaction.fromBytes(Bytes.fromHexString(rlp)); + tx.sender(); + }); + } else { + Bytes rlpBytes = Bytes.fromHexString(rlp); + Transaction tx = Transaction.fromBytes(rlpBytes); + assertEquals(Address.fromBytes(Bytes.fromHexString(sender)), tx.sender()); + assertEquals(rlpBytes, tx.toBytes()); + assertEquals(Bytes.fromHexString(hash), tx.hash().toBytes()); + } + } + + private static Stream readTransactionTests() throws IOException { + URL testFolder = MerkleTrieTestSuite.class.getClassLoader().getResource("tests"); + if (testFolder == null) { + throw new RuntimeException("Tests folder missing. Please run git submodule --init"); + } + Path folderPath = Paths.get(testFolder.getFile(), "TransactionTests"); + List tests = Arrays + .stream(folderPath.toFile().listFiles(File::isDirectory)) + .map(folder -> folder.listFiles((file) -> file.getName().endsWith(".json"))) + .collect(ArrayList::new, (list, files) -> { + for (File f : files) { + if (!f.getName().contains("GasLimitOverflow") + && !f.getName().contains("GasLimitxPriceOverflow") + && !f.getName().contains("NotEnoughGas") + && !f.getName().contains("NotEnoughGAS") + && !f.getName().contains("EmptyTransaction")) { + list.addAll(readTestCase(f)); + } + } + }, List::addAll); + tests.sort(Comparator.comparing(a -> ((String) a.get()[0]))); + return tests.stream(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static List readTestCase(File testFile) { + try { + Map test = mapper.readerFor(Map.class).readValue(testFile); + String name = (String) test.keySet().iterator().next(); + Map testData = (Map) test.get(name); + String rlp = (String) testData.get("rlp"); + List arguments = new ArrayList<>(); + for (String milestone : new String[] { + // "Byzantium", + // "Constantinople", + // "EIP150", + // "EIP158", + "Frontier", + "Homestead"}) { + Map milestoneData = (Map) testData.get(milestone); + if (!milestoneData.isEmpty()) { + arguments.add( + Arguments.of( + testFile.getParentFile().getName(), + name, + milestone, + rlp, + milestoneData.get("hash"), + milestoneData.get("sender"))); + } + } + if (arguments.isEmpty()) { + arguments.add(Arguments.of(testFile.getParentFile().getName(), name, "(no milestone)", rlp, null, null)); + } + + return arguments; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/eth-reference-tests/src/test/resources/tests b/eth-reference-tests/src/test/resources/tests new file mode 160000 index 00000000..428842e9 --- /dev/null +++ b/eth-reference-tests/src/test/resources/tests @@ -0,0 +1 @@ +Subproject commit 428842e9731b7065366dcd7829bbfc0f9e1135fa diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle new file mode 100644 index 00000000..3ae51f0b --- /dev/null +++ b/gradle/check-licenses.gradle @@ -0,0 +1,152 @@ +/** + * Check that the licenses of our 3rd parties are in our acceptedLicenses list. + * + * run it with "gradle checkLicenses" + * + * To add new accepted licenses you need to update this script. + * Some products may be available with multiple licenses. In this case you must update + * this script to add it in the downloadLicenses#licenses. + */ + +// Some parts of this code comes from Zipkin/https://github.com/openzipkin/zipkin/pull/852 +// Zipkin itself is under Apache License. + +/** + * The lists of the license we accept. + */ +ext.acceptedLicenses = [ + 'Apache License, Version 2.0', + 'Bouncy Castle Licence', + 'The 2-Clause BSD License', + 'The 3-Clause BSD License', + 'Common Development and Distribution License 1.0', + 'Eclipse Distribution License - v 1.0', + 'Eclipse Public License 1.0', + 'Eclipse Public License 2.0', + 'The MIT License', + 'Unicode/ICU License', +]*.toLowerCase() + +/** + * This is the configuration we need for our licenses plugin: 'com.github.hierynomus.license' + * This plugin generates a list of dependencies. + */ +downloadLicenses { + includeProjectDependencies = true + reportByDependency = false + reportByLicenseType = true + dependencyConfiguration = 'compileClasspath' + + ext.apache2 = license( + 'Apache License, Version 2.0', + 'http://opensource.org/licenses/Apache-2.0') + ext.bsd = license( + 'The 2-Clause BSD License', + 'https://opensource.org/licenses/BSD-2-Clause') + ext.bsd3Clause = license( + 'The 3-Clause BSD License', + 'http://opensource.org/licenses/BSD-3-Clause') + ext.cddl1 = license( + 'Common Development and Distribution License 1.0', + 'https://opensource.org/licenses/CDDL-1.0') + ext.edl1 = license( + 'Eclipse Distribution License - v 1.0', + 'http://www.eclipse.org/org/documents/edl-v10.html') + ext.epl1 = license( + 'Eclipse Public License 1.0', + 'https://opensource.org/licenses/EPL-1.0') + ext.epl2 = license( + 'Eclipse Public License 2.0', + 'https://opensource.org/licenses/EPL-2.0') + ext.mit = license( + 'The MIT License', + 'https://opensource.org/licenses/MIT') + + aliases = [ + (apache2): [ + 'Apache', + 'Apache 2', + 'Apache 2.0', + 'Apache-2.0', + 'Apache License', + 'Apache License 2.0', + 'Apache License Version 2.0', + 'Apache License, Version 2.0', + 'Apache Software Licenses', + 'ASL, Version 2', + 'The Apache License, Version 2.0', + 'The Apache Software License, Version 2.0', + ], + (bsd): [ + 'Berkeley Software Distribution (BSD) License', + 'BSD', + 'BSD licence', + 'BSD Licence', + 'BSD License', + 'New BSD License', + 'The BSD Licence', + 'The BSD License', + ], + (bsd3Clause): [ + 'BSD 3-Clause', + 'BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)', + 'The 3-Clause BSD License', + 'The BSD 3-Clause License', + ], + (cddl1): [ + 'CDDL-1.0', + 'Common Development and Distribution License 1.0', + 'Dual license consisting of the CDDL v1.1 and GPL v2', + ], + (edl1): [ + 'Eclipse Distribution License - v 1.0', + ], + (epl1): [ + 'Eclipse Public License - v 1.0', + ], + (epl2): [ + 'Eclipse Public License v2.0', + 'Eclipse Public License - v 2.0', + ], + (mit): [ + 'MIT license', + 'MIT License', + ], + ] + + licenses = [ + (group('cava')): apache2, + + // https://checkerframework.org/manual/#license + // The more permissive MIT License applies to code that you might want + // to include in your own program, such as the annotations and run-time utility classes. + (group('org.checkerframework')): mit + ] +} + + +task checkLicenses { + description "Verify that all dependencies use white-listed licenses." + dependsOn ':downloadLicenses' + + def bads = "" + doLast { + def xml = new XmlParser().parse("$rootProject.buildDir/reports/license/license-dependency.xml") + xml.each { license -> + if (!acceptedLicenses.contains((license.@name).toLowerCase())) { + def depStrings = [] + license.dependency.each { depStrings << it.text() } + bads = bads + depStrings + " => -${license.@name}- \n" + } + } + if (bads != "") { + throw new GradleException("Some 3rd parties are using licenses not in our accepted licenses list:\n" + + bads + + "If it's a license acceptable for us, add it in the file check-licenses.gradle.\n" + + "Be careful, some 3rd parties may accept multiple licenses.\n" + + "In this case, select the one you want to use by changing downloadLicenses.licenses\n" + ) + } + } +} +check.dependsOn checkLicenses diff --git a/gradle/eclipse-java-consensys-style.xml b/gradle/eclipse-java-consensys-style.xml new file mode 100644 index 00000000..c1c31658 --- /dev/null +++ b/gradle/eclipse-java-consensys-style.xml @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/greclipse-gradle-consensys-style.properties b/gradle/greclipse-gradle-consensys-style.properties new file mode 100644 index 00000000..163a2e52 --- /dev/null +++ b/gradle/greclipse-gradle-consensys-style.properties @@ -0,0 +1,51 @@ +#Whether to use 'space', 'tab' or 'mixed' (both) characters for indentation. +#The default value is 'tab'. +org.eclipse.jdt.core.formatter.tabulation.char=space + +#Number of spaces used for indentation in case 'space' characters +#have been selected. The default value is 4. +org.eclipse.jdt.core.formatter.tabulation.size=2 + +#Number of spaces used for indentation in case 'mixed' characters +#have been selected. The default value is 4. +org.eclipse.jdt.core.formatter.indentation.size=1 + +#Whether or not indentation characters are inserted into empty lines. +#The default value is 'true'. +org.eclipse.jdt.core.formatter.indent_empty_lines=false + +#Number of spaces used for multiline indentation. +#The default value is 2. +groovy.formatter.multiline.indentation=1 + +#Length after which list are considered too long. These will be wrapped. +#The default value is 30. +groovy.formatter.longListLength=30 + +#Whether opening braces position shall be the next line. +#The default value is 'same'. +groovy.formatter.braces.start=same + +#Whether closing braces position shall be the next line. +#The default value is 'next'. +groovy.formatter.braces.end=next + +#Remove unnecessary semicolons. The default value is 'false'. +groovy.formatter.remove.unnecessary.semicolons=false + +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line + +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert diff --git a/gradle/license-header.txt b/gradle/license-header.txt new file mode 100644 index 00000000..893267fb --- /dev/null +++ b/gradle/license-header.txt @@ -0,0 +1,15 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..91ca28c8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..d2c45a4b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/io/build.gradle b/io/build.gradle new file mode 100644 index 00000000..158095ca --- /dev/null +++ b/io/build.gradle @@ -0,0 +1,12 @@ +description = 'Classes and utilities for handling file and network IO.' + +dependencies { + compileOnly project(':units') + compile 'com.google.guava:guava' + + testCompile project(':bytes') + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/io/src/main/java/net/consensys/cava/io/Base64.java b/io/src/main/java/net/consensys/cava/io/Base64.java new file mode 100644 index 00000000..915c8d01 --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/Base64.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; + +/** + * Utility methods for encoding and decoding base64 strings. + */ +public final class Base64 { + private Base64() {} + + /** + * Encode a byte array to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encodeBytes(byte[] bytes) { + requireNonNull(bytes); + return new String(java.util.Base64.getEncoder().encode(bytes), UTF_8); + } + + /** + * Encode bytes to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encode(Bytes bytes) { + requireNonNull(bytes); + return encodeBytes(bytes.toArrayUnsafe()); + } + + /** + * Decode a base64 encoded string to a byte array. + * + * @param b64 The base64 encoded string. + * @return A byte array. + */ + public static byte[] decodeBytes(String b64) { + requireNonNull(b64); + return java.util.Base64.getDecoder().decode(b64.getBytes(UTF_8)); + } + + /** + * Decode a base64 encoded string to bytes. + * + * @param b64 The base64 encoded string. + * @return The decoded bytes. + */ + public static Bytes decode(String b64) { + return Bytes.wrap(decodeBytes(b64)); + } +} diff --git a/io/src/main/java/net/consensys/cava/io/IOConsumer.java b/io/src/main/java/net/consensys/cava/io/IOConsumer.java new file mode 100644 index 00000000..e5a3e79a --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/IOConsumer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import java.io.IOException; + +/** + * Represents an operation that accepts a single input argument and returns no result. + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @throws IOException If an IO error occurs. + */ + void accept(T t) throws IOException; +} diff --git a/io/src/main/java/net/consensys/cava/io/NullOutputStream.java b/io/src/main/java/net/consensys/cava/io/NullOutputStream.java new file mode 100644 index 00000000..e42434e5 --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/NullOutputStream.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import java.io.OutputStream; + +final class NullOutputStream extends OutputStream { + static final NullOutputStream INSTANCE = new NullOutputStream(); + + @Override + public void write(int b) { + // do nothing + } +} diff --git a/io/src/main/java/net/consensys/cava/io/Streams.java b/io/src/main/java/net/consensys/cava/io/Streams.java new file mode 100644 index 00000000..48e86b92 --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/Streams.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import static java.util.Objects.requireNonNull; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Enumeration; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Utilities for working with streams. + */ +public final class Streams { + private Streams() {} + + private static final PrintStream NULL_PRINT_STREAM = new PrintStream(NullOutputStream.INSTANCE); + + /** + * @return An {@link OutputStream} that discards all input. + */ + public static OutputStream nullOutputStream() { + return NullOutputStream.INSTANCE; + } + + /** + * @return A {@link PrintStream} that discards all input. + */ + public static PrintStream nullPrintStream() { + return NULL_PRINT_STREAM; + } + + /** + * Stream an {@link Enumeration}. + * + * @param enumeration The enumeration. + * @param The type of objects in the enumeration. + * @return A stream over the enumeration. + */ + public static Stream enumerationStream(Enumeration enumeration) { + requireNonNull(enumeration); + return StreamSupport.stream(new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) { + @Override + public boolean tryAdvance(Consumer action) { + if (enumeration.hasMoreElements()) { + action.accept(enumeration.nextElement()); + return true; + } + return false; + } + + @Override + public void forEachRemaining(Consumer action) { + while (enumeration.hasMoreElements()) { + action.accept(enumeration.nextElement()); + } + } + }, false); + } +} diff --git a/io/src/main/java/net/consensys/cava/io/file/Files.java b/io/src/main/java/net/consensys/cava/io/file/Files.java new file mode 100644 index 00000000..eb6a606b --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/file/Files.java @@ -0,0 +1,196 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io.file; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.file.Files.delete; +import static java.nio.file.Files.walkFileTree; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.io.IOConsumer; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; + +import com.google.common.base.Charsets; + +/** + * Utility methods for working with files. + */ +public final class Files { + private Files() {} + + /** + * Create a file, if it does not already exist. + * + * @param path The path to the file to create. + * @param attrs An optional list of file attributes to set atomically when creating the file. + * @return true if the file was created. + * @throws IOException If an I/O error occurs or the parent directory does not exist. + */ + public static boolean createFileIfMissing(Path path, FileAttribute... attrs) throws IOException { + requireNonNull(path); + try { + java.nio.file.Files.createFile(path, attrs); + } catch (FileAlreadyExistsException e) { + return false; + } + return true; + } + + /** + * Delete a directory and all files contained within it. + * + * @param directory The directory to delete. + * @throws IOException If an I/O error occurs. + */ + public static void deleteRecursively(Path directory) throws IOException { + checkNotNull(directory); + + walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Copies all characters from a file to an writer. + * + * @param source The source file. + * @param out The output writer. + * @return The total characters written. + * @throws IOException If an I/O error occurs. + */ + public static long copy(Path source, Writer out) throws IOException { + return copy(source, out, Charsets.UTF_8); + } + + /** + * Copies all characters from a file to an writer. + * + * @param source The source file. + * @param out The output writer. + * @param charset The charset of the source file. + * @return The total characters written. + * @throws IOException If an I/O error occurs. + */ + public static long copy(Path source, Writer out, Charset charset) throws IOException { + requireNonNull(source); + requireNonNull(out); + requireNonNull(charset); + + try (Reader in = java.nio.file.Files.newBufferedReader(source, charset)) { + long total = 0L; + char[] buf = new char[4096]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + total += n; + } + return total; + } + } + + /** + * Write a temporary file and then replace target. + * + * @param path The target file to be replaced (if it exists). + * @param bytes The bytes to be written. + * @throws IOException If an I/O error occurs. + */ + public static void atomicReplace(Path path, byte[] bytes) throws IOException { + requireNonNull(bytes); + Path directory = path.getParent(); + java.nio.file.Files.createDirectories(directory); + Path tempFile = java.nio.file.Files.createTempFile(directory, "." + path.getName(0), ".tmp"); + try { + java.nio.file.Files.write(tempFile, bytes); + java.nio.file.Files.move(tempFile, path, REPLACE_EXISTING, ATOMIC_MOVE); + } catch (Throwable e) { + try { + java.nio.file.Files.delete(tempFile); + } catch (IOException e2) { + e.addSuppressed(e2); + } + throw e; + } + } + + /** + * Write a temporary file and then replace target. + * + * @param path The target file to be replaced (if it exists). + * @param fn A consumer that will be provided a buffered {@link Writer} instance that will write to the file. + * @throws IOException If an I/O error occurs. + */ + public static void atomicReplace(Path path, IOConsumer fn) throws IOException { + atomicReplace(path, Charsets.UTF_8, fn); + } + + /** + * Write a temporary file and then replace target. + * + * @param path The target file to be replaced (if it exists). + * @param charset The charset of the file. + * @param fn A consumer that will be provided a buffered {@link Writer} instance that will write to the file. + * @throws IOException If an I/O error occurs. + */ + public static void atomicReplace(Path path, Charset charset, IOConsumer fn) throws IOException { + requireNonNull(charset); + requireNonNull(fn); + Path directory = path.getParent(); + java.nio.file.Files.createDirectories(directory); + Path tempFile = java.nio.file.Files.createTempFile(directory, "." + path.getName(0), ".tmp"); + Writer writer = null; + try { + writer = java.nio.file.Files.newBufferedWriter(tempFile, charset); + fn.accept(writer); + writer.flush(); + writer.close(); + java.nio.file.Files.move(tempFile, path, REPLACE_EXISTING, ATOMIC_MOVE); + } catch (Throwable e) { + if (writer != null) { + try { + writer.close(); + } catch (IOException e2) { + e.addSuppressed(e2); + } + } + try { + java.nio.file.Files.delete(tempFile); + } catch (IOException e2) { + e.addSuppressed(e2); + } + throw e; + } + } +} diff --git a/io/src/main/java/net/consensys/cava/io/file/package-info.java b/io/src/main/java/net/consensys/cava/io/file/package-info.java new file mode 100644 index 00000000..dff43ecc --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/file/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes and utilities for handling file IO. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-io' (cava-io.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.io.file; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/io/src/main/java/net/consensys/cava/io/package-info.java b/io/src/main/java/net/consensys/cava/io/package-info.java new file mode 100644 index 00000000..66e53469 --- /dev/null +++ b/io/src/main/java/net/consensys/cava/io/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes and utilities for handling file and network IO. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-io' (cava-io.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.io; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/io/src/test/java/net/consensys/cava/io/Base64Test.java b/io/src/test/java/net/consensys/cava/io/Base64Test.java new file mode 100644 index 00000000..58912735 --- /dev/null +++ b/io/src/test/java/net/consensys/cava/io/Base64Test.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class Base64Test { + + @Test + void shouldEncodeByteArray() { + String s = Base64.encodeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldEncodeBytesValue() { + String s = Base64.encode(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8)); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldDecodeToByteArray() { + byte[] bytes = Base64.decodeBytes("AQIDBAUGBwg="); + assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, bytes); + } + + @Test + void shouldDecodeToBytesValue() { + Bytes bytes = Base64.decode("AQIDBAUGBwg="); + assertEquals(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8), bytes); + } +} diff --git a/io/src/test/java/net/consensys/cava/io/StreamsTest.java b/io/src/test/java/net/consensys/cava/io/StreamsTest.java new file mode 100644 index 00000000..27185e65 --- /dev/null +++ b/io/src/test/java/net/consensys/cava/io/StreamsTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io; + +import static net.consensys.cava.io.Streams.enumerationStream; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +class StreamsTest { + + @Test + void shouldStreamAnEnumeration() { + Enumeration enumeration = Collections.enumeration(Arrays.asList("RED", "BLUE", "GREEN")); + List result = enumerationStream(enumeration).map(String::toLowerCase).collect(Collectors.toList()); + assertEquals(Arrays.asList("red", "blue", "green"), result); + } +} diff --git a/io/src/test/java/net/consensys/cava/io/file/FilesTest.java b/io/src/test/java/net/consensys/cava/io/file/FilesTest.java new file mode 100644 index 00000000..3638b4c4 --- /dev/null +++ b/io/src/test/java/net/consensys/cava/io/file/FilesTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.io.file; + +import static net.consensys.cava.io.file.Files.deleteRecursively; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class FilesTest { + + @Test + void deleteRecursivelyShouldDeleteEverything() throws Exception { + Path directory = Files.createTempDirectory(this.getClass().getSimpleName()); + Path testData = directory.resolve("test_data"); + Files.createFile(testData); + + Path testDir = directory.resolve("test_dir"); + Files.createDirectory(testDir); + Path testData2 = testDir.resolve("test_data"); + Files.createFile(testData2); + + assertTrue(Files.exists(directory)); + assertTrue(Files.exists(testData)); + assertTrue(Files.exists(testDir)); + assertTrue(Files.exists(testData2)); + + deleteRecursively(directory); + + assertFalse(Files.exists(directory)); + assertFalse(Files.exists(testData)); + assertFalse(Files.exists(testDir)); + assertFalse(Files.exists(testData2)); + } +} diff --git a/junit/build.gradle b/junit/build.gradle new file mode 100644 index 00000000..9359e021 --- /dev/null +++ b/junit/build.gradle @@ -0,0 +1,13 @@ +description = 'Utilities for better junit testing.' + +dependencies { + compile project(':io') + compileOnly 'io.vertx:vertx-core' + compileOnly 'org.bouncycastle:bcprov-jdk15on' + compileOnly 'org.junit.jupiter:junit-jupiter-api' + + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/junit/src/main/java/net/consensys/cava/junit/BouncyCastleExtension.java b/junit/src/main/java/net/consensys/cava/junit/BouncyCastleExtension.java new file mode 100644 index 00000000..cd686264 --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/BouncyCastleExtension.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * A junit5 extension, that installs a BouncyCastle security provider. + */ +public class BouncyCastleExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + } +} diff --git a/junit/src/main/java/net/consensys/cava/junit/TempDirectory.java b/junit/src/main/java/net/consensys/cava/junit/TempDirectory.java new file mode 100644 index 00000000..a648a46f --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/TempDirectory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A parameter annotation for injecting a temporary directory into junit5 tests. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface TempDirectory { +} diff --git a/junit/src/main/java/net/consensys/cava/junit/TempDirectoryExtension.java b/junit/src/main/java/net/consensys/cava/junit/TempDirectoryExtension.java new file mode 100644 index 00000000..4d3758a1 --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/TempDirectoryExtension.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import static java.nio.file.Files.createTempDirectory; +import static net.consensys.cava.io.file.Files.deleteRecursively; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * A junit5 extension, that provides a temporary directory for tests. + * + * The temporary directory is created for the test suite and injected into any tests with parameters annotated by + * {@link TempDirectory}. + */ +public final class TempDirectoryExtension implements ParameterResolver, AfterAllCallback { + + private Path tempDirectory; + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().isAnnotationPresent(TempDirectory.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + if (tempDirectory == null) { + try { + tempDirectory = createTempDirectory(extensionContext.getRequiredTestClass().getSimpleName()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return tempDirectory; + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + if (tempDirectory != null) { + deleteRecursively(tempDirectory); + } + } +} diff --git a/junit/src/main/java/net/consensys/cava/junit/VertxExtension.java b/junit/src/main/java/net/consensys/cava/junit/VertxExtension.java new file mode 100644 index 00000000..f1f7b3e9 --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/VertxExtension.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import io.vertx.core.Vertx; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * A junit5 extension, that provides a Vert.X instance for tests. + * + * The Vert.X instance created for the test suite and injected into any tests with parameters annotated by + * {@link VertxInstance}. + */ +public class VertxExtension implements ParameterResolver, AfterAllCallback { + + private Vertx vertx = null; + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().isAnnotationPresent(VertxInstance.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + if (vertx == null) { + System.setProperty("vertx.disableFileCPResolving", "true"); + vertx = Vertx.vertx(); + } + return vertx; + } + + @Override + public void afterAll(ExtensionContext context) { + if (vertx != null) { + vertx.close(); + } + } +} diff --git a/junit/src/main/java/net/consensys/cava/junit/VertxInstance.java b/junit/src/main/java/net/consensys/cava/junit/VertxInstance.java new file mode 100644 index 00000000..ad049885 --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/VertxInstance.java @@ -0,0 +1,26 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A parameter annotation for injecting a temporary Vert.X instance into junit5 tests. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface VertxInstance { +} diff --git a/junit/src/main/java/net/consensys/cava/junit/package-info.java b/junit/src/main/java/net/consensys/cava/junit/package-info.java new file mode 100644 index 00000000..240f51f2 --- /dev/null +++ b/junit/src/main/java/net/consensys/cava/junit/package-info.java @@ -0,0 +1,8 @@ +/** + * Utilities for better junit testing. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-junit' (cava-junit.jar). + */ +package net.consensys.cava.junit; diff --git a/junit/src/test/java/net/consensys/cava/junit/TempDirectoryExtensionTest.java b/junit/src/test/java/net/consensys/cava/junit/TempDirectoryExtensionTest.java new file mode 100644 index 00000000..24b475d1 --- /dev/null +++ b/junit/src/test/java/net/consensys/cava/junit/TempDirectoryExtensionTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.junit; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +class TempDirectoryExtensionTest { + + @Test + void shouldHaveAccessToATemporaryDirectory(@TempDirectory Path tempDir) throws Exception { + assertTrue(Files.exists(tempDir)); + assertTrue(Files.isDirectory(tempDir)); + Files.createFile(tempDir.resolve("file")); + } +} diff --git a/kv/build.gradle b/kv/build.gradle new file mode 100644 index 00000000..2f53013e --- /dev/null +++ b/kv/build.gradle @@ -0,0 +1,19 @@ +description = 'Key value store implementations.' + +dependencies { + compile project(':bytes') + compileOnly project(':concurrent') + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + + compileOnly 'org.fusesource.leveldbjni:leveldbjni-all' + + testCompile project(':concurrent') + testCompile 'com.winterbe:expekt' + testCompile 'org.fusesource.leveldbjni:leveldbjni-all' + testCompile 'org.jetbrains.spek:spek-api' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.jetbrains.spek:spek-junit-platform-engine' + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/kv/src/main/kotlin/net/consensys/cava/kv/KeyValueStore.kt b/kv/src/main/kotlin/net/consensys/cava/kv/KeyValueStore.kt new file mode 100644 index 00000000..46da1c34 --- /dev/null +++ b/kv/src/main/kotlin/net/consensys/cava/kv/KeyValueStore.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.kv + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.concurrent.AsyncCompletion +import net.consensys.cava.concurrent.AsyncResult +import net.consensys.cava.concurrent.coroutines.experimental.asyncCompletion +import net.consensys.cava.concurrent.coroutines.experimental.asyncResult +import java.util.Optional + +/** + * A key-value store. + */ +interface KeyValueStore { + + /** + * Retrieves data from the store. + * + * @param key The key for the content. + * @return The stored data, or null if no data was stored under the specified key. + */ + suspend fun get(key: Bytes): Bytes? + + /** + * Retrieves data from the store. + * + * @param key The key for the content. + * @return An [AsyncResult] that will complete with the stored content, + * or an empty optional if no content was available. + */ + fun getAsync(key: Bytes): AsyncResult> = asyncResult { Optional.ofNullable(get(key)) } + + /** + * Puts data into the store. + * + * @param key The key to associate with the data, for use when retrieving. + * @param value the data to store. + */ + suspend fun put(key: Bytes, value: Bytes) + + /** + * Puts data into the store. + * + * Note: if the storage implementation already contains content for the given key, it does not need to replace the + * existing content. + * + * @param key The key to associate with the data, for use when retrieving. + * @param value the data to store. + * @return An [AsyncCompletion] that will complete when the content is stored. + */ + fun putAsync(key: Bytes, value: Bytes): AsyncCompletion = asyncCompletion { put(key, value) } +} diff --git a/kv/src/main/kotlin/net/consensys/cava/kv/LevelDBKeyValueStore.kt b/kv/src/main/kotlin/net/consensys/cava/kv/LevelDBKeyValueStore.kt new file mode 100644 index 00000000..bc3ff4bd --- /dev/null +++ b/kv/src/main/kotlin/net/consensys/cava/kv/LevelDBKeyValueStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.kv + +import kotlinx.coroutines.experimental.Unconfined +import kotlinx.coroutines.experimental.withContext +import net.consensys.cava.bytes.Bytes +import org.iq80.leveldb.DB + +/** + * A key-value store backed by LevelDB. + */ +class LevelDBKeyValueStore(private val levelDB: DB) : KeyValueStore { + + override suspend fun get(key: Bytes): Bytes? = withContext(Unconfined) { + val rawValue = levelDB[key.toArrayUnsafe()] + if (rawValue == null) { + null + } else { + Bytes.wrap(rawValue) + } + } + + override suspend fun put(key: Bytes, value: Bytes) = withContext(Unconfined) { + levelDB.put(key.toArrayUnsafe(), value.toArrayUnsafe()) + } +} diff --git a/kv/src/main/kotlin/net/consensys/cava/kv/MapKeyValueStore.kt b/kv/src/main/kotlin/net/consensys/cava/kv/MapKeyValueStore.kt new file mode 100644 index 00000000..39969a4b --- /dev/null +++ b/kv/src/main/kotlin/net/consensys/cava/kv/MapKeyValueStore.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.kv + +import net.consensys.cava.bytes.Bytes + +/** + * A key-value store backed by an in-memory Map. + */ +class MapKeyValueStore(private val map: MutableMap) : KeyValueStore { + + override suspend fun get(key: Bytes): Bytes? = map[key] + + override suspend fun put(key: Bytes, value: Bytes) { + map[key] = value + } +} diff --git a/kv/src/test/java/net/consensys/cava/kv/KeyValueStoreTest.java b/kv/src/test/java/net/consensys/cava/kv/KeyValueStoreTest.java new file mode 100644 index 00000000..5cb54fe4 --- /dev/null +++ b/kv/src/test/java/net/consensys/cava/kv/KeyValueStoreTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.kv; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.concurrent.AsyncCompletion; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class KeyValueStoreTest { + + @Test + void testPutAndGet() throws Exception { + Map map = new HashMap<>(); + KeyValueStore store = new MapKeyValueStore(map); + AsyncCompletion completion = store.putAsync(Bytes.of(123), Bytes.of(10, 12, 13)); + completion.join(); + Optional value = store.getAsync(Bytes.of(123)).get(); + assertTrue(value.isPresent()); + assertEquals(Bytes.of(10, 12, 13), value.get()); + assertEquals(Bytes.of(10, 12, 13), map.get(Bytes.of(123))); + } + + @Test + void testNoValue() throws Exception { + Map map = new HashMap<>(); + KeyValueStore store = new MapKeyValueStore(map); + assertFalse(store.getAsync(Bytes.of(123)).get().isPresent()); + } +} diff --git a/kv/src/test/kotlin/net/consensys/cava/kv/KeyValueStoreSpec.kt b/kv/src/test/kotlin/net/consensys/cava/kv/KeyValueStoreSpec.kt new file mode 100644 index 00000000..08717cfb --- /dev/null +++ b/kv/src/test/kotlin/net/consensys/cava/kv/KeyValueStoreSpec.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.kv + +import com.google.common.io.MoreFiles +import com.google.common.io.RecursiveDeleteOption +import com.winterbe.expekt.should +import kotlinx.coroutines.experimental.runBlocking +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.kv.Vars.foo +import net.consensys.cava.kv.Vars.foobar +import org.fusesource.leveldbjni.JniDBFactory +import org.iq80.leveldb.Options +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import java.nio.file.Files + +object Vars { + val foo = Bytes.wrap("foo".toByteArray())!! + val foobar = Bytes.wrap("foobar".toByteArray())!! +} + +object KeyValueStoreSpec : Spek({ + val backingMap = mutableMapOf() + val kv = MapKeyValueStore(backingMap) + + describe("a map-backed key value store") { + + it("should allow to store values") { + runBlocking { + kv.put(foo, foo) + backingMap.get(foo).should.equal(foo) + } + } + + it("should allow to retrieve values") { + runBlocking { + kv.put(foobar, foo) + kv.get(foobar).should.equal(foo) + } + } + + it("should return an empty optional when no value is present") { + runBlocking { + kv.get(Bytes.wrap("foofoobar".toByteArray())).should.be.`null` + } + } + } +}) + +object LevelDBKeyValueStoreSpec : Spek({ + val path = Files.createTempDirectory("leveldb") + val db = JniDBFactory.factory.open(path.toFile(), Options().createIfMissing(true)) + val kv = LevelDBKeyValueStore(db) + afterGroup { + db.close() + MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE) + } + describe("a levelDB-backed key value store") { + + it("should allow to store values") { + runBlocking { + kv.put(foo, foo) + Bytes.wrap(db.get(foo.toArrayUnsafe())).should.equal(foo) + } + } + + it("should allow to retrieve values") { + runBlocking { + kv.put(foobar, foo) + kv.get(foobar).should.equal(foo) + } + } + + it("should return an empty optional when no value is present") { + runBlocking { + kv.get(Bytes.wrap("foofoobar".toByteArray())).should.be.`null` + } + } + } +}) diff --git a/merkle-trie/build.gradle b/merkle-trie/build.gradle new file mode 100644 index 00000000..44b3dee9 --- /dev/null +++ b/merkle-trie/build.gradle @@ -0,0 +1,17 @@ +description = 'Patricia Merkle Trie implementations.' + +dependencies { + compile project(':bytes') + compile project(':concurrent') + compile project(':crypto') + compile project(':rlp') + compile 'com.google.guava:guava' + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + + testCompile project(':junit') + testCompile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/merkle-trie/src/main/java/net/consensys/cava/trie/CompactEncoding.java b/merkle-trie/src/main/java/net/consensys/cava/trie/CompactEncoding.java new file mode 100644 index 00000000..8d2ccb6c --- /dev/null +++ b/merkle-trie/src/main/java/net/consensys/cava/trie/CompactEncoding.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.MutableBytes; + +/** + * Compact (Hex-prefix) encoding and decoding. + * + *

+ * An implementation of Compact + * (Hex-prefix) encoding. + */ +public final class CompactEncoding { + private CompactEncoding() {} + + public static final byte LEAF_TERMINATOR = 0x10; + + /** + * Calculate a RADIX-16 path for a given byte sequence. + * + * @param bytes The byte sequence to calculate the path for. + * @return The Radix-16 path. + */ + public static Bytes bytesToPath(Bytes bytes) { + MutableBytes path = MutableBytes.create(bytes.size() * 2 + 1); + int j = 0; + for (int i = 0; i < bytes.size(); i += 1, j += 2) { + byte b = bytes.get(i); + path.set(j, (byte) ((b >>> 4) & 0x0f)); + path.set(j + 1, (byte) (b & 0x0f)); + } + path.set(j, LEAF_TERMINATOR); + return path; + } + + /** + * Encode a Radix-16 path. + * + * @param path A Radix-16 path. + * @return A compact-encoded path. + */ + public static Bytes encode(Bytes path) { + int size = path.size(); + boolean isLeaf = size > 0 && path.get(size - 1) == LEAF_TERMINATOR; + if (isLeaf) { + size = size - 1; + } + + MutableBytes encoded = MutableBytes.create((size + 2) / 2); + int i = 0; + int j = 0; + + if (size % 2 == 1) { + // add first nibble to magic + byte high = (byte) (isLeaf ? 0x03 : 0x01); + byte low = path.get(i++); + if ((low & 0xf0) != 0) { + throw new IllegalArgumentException("Invalid path: contains elements larger than a nibble"); + } + encoded.set(j++, (byte) (high << 4 | low)); + } else { + byte high = (byte) (isLeaf ? 0x02 : 0x00); + encoded.set(j++, (byte) (high << 4)); + } + + while (i < size) { + byte high = path.get(i++); + byte low = path.get(i++); + if ((high & 0xf0) != 0 || (low & 0xf0) != 0) { + throw new IllegalArgumentException("Invalid path: contains elements larger than a nibble"); + } + encoded.set(j++, (byte) (high << 4 | low)); + } + + return encoded; + } + + /** + * Decode a compact-encoded path to Radix-16. + * + * @param encoded A compact-encoded path. + * @return A Radix-16 path. + */ + public static Bytes decode(Bytes encoded) { + int size = encoded.size(); + checkArgument(size > 0); + byte magic = encoded.get(0); + checkArgument((magic & 0xc0) == 0, "Invalid compact encoding"); + + boolean isLeaf = (magic & 0x20) != 0; + + int pathLength = ((size - 1) * 2) + (isLeaf ? 1 : 0); + MutableBytes path; + int i = 0; + + if ((magic & 0x10) != 0) { + // need to use lower nibble of magic + path = MutableBytes.create(pathLength + 1); + path.set(i++, (byte) (magic & 0x0f)); + } else { + path = MutableBytes.create(pathLength); + } + + for (int j = 1; j < size; j++) { + byte b = encoded.get(j); + path.set(i++, (byte) ((b >>> 4) & 0x0f)); + path.set(i++, (byte) (b & 0x0f)); + } + + if (isLeaf) { + path.set(i, LEAF_TERMINATOR); + } + + return path; + } +} diff --git a/merkle-trie/src/main/java/net/consensys/cava/trie/package-info.java b/merkle-trie/src/main/java/net/consensys/cava/trie/package-info.java new file mode 100644 index 00000000..ad898e4d --- /dev/null +++ b/merkle-trie/src/main/java/net/consensys/cava/trie/package-info.java @@ -0,0 +1,14 @@ +/** + * 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 standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-merkle-trie' (cava-merkle-trie.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.trie; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/BranchNode.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/BranchNode.kt new file mode 100644 index 00000000..bdbf3115 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/BranchNode.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.bytes.MutableBytes +import net.consensys.cava.crypto.Hash.keccak256 +import net.consensys.cava.rlp.RLP +import net.consensys.cava.trie.CompactEncoding +import java.lang.ref.SoftReference +import java.lang.ref.WeakReference + +private val NULL_NODE: NullNode<*> = NullNode.instance() + +internal class BranchNode( + private val children: List>, + private val value: V?, + private val nodeFactory: NodeFactory, + private val valueSerializer: (V) -> Bytes +) : Node { + + companion object { + const val RADIX = CompactEncoding.LEAF_TERMINATOR.toInt() + } + + private var rlp: WeakReference? = null + private var hash: SoftReference? = null + + init { + assert(children.size == RADIX) + } + + override suspend fun accept(visitor: NodeVisitor, path: Bytes): Node = visitor.visit(this, path) + + override suspend fun path(): Bytes = Bytes.EMPTY + + override suspend fun value(): V? = value + + fun child(index: Byte): Node = children[index.toInt()] + + override fun rlp(): Bytes { + val prevEncoded = rlp?.get() + if (prevEncoded != null) { + return prevEncoded + } + val encoded = RLP.encodeList { out -> + for (i in 0 until RADIX) { + out.writeRLP(children[i].rlpRef()) + } + if (value != null) { + out.writeValue(valueSerializer(value)) + } else { + out.writeValue(Bytes.EMPTY) + } + } + rlp = WeakReference(encoded) + return encoded + } + + override fun rlpRef(): Bytes { + val rlp = rlp() + return if (rlp.size() < 32) rlp else RLP.encodeValue(hash()) + } + + override fun hash(): Bytes32 { + val prevHashed = hash?.get() + if (prevHashed != null) { + return prevHashed + } + val hashed = keccak256(rlp()) + hash = SoftReference(hashed) + return hashed + } + + override suspend fun replacePath(path: Bytes): Node = nodeFactory.createExtension(path, this) + + suspend fun replaceChild(index: Byte, updatedChild: Node): Node { + val newChildren = ArrayList(children) + newChildren[index.toInt()] = updatedChild + + if (updatedChild === NULL_NODE) { + if (value != null && !hasChildren()) { + return nodeFactory.createLeaf(Bytes.of(index), value) + } else if (value == null) { + val flattened = maybeFlatten(newChildren) + if (flattened != null) { + return flattened + } + } + } + + return nodeFactory.createBranch(newChildren, value) + } + + suspend fun replaceValue(value: V): Node = nodeFactory.createBranch(children, value) + + suspend fun removeValue(): Node = maybeFlatten(children) ?: nodeFactory.createBranch(children, null) + + private fun hasChildren(): Boolean { + for (child in children) { + if (child !== NULL_NODE) { + return true + } + } + return false + } +} + +private suspend fun maybeFlatten(children: List>): Node? { + val onlyChildIndex = findOnlyChild(children) + if (onlyChildIndex < 0) { + return null + } + + val onlyChild = children[onlyChildIndex] + + // replace the path of the only child and return it + val onlyChildPath = onlyChild.path() + val completePath = MutableBytes.create(1 + onlyChildPath.size()) + completePath.set(0, onlyChildIndex.toByte()) + onlyChildPath.copyTo(completePath, 1) + return onlyChild.replacePath(completePath) +} + +private fun findOnlyChild(children: List>): Int { + var onlyChildIndex = -1 + assert(children.size == BranchNode.RADIX.toInt()) + for (i in 0 until BranchNode.RADIX) { + if (children[i] !== NULL_NODE) { + if (onlyChildIndex >= 0) { + return -1 + } + onlyChildIndex = i + } + } + return onlyChildIndex +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/DefaultNodeFactory.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/DefaultNodeFactory.kt new file mode 100644 index 00000000..6fb92dfc --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/DefaultNodeFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import java.util.Collections + +internal class DefaultNodeFactory(private val valueSerializer: (V) -> Bytes) : NodeFactory { + + private val nullNode: NullNode = NullNode.instance() + + override suspend fun createExtension(path: Bytes, child: Node): Node = ExtensionNode(path, child, this) + + override suspend fun createBranch(leftIndex: Byte, left: Node, rightIndex: Byte, right: Node): Node { + assert(leftIndex <= BranchNode.RADIX) + assert(rightIndex <= BranchNode.RADIX) + assert(leftIndex != rightIndex) + + val children: MutableList> = Collections.nCopies(BranchNode.RADIX, nullNode).toMutableList() + return when { + leftIndex.toInt() == BranchNode.RADIX -> { + children[rightIndex.toInt()] = right + createBranch(children, left.value()) + } + rightIndex.toInt() == BranchNode.RADIX -> { + children[leftIndex.toInt()] = left + createBranch(children, right.value()) + } + else -> { + children[leftIndex.toInt()] = left + children[rightIndex.toInt()] = right + createBranch(children, null) + } + } + } + + override suspend fun createBranch(newChildren: List>, value: V?): Node { + return BranchNode(newChildren, value, this, valueSerializer) + } + + override suspend fun createLeaf(path: Bytes, value: V): Node { + return LeafNode(path, value, this, valueSerializer) + } +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/ExtensionNode.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/ExtensionNode.kt new file mode 100644 index 00000000..bb5ad61e --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/ExtensionNode.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.crypto.Hash.keccak256 +import net.consensys.cava.rlp.RLP +import net.consensys.cava.trie.CompactEncoding +import java.lang.ref.SoftReference +import java.lang.ref.WeakReference + +internal class ExtensionNode( + private val path: Bytes, + private val child: Node, + private val nodeFactory: NodeFactory +) : Node { + private var rlp: WeakReference? = null + private var hash: SoftReference? = null + + init { + assert(path.size() > 0) + assert(path.get(path.size() - 1) != CompactEncoding.LEAF_TERMINATOR) { "Extension path ends in a leaf terminator" } + } + + override suspend fun accept(visitor: NodeVisitor, path: Bytes): Node = visitor.visit(this, path) + + override suspend fun path(): Bytes = path + + override suspend fun value(): V? = throw UnsupportedOperationException() + + fun child(): Node = child + + override fun rlp(): Bytes { + val prevEncoded = rlp?.get() + if (prevEncoded != null) { + return prevEncoded + } + val encoded = RLP.encodeList { writer -> + writer.writeValue(CompactEncoding.encode(path)) + writer.writeRLP(child.rlpRef()) + } + rlp = WeakReference(encoded) + return encoded + } + + override fun rlpRef(): Bytes { + val rlp = rlp() + return if (rlp.size() < 32) rlp else RLP.encodeValue(hash()) + } + + override fun hash(): Bytes32 { + val prevHashed = hash?.get() + if (prevHashed != null) { + return prevHashed + } + val rlp = rlp() + val hashed = keccak256(rlp) + hash = SoftReference(hashed) + return hashed + } + + suspend fun replaceChild(updatedChild: Node): Node { + // collapse this extension - if the child is a branch, it will create a new extension + val childPath = updatedChild.path() + return updatedChild.replacePath(Bytes.concatenate(path, childPath)) + } + + override suspend fun replacePath(path: Bytes): Node { + return if (path.size() == 0) child else nodeFactory.createExtension(path, child) + } +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/GetVisitor.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/GetVisitor.kt new file mode 100644 index 00000000..8b23e096 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/GetVisitor.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.trie.CompactEncoding + +internal class GetVisitor : NodeVisitor { + + override suspend fun visit(extensionNode: ExtensionNode, path: Bytes): Node { + val extensionPath = extensionNode.path() + val commonPathLength = extensionPath.commonPrefixLength(path) + assert(commonPathLength < path.size()) { "Visiting path doesn't end with a non-matching terminator" } + + if (commonPathLength < extensionPath.size()) { + // path diverges before the end of the extension, so it cannot match + return NullNode.instance() + } + + return extensionNode.child().accept(this, path.slice(commonPathLength)) + } + + override suspend fun visit(branchNode: BranchNode, path: Bytes): Node { + assert(path.size() > 0) { "Visiting path doesn't end with a non-matching terminator" } + + val childIndex = path.get(0) + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode + } + + return branchNode.child(childIndex).accept(this, path.slice(1)) + } + + override suspend fun visit(leafNode: LeafNode, path: Bytes): Node { + val leafPath = leafNode.path() + + if (leafPath.commonPrefixLength(path) != leafPath.size()) { + return NullNode.instance() + } + + return leafNode + } + + override suspend fun visit(nullNode: NullNode, path: Bytes): Node = NullNode.instance() +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/LeafNode.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/LeafNode.kt new file mode 100644 index 00000000..cf565b88 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/LeafNode.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.crypto.Hash.keccak256 +import net.consensys.cava.rlp.RLP +import net.consensys.cava.trie.CompactEncoding +import java.lang.ref.SoftReference +import java.lang.ref.WeakReference + +internal class LeafNode( + private val path: Bytes, + private val value: V, + private val nodeFactory: NodeFactory, + private val valueSerializer: (V) -> Bytes +) : Node { + private var rlp: WeakReference? = null + private var hash: SoftReference? = null + + override suspend fun accept(visitor: NodeVisitor, path: Bytes): Node = visitor.visit(this, path) + + override suspend fun path(): Bytes = path + + override suspend fun value(): V? = value + + override fun rlp(): Bytes { + val prevEncoded = rlp?.get() + if (prevEncoded != null) { + return prevEncoded + } + + val encoded = RLP.encodeList { writer -> + writer.writeValue(CompactEncoding.encode(path)) + writer.writeValue(valueSerializer(value)) + } + rlp = WeakReference(encoded) + return encoded + } + + override fun rlpRef(): Bytes { + val rlp = rlp() + return if (rlp.size() < 32) rlp else RLP.encodeValue(hash()) + } + + override fun hash(): Bytes32 { + val prevHashed = hash?.get() + if (prevHashed != null) { + return prevHashed + } + val hashed = keccak256(rlp()) + hash = SoftReference(hashed) + return hashed + } + + override suspend fun replacePath(path: Bytes): Node = nodeFactory.createLeaf(path, value) +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrie.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrie.kt new file mode 100644 index 00000000..b7202993 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrie.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.trie.CompactEncoding.bytesToPath +import java.util.function.Function + +internal fun bytesIdentity(b: Bytes): Bytes = b +internal fun stringSerializer(s: String): Bytes = Bytes.wrap(s.toByteArray(Charsets.UTF_8)) +internal fun stringDeserializer(b: Bytes): String = String(b.toArrayUnsafe(), Charsets.UTF_8) + +/** + * An in-memory [MerkleTrie]. + * + * @param The type of values stored by this trie. + * @param valueSerializer A function for serializing values to bytes. + * @constructor Creates an empty trie. + */ +class MerklePatriciaTrie(valueSerializer: (V) -> Bytes) : MerkleTrie { + + companion object { + /** + * Create a trie with keys and values of type [Bytes]. + */ + @JvmStatic + fun storingBytes(): MerklePatriciaTrie = MerklePatriciaTrie(::bytesIdentity) + + /** + * Create a trie with value of type [String]. + * + * Strings are stored in UTF-8 encoding. + */ + @JvmStatic + fun storingStrings(): MerklePatriciaTrie = MerklePatriciaTrie(::stringSerializer) + } + + private val getVisitor = GetVisitor() + private val removeVisitor = RemoveVisitor() + private val nodeFactory: DefaultNodeFactory = DefaultNodeFactory(valueSerializer) + private var root: Node = NullNode.instance() + + /** + * Creates an empty trie. + * + * @param valueSerializer A function for serializing values to bytes. + */ + constructor(valueSerializer: Function) : this(valueSerializer::apply) + + override suspend fun get(key: Bytes): V? = root.accept(getVisitor, bytesToPath(key)).value() + + override suspend fun put(key: Bytes, value: V?) { + if (value == null) { + return remove(key) + } + this.root = root.accept(PutVisitor(nodeFactory, value), bytesToPath(key)) + } + + override suspend fun remove(key: Bytes) { + this.root = root.accept(removeVisitor, bytesToPath(key)) + } + + override fun rootHash(): Bytes32 = root.hash() + + /** + * @return A string representation of the object. + */ + override fun toString(): String { + return javaClass.simpleName + "[" + rootHash() + "]" + } +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorage.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorage.kt new file mode 100644 index 00000000..dcd0734a --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorage.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.concurrent.AsyncCompletion +import net.consensys.cava.concurrent.AsyncResult +import net.consensys.cava.concurrent.coroutines.experimental.await +import java.util.Optional + +/** + * Storage for use in a [StoredMerklePatriciaTrie]. + */ +interface MerkleStorage { + + /** + * Get the stored content under the given hash. + * + * @param hash The hash for the content. + * @return The stored content. + */ + suspend fun get(hash: Bytes32): Bytes? + + /** + * Store content with a given hash. + * + * Note: if the storage implementation already contains content for the given hash, it does not need to replace the + * existing content. + * + * @param hash The hash for the content. + * @param content The content to store. + */ + suspend fun put(hash: Bytes32, content: Bytes) +} + +/** + * Storage for use in a [StoredMerklePatriciaTrie]. + * + * This abstract implementation of [MerkleStorage] provides variations of get/put methods that use + * [AsyncResult] and [AsyncCompletion] rather than kotlin coroutines, making it possible to implement in Java. + */ +abstract class AsyncMerkleStorage : MerkleStorage { + override suspend fun get(hash: Bytes32): Bytes? = getAsync(hash).await().orElse(null) + + /** + * Get the stored content under the given hash. + * + * @param hash The hash for the content. + * @return An [AsyncResult] that will complete with the stored content, or an exception. + */ + abstract fun getAsync(hash: Bytes32): AsyncResult> + + override suspend fun put(hash: Bytes32, content: Bytes) = putAsync(hash, content).await() + + /** + * Store content with a given hash. + * + * Note: if the storage implementation already contains content for the given hash, it does not need to replace the + * existing content. + * + * @param hash The hash for the content. + * @param content The content to store. + * @return An [AsyncCompletion] that will complete when the content is stored, or with an exception. + */ + abstract fun putAsync(hash: Bytes32, content: Bytes): AsyncCompletion +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorageException.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorageException.kt new file mode 100644 index 00000000..d149fd31 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleStorageException.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +/** + * This exception is thrown when there is an issue retrieving or decoding values from [MerkleStorage]. + */ +class MerkleStorageException : RuntimeException { + + /** + * Constructs a new exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message The detail message. + */ + constructor(message: String) : super(message) + + /** + * Constructs a new exception with the specified detail message and + * cause. + * + * @param message The detail message. + * @param cause The cause. + */ + constructor(message: String, cause: Exception) : super(message, cause) +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleTrie.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleTrie.kt new file mode 100644 index 00000000..eaebb204 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/MerkleTrie.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.concurrent.AsyncCompletion +import net.consensys.cava.concurrent.AsyncResult +import net.consensys.cava.concurrent.coroutines.experimental.asyncCompletion +import net.consensys.cava.concurrent.coroutines.experimental.asyncResult +import net.consensys.cava.crypto.Hash.keccak256 +import net.consensys.cava.rlp.RLP +import java.util.Optional + +/** + * A Merkle Trie. + */ +interface MerkleTrie { + + companion object { + /** + * The root hash of an empty tree. + */ + val EMPTY_TRIE_ROOT_HASH: Bytes32 = keccak256(RLP.encodeValue(Bytes.EMPTY)) + } + + /** + * Returns the value that corresponds to the specified key, or an empty byte array if no such value exists. + * + * @param key The key of the value to be returned. + * @return The value that corresponds to the specified key, or null if no such value exists. + * @throws MerkleStorageException If there is an error while accessing or decoding data from storage. + */ + suspend fun get(key: K): V? + + /** + * Returns the value that corresponds to the specified key, or an empty byte array if no such value exists. + * + * @param key The key of the value to be returned. + * @return An Optional containing the value that corresponds to the specified key, or an empty Optional if no such + * value exists. + */ + fun getAsync(key: K): AsyncResult> = asyncResult { Optional.ofNullable(get(key)) } + + /** + * Updates the value that corresponds to the specified key, creating the value if one does not already exist. + * + * If the value is null, deletes the value that corresponds to the specified key, if such a value exists. + * + * @param key The key that corresponds to the value to be updated. + * @param value The value to associate the key with. + * @throws MerkleStorageException If there is an error while writing to storage. + */ + suspend fun put(key: K, value: V?) + + /** + * Updates the value that corresponds to the specified key, creating the value if one does not already exist. + * + * If the value is null, deletes the value that corresponds to the specified key, if such a value exists. + * + * @param key The key that corresponds to the value to be updated. + * @param value The value to associate the key with. + * @return A completion that will complete when the value has been put into the trie. + */ + fun putAsync(key: K, value: V?): AsyncCompletion = asyncCompletion { put(key, value) } + + /** + * Deletes the value that corresponds to the specified key, if such a value exists. + * + * @param key The key of the value to be deleted. + * @throws MerkleStorageException If there is an error while writing to storage. + */ + suspend fun remove(key: K) + + /** + * Deletes the value that corresponds to the specified key, if such a value exists. + * + * @param key The key of the value to be deleted. + * @return A completion that will complete when the value has been removed. + */ + fun removeAsync(key: K): AsyncCompletion = asyncCompletion { remove(key) } + + /** + * Returns the KECCAK256 hash of the root node of the trie. + * + * @return The KECCAK256 hash of the root node of the trie. + */ + fun rootHash(): Bytes32 +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/Node.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/Node.kt new file mode 100644 index 00000000..9206c2df --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/Node.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 + +internal interface Node { + + suspend fun accept(visitor: NodeVisitor, path: Bytes): Node + + suspend fun path(): Bytes + + suspend fun value(): V? + + fun rlp(): Bytes + + fun rlpRef(): Bytes + + fun hash(): Bytes32 + + suspend fun replacePath(path: Bytes): Node +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeFactory.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeFactory.kt new file mode 100644 index 00000000..941392ea --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes + +internal interface NodeFactory { + + suspend fun createExtension(path: Bytes, child: Node): Node + + suspend fun createBranch(leftIndex: Byte, left: Node, rightIndex: Byte, right: Node): Node + + suspend fun createBranch(newChildren: List>, value: V?): Node + + suspend fun createLeaf(path: Bytes, value: V): Node +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeVisitor.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeVisitor.kt new file mode 100644 index 00000000..b57e5243 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NodeVisitor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes + +internal interface NodeVisitor { + + suspend fun visit(extensionNode: ExtensionNode, path: Bytes): Node + + suspend fun visit(branchNode: BranchNode, path: Bytes): Node + + suspend fun visit(leafNode: LeafNode, path: Bytes): Node + + suspend fun visit(nullNode: NullNode, path: Bytes): Node +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NullNode.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NullNode.kt new file mode 100644 index 00000000..29dfbc59 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/NullNode.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.crypto.Hash.keccak256 +import net.consensys.cava.rlp.RLP + +internal class NullNode private constructor() : Node { + + companion object { + private val RLP_NULL = RLP.encodeByteArray(ByteArray(0)) + private val HASH = keccak256(RLP_NULL) + private val instance = NullNode() + + @Suppress("UNCHECKED_CAST") + fun instance(): NullNode = instance as NullNode + } + + override suspend fun accept(visitor: NodeVisitor, path: Bytes): Node = visitor.visit(this, path) + + override suspend fun path(): Bytes = Bytes.EMPTY + + override suspend fun value(): V? = null + + override fun rlp(): Bytes = RLP_NULL + + override fun rlpRef(): Bytes = RLP_NULL + + override fun hash(): Bytes32 = HASH + + override suspend fun replacePath(path: Bytes): Node = this +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/PutVisitor.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/PutVisitor.kt new file mode 100644 index 00000000..0f00ce22 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/PutVisitor.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.trie.CompactEncoding + +internal class PutVisitor( + private val nodeFactory: NodeFactory, + private val value: V +) : NodeVisitor { + + override suspend fun visit(extensionNode: ExtensionNode, path: Bytes): Node { + val extensionPath = extensionNode.path() + val commonPathLength = extensionPath.commonPrefixLength(path) + assert(commonPathLength < path.size()) { "Visiting path doesn't end with a non-matching terminator" } + + if (commonPathLength == extensionPath.size()) { + val child = extensionNode.child() + val updatedChild = child.accept(this, path.slice(commonPathLength)) + return extensionNode.replaceChild(updatedChild) + } + + // The path diverges before the end of the extension, so create a new branch + + val leafIndex = path.get(commonPathLength) + val leafPath = path.slice(commonPathLength + 1) + + val extensionIndex = extensionPath.get(commonPathLength) + val updatedExtension = extensionNode.replacePath(extensionPath.slice(commonPathLength + 1)) + val leaf = nodeFactory.createLeaf(leafPath, value) + val branch = nodeFactory.createBranch(leafIndex, leaf, extensionIndex, updatedExtension) + + if (commonPathLength == 0) { + return branch + } + return nodeFactory.createExtension(extensionPath.slice(0, commonPathLength), branch) + } + + override suspend fun visit(branchNode: BranchNode, path: Bytes): Node { + assert(path.size() > 0) { "Visiting path doesn't end with a non-matching terminator" } + + val childIndex = path.get(0) + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode.replaceValue(value) + } + + val updatedChild = branchNode.child(childIndex).accept(this, path.slice(1)) + return branchNode.replaceChild(childIndex, updatedChild) + } + + override suspend fun visit(leafNode: LeafNode, path: Bytes): Node { + val leafPath = leafNode.path() + val commonPathLength = leafPath.commonPrefixLength(path) + + // Check if the current leaf node should be replaced + if (commonPathLength == leafPath.size() && commonPathLength == path.size()) { + return nodeFactory.createLeaf(leafPath, value) + } + + assert(commonPathLength < leafPath.size() && commonPathLength < path.size(), + { "Should not have consumed non-matching terminator" }) + + // The current leaf path must be split to accommodate the new value. + + val newLeafIndex = path.get(commonPathLength) + val newLeafPath = path.slice(commonPathLength + 1) + + val updatedLeafIndex = leafPath.get(commonPathLength) + val updatedLeaf = leafNode.replacePath(leafPath.slice(commonPathLength + 1)) + + val leaf = nodeFactory.createLeaf(newLeafPath, value) + val branch = nodeFactory.createBranch(updatedLeafIndex, updatedLeaf, newLeafIndex, leaf) + + if (commonPathLength == 0) { + return branch + } + + return nodeFactory.createExtension(leafPath.slice(0, commonPathLength), branch) + } + + override suspend fun visit(nullNode: NullNode, path: Bytes): Node = nodeFactory.createLeaf(path, value) +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/RemoveVisitor.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/RemoveVisitor.kt new file mode 100644 index 00000000..63396566 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/RemoveVisitor.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.trie.CompactEncoding + +internal class RemoveVisitor : NodeVisitor { + + override suspend fun visit(extensionNode: ExtensionNode, path: Bytes): Node { + val extensionPath = extensionNode.path() + val commonPathLength = extensionPath.commonPrefixLength(path) + assert(commonPathLength < path.size()) { "Visiting path doesn't end with a non-matching terminator" } + + if (commonPathLength == extensionPath.size()) { + val child = extensionNode.child() + val updatedChild = child.accept(this, path.slice(commonPathLength)) + return extensionNode.replaceChild(updatedChild) + } + + // The path diverges before the end of the extension, so it cannot match + + return extensionNode + } + + override suspend fun visit(branchNode: BranchNode, path: Bytes): Node { + assert(path.size() > 0) { "Visiting path doesn't end with a non-matching terminator" } + + val childIndex = path.get(0) + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode.removeValue() + } + + val updatedChild = branchNode.child(childIndex).accept(this, path.slice(1)) + return branchNode.replaceChild(childIndex, updatedChild) + } + + override suspend fun visit(leafNode: LeafNode, path: Bytes): Node { + val leafPath = leafNode.path() + val commonPathLength = leafPath.commonPrefixLength(path) + if (commonPathLength == leafPath.size()) { + return NullNode.instance() + } + return leafNode + } + + override suspend fun visit(nullNode: NullNode, path: Bytes): Node = NullNode.instance() +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrie.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrie.kt new file mode 100644 index 00000000..f7f54fd6 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrie.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.trie.CompactEncoding.bytesToPath +import java.util.function.Function + +/** + * A [MerkleTrie] that persists trie nodes to a [MerkleStorage] key/value store. + * + * @param The type of values stored by this trie. + */ +class StoredMerklePatriciaTrie : MerkleTrie { + + companion object { + /** + * Create a trie with value of type [Bytes]. + * + * @param storage The storage to use for persistence. + */ + @JvmStatic + fun storingBytes(storage: MerkleStorage): StoredMerklePatriciaTrie = + StoredMerklePatriciaTrie(storage, ::bytesIdentity, ::bytesIdentity) + + /** + * Create a trie with keys and values of type [Bytes]. + * + * @param storage The storage to use for persistence. + * @param rootHash The initial root has for the trie, which should be already present in `storage`. + */ + @JvmStatic + fun storingBytes(storage: MerkleStorage, rootHash: Bytes32): StoredMerklePatriciaTrie = + StoredMerklePatriciaTrie(storage, rootHash, ::bytesIdentity, ::bytesIdentity) + + /** + * Create a trie with value of type [String]. + * + * Strings are stored in UTF-8 encoding. + * + * @param storage The storage to use for persistence. + */ + @JvmStatic + fun storingStrings(storage: MerkleStorage): StoredMerklePatriciaTrie = + StoredMerklePatriciaTrie(storage, ::stringSerializer, ::stringDeserializer) + + /** + * Create a trie with keys and values of type [String]. + * + * Strings are stored in UTF-8 encoding. + * + * @param storage The storage to use for persistence. + * @param rootHash The initial root has for the trie, which should be already present in `storage`. + */ + @JvmStatic + fun storingStrings(storage: MerkleStorage, rootHash: Bytes32): StoredMerklePatriciaTrie = + StoredMerklePatriciaTrie(storage, rootHash, ::stringSerializer, ::stringDeserializer) + } + + private val getVisitor = GetVisitor() + private val removeVisitor = RemoveVisitor() + private val storage: MerkleStorage + private val nodeFactory: StoredNodeFactory + private var root: Node + + /** + * Create a trie. + * + * @param storage The storage to use for persistence. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + constructor( + storage: MerkleStorage, + valueSerializer: Function, + valueDeserializer: Function + ) : this(storage, MerkleTrie.EMPTY_TRIE_ROOT_HASH, valueSerializer::apply, valueDeserializer::apply) + + /** + * Create a trie. + * + * @param storage The storage to use for persistence. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + constructor( + storage: MerkleStorage, + valueSerializer: (V) -> Bytes, + valueDeserializer: (Bytes) -> V + ) : this(storage, MerkleTrie.EMPTY_TRIE_ROOT_HASH, valueSerializer, valueDeserializer) + + /** + * Create a trie. + * + * @param storage The storage to use for persistence. + * @param rootHash The initial root has for the trie, which should be already present in `storage`. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + constructor( + storage: MerkleStorage, + rootHash: Bytes32, + valueSerializer: Function, + valueDeserializer: Function + ) : this(storage, rootHash, valueSerializer::apply, valueDeserializer::apply) + + /** + * Create a trie. + * + * @param storage The storage to use for persistence. + * @param rootHash The initial root has for the trie, which should be already present in `storage`. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + constructor( + storage: MerkleStorage, + rootHash: Bytes32, + valueSerializer: (V) -> Bytes, + valueDeserializer: (Bytes) -> V + ) { + this.storage = storage + this.nodeFactory = StoredNodeFactory(storage, valueSerializer, valueDeserializer) + + this.root = if (rootHash == MerkleTrie.EMPTY_TRIE_ROOT_HASH) { + NullNode.instance() + } else { + StoredNode(nodeFactory, rootHash) + } + } + + override suspend fun get(key: Bytes): V? = root.accept(getVisitor, bytesToPath(key)).value() + + override suspend fun put(key: Bytes, value: V?) { + if (value == null) { + return remove(key) + } + updateRoot(root.accept(PutVisitor(nodeFactory, value), bytesToPath(key))) + } + + override suspend fun remove(key: Bytes) = updateRoot(root.accept(removeVisitor, bytesToPath(key))) + + override fun rootHash(): Bytes32 = root.hash() + + /** + * Forces any cached trie nodes to be released, so they can be garbage collected. + * + * Note: nodes are already stored using [java.lang.ref.SoftReference]'s, so they will be released automatically + * based on memory demands. + */ + fun clearCache() { + val currentRoot = root + if (currentRoot is StoredNode<*>) { + currentRoot.unload() + } + } + + private suspend fun updateRoot(newRoot: Node) { + this.root = if (newRoot is StoredNode<*>) { + newRoot + } else { + storage.put(newRoot.hash(), newRoot.rlp()) + StoredNode(nodeFactory, newRoot) + } + } + + /** + * @return A string representation of the object. + */ + override fun toString(): String { + return javaClass.simpleName + "[" + rootHash() + "]" + } +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNode.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNode.kt new file mode 100644 index 00000000..bca8efd0 --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNode.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import kotlinx.coroutines.experimental.CoroutineStart +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.async +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.rlp.RLP +import java.lang.ref.SoftReference +import java.util.concurrent.atomic.AtomicReference + +internal class StoredNode : Node { + private val nodeFactory: StoredNodeFactory + private val hash: Bytes32 + private var loaded: SoftReference>? = null + private val loader = AtomicReference>>() + + constructor(nodeFactory: StoredNodeFactory, hash: Bytes32) { + this.nodeFactory = nodeFactory + this.hash = hash + } + + constructor(nodeFactory: StoredNodeFactory, node: Node) { + this.nodeFactory = nodeFactory + this.hash = node.hash() + this.loaded = SoftReference(node) + } + + override suspend fun accept(visitor: NodeVisitor, path: Bytes): Node { + val node = load() + val resultNode = node.accept(visitor, path) + if (node === resultNode) { + return this + } + return resultNode + } + + override suspend fun path(): Bytes = load().path() + + override suspend fun value(): V? = load().value() + + // Getting the rlp representation is only needed when persisting a concrete node + override fun rlp(): Bytes = throw UnsupportedOperationException() + + override fun rlpRef(): Bytes { + val loadedNode = loaded?.get() + if (loadedNode != null) { + return loadedNode.rlpRef() + } + // If this node was stored, then it must have a rlp larger than a hash + return RLP.encodeValue(hash) + } + + override fun hash(): Bytes32 = hash + + override suspend fun replacePath(path: Bytes): Node = load().replacePath(path) + + private suspend fun load(): Node { + val loadedNode = loaded?.get() + if (loadedNode != null) { + return loadedNode + } + + val deferred: Deferred> = async(start = CoroutineStart.LAZY) { + val node = nodeFactory.retrieve(hash) + loaded = SoftReference(node) + loader.set(null) + node + } + + while (!loader.compareAndSet(null, deferred)) { + // already loading + val prevDeferred = loader.get() + if (prevDeferred != null) { + return prevDeferred.await() + } + } + + // we've set the loader + + // check for a loaded node again, in case a loader just completed + val node = loaded?.get() + if (node != null) { + // remove our loader, if it's still set + loader.compareAndSet(deferred, null) + return node + } + + return deferred.await() + } + + fun unload() { + loaded = null + } +} diff --git a/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNodeFactory.kt b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNodeFactory.kt new file mode 100644 index 00000000..994e38ec --- /dev/null +++ b/merkle-trie/src/main/kotlin/net/consensys/cava/trie/experimental/StoredNodeFactory.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.rlp.RLP +import net.consensys.cava.rlp.RLPException +import net.consensys.cava.rlp.RLPReader +import net.consensys.cava.trie.CompactEncoding +import java.util.Collections + +internal class StoredNodeFactory( + private val storage: MerkleStorage, + private val valueSerializer: (V) -> Bytes, + private val valueDeserializer: (Bytes) -> V +) : NodeFactory { + + private val nullNode: NullNode = NullNode.instance() + + override suspend fun createExtension(path: Bytes, child: Node): Node { + return maybeStore(ExtensionNode(path, child, this)) + } + + override suspend fun createBranch(leftIndex: Byte, left: Node, rightIndex: Byte, right: Node): Node { + assert(leftIndex <= BranchNode.RADIX) + assert(rightIndex <= BranchNode.RADIX) + assert(leftIndex != rightIndex) + + val children: MutableList> = Collections.nCopies(BranchNode.RADIX, nullNode).toMutableList() + return when { + leftIndex.toInt() == BranchNode.RADIX -> { + children[rightIndex.toInt()] = right + createBranch(children, left.value()) + } + rightIndex.toInt() == BranchNode.RADIX -> { + children[leftIndex.toInt()] = left + createBranch(children, right.value()) + } + else -> { + children[leftIndex.toInt()] = left + children[rightIndex.toInt()] = right + createBranch(children, null) + } + } + } + + override suspend fun createBranch(newChildren: List>, value: V?): Node { + return maybeStore(BranchNode(newChildren, value, this, valueSerializer)) + } + + override suspend fun createLeaf(path: Bytes, value: V): Node { + return maybeStore(LeafNode(path, value, this, valueSerializer)) + } + + private suspend fun maybeStore(node: Node): Node { + val nodeRLP = node.rlp() + if (nodeRLP.size() < 32) { + return node + } + storage.put(node.hash(), node.rlp()) + return StoredNode(this, node) + } + + internal suspend fun retrieve(hash: Bytes32): Node { + val bytes = storage.get(hash) ?: throw MerkleStorageException("Missing value for hash $hash") + val node = decode(bytes) { "Invalid RLP value for hash $hash" } + assert(hash == node.hash()) { "Node hash ${node.hash()} not equal to expected $hash" } + return node + } + + private fun decode(rlp: Bytes, errMessage: () -> String): Node { + try { + return RLP.decode(rlp) { reader -> decode(reader, errMessage) } + } catch (ex: RLPException) { + throw MerkleStorageException(errMessage(), ex) + } + } + + private fun decode(nodeRLPs: RLPReader, errMessage: () -> String): Node { + return nodeRLPs.readList { listReader -> + val remaining = listReader.remaining() + when (remaining) { + 1 -> decodeNull(listReader, errMessage) + 2 -> { + val encodedPath = listReader.readValue() + val path: Bytes + try { + path = CompactEncoding.decode(encodedPath) + } catch (e: IllegalArgumentException) { + throw MerkleStorageException(errMessage() + ": invalid path " + encodedPath, e) + } + + val size = path.size() + if (size > 0 && path.get(size - 1) == CompactEncoding.LEAF_TERMINATOR) { + decodeLeaf(path, listReader, errMessage) + } else { + decodeExtension(path, listReader, errMessage) + } + } + BranchNode.RADIX + 1 -> decodeBranch(listReader, errMessage) + else -> throw MerkleStorageException(errMessage() + ": invalid list size " + remaining) + } + } + } + + private fun decodeExtension(path: Bytes, valueRlp: RLPReader, errMessage: () -> String): Node { + val child = if (valueRlp.nextIsList()) { + decode(valueRlp, errMessage) + } else { + val childHash: Bytes32 + try { + childHash = Bytes32.wrap(valueRlp.readValue()) + } catch (e: RLPException) { + throw MerkleStorageException(errMessage() + ": invalid extension target") + } catch (e: IllegalArgumentException) { + throw MerkleStorageException(errMessage() + ": invalid extension target") + } + StoredNode(this, childHash) + } + return ExtensionNode(path, child, this) + } + + private fun decodeBranch(nodeRLPs: RLPReader, errMessage: () -> String): BranchNode { + val children = ArrayList>(BranchNode.RADIX.toInt()) + for (i in 0 until BranchNode.RADIX) { + val updatedChild = when { + nodeRLPs.nextIsEmpty() -> { + nodeRLPs.readValue() + nullNode + } + nodeRLPs.nextIsList() -> { + val child = decode(nodeRLPs, errMessage) + StoredNode(this, child) + } + else -> { + val childHash: Bytes32 + try { + childHash = Bytes32.wrap(nodeRLPs.readValue()) + } catch (e: RLPException) { + throw MerkleStorageException(errMessage() + ": invalid branch child " + i) + } catch (e: IllegalArgumentException) { + throw MerkleStorageException(errMessage() + ": invalid branch child " + i) + } + StoredNode(this, childHash) + } + } + children.add(updatedChild) + } + + val value = if (nodeRLPs.nextIsEmpty()) { + nodeRLPs.readValue() + null + } else { + decodeValue(nodeRLPs, errMessage) + } + + return BranchNode(children, value, this, valueSerializer) + } + + private fun decodeLeaf(path: Bytes, valueRlp: RLPReader, errMessage: () -> String): LeafNode { + if (valueRlp.nextIsEmpty()) { + throw MerkleStorageException(errMessage() + ": leaf has null value") + } + val value = decodeValue(valueRlp, errMessage) + return LeafNode(path, value, this, valueSerializer) + } + + private fun decodeNull(nodeRLPs: RLPReader, errMessage: () -> String): NullNode { + if (!nodeRLPs.nextIsEmpty()) { + throw MerkleStorageException(errMessage() + ": list size 1 but not null") + } + nodeRLPs.readValue() + return nullNode + } + + private fun decodeValue(valueRlp: RLPReader, errMessage: () -> String): V { + val bytes: Bytes + try { + bytes = valueRlp.readValue() + } catch (ex: RLPException) { + throw MerkleStorageException(errMessage() + ": failed decoding value rlp " + valueRlp, ex) + } + return deserializeValue(errMessage, bytes) + } + + private fun deserializeValue(errMessage: () -> String, bytes: Bytes): V { + try { + return valueDeserializer(bytes) + } catch (ex: IllegalArgumentException) { + throw MerkleStorageException(errMessage() + ": failed deserializing value " + bytes, ex) + } + } +} diff --git a/merkle-trie/src/test/java/net/consensys/cava/trie/CompactEncodingTest.java b/merkle-trie/src/test/java/net/consensys/cava/trie/CompactEncodingTest.java new file mode 100644 index 00000000..983e99d5 --- /dev/null +++ b/merkle-trie/src/test/java/net/consensys/cava/trie/CompactEncodingTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class CompactEncodingTest { + + @Test + void bytesToPath() { + Bytes path = CompactEncoding.bytesToPath(Bytes.of(0xab, 0xcd, 0xff)); + assertEquals(Bytes.of(0xa, 0xb, 0xc, 0xd, 0xf, 0xf, 0x10), path); + } + + @Test + void encodePath() { + assertEquals(Bytes.of(0x11, 0x23, 0x45), CompactEncoding.encode(Bytes.of(0x01, 0x02, 0x03, 0x04, 0x05))); + assertEquals( + Bytes.of(0x00, 0x01, 0x23, 0x45), + CompactEncoding.encode(Bytes.of(0x00, 0x01, 0x02, 0x03, 0x04, 0x05))); + assertEquals( + Bytes.of(0x20, 0x0f, 0x1c, 0xb8), + CompactEncoding.encode(Bytes.of(0x00, 0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10))); + assertEquals(Bytes.of(0x3f, 0x1c, 0xb8), CompactEncoding.encode(Bytes.of(0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10))); + } + + @Test + void decode() { + assertEquals(Bytes.of(0x01, 0x02, 0x03, 0x04, 0x05), CompactEncoding.decode(Bytes.of(0x11, 0x23, 0x45))); + assertEquals( + Bytes.of(0x00, 0x01, 0x02, 0x03, 0x04, 0x05), + CompactEncoding.decode(Bytes.of(0x00, 0x01, 0x23, 0x45))); + assertEquals( + Bytes.of(0x00, 0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10), + CompactEncoding.decode(Bytes.of(0x20, 0x0f, 0x1c, 0xb8))); + assertEquals(Bytes.of(0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10), CompactEncoding.decode(Bytes.of(0x3f, 0x1c, 0xb8))); + } +} diff --git a/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/MerklePatriciaTrieJavaTest.java b/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/MerklePatriciaTrieJavaTest.java new file mode 100644 index 00000000..103d26ff --- /dev/null +++ b/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/MerklePatriciaTrieJavaTest.java @@ -0,0 +1,248 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental; + +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 net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; +import net.consensys.cava.junit.BouncyCastleExtension; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class MerklePatriciaTrieJavaTest { + private MerkleTrie trie; + + @BeforeEach + void setup() { + trie = MerklePatriciaTrie.storingStrings(); + } + + @Test + void testEmptyTreeReturnsEmpty() throws Exception { + assertFalse(trie.getAsync(Bytes.EMPTY).get().isPresent()); + } + + @Test + void testEmptyTreeHasKnownRootHash() { + assertEquals("0x56E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421", trie.rootHash().toString()); + } + + @Test + void testDeletesEntryUpdateWithNull() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + trie.putAsync(key, null).join(); + assertFalse(trie.getAsync(key).get().isPresent()); + } + + @Test + void testReplaceSingleValue() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key).get()); + + trie.putAsync(key, "value2").join(); + assertEquals(Optional.of("value2"), trie.getAsync(key).get()); + } + + @Test + void testHashChangesWhenSingleValueReplaced() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + final Bytes32 hash1 = trie.rootHash(); + + trie.putAsync(key, "value2").join(); + final Bytes32 hash2 = trie.rootHash(); + + assertNotEquals(hash2, hash1); + + trie.putAsync(key, "value1").join(); + assertEquals(hash1, trie.rootHash()); + } + + @Test + void testReadPastLeaf() throws Exception { + final Bytes key1 = Bytes.of(1); + final Bytes key2 = Bytes.of(1, 3); + trie.putAsync(key1, "value").join(); + assertFalse(trie.getAsync(key2).get().isPresent()); + } + + @Test + void testBranchValue() throws Exception { + final Bytes key1 = Bytes.of(1); + final Bytes key2 = Bytes.of(16); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testReadPastBranch() throws Exception { + final Bytes key1 = Bytes.of(12); + final Bytes key2 = Bytes.of(12, 54); + final Bytes key3 = Bytes.of(3); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertFalse(trie.getAsync(key3).get().isPresent()); + } + + @Test + void testBranchWithValue() throws Exception { + final Bytes key1 = Bytes.of(5); + final Bytes key2 = Bytes.EMPTY; + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testExtendAndBranch() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertFalse(trie.getAsync(Bytes.of(1, 4)).get().isPresent()); + } + + @Test + void testBranchFromTopOfExtend() throws Exception { + final Bytes key1 = Bytes.of(0xFE, 1); + final Bytes key2 = Bytes.of(0xFE, 2); + final Bytes key3 = Bytes.of(0xE1, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + assertFalse(trie.getAsync(Bytes.of(1, 4)).get().isPresent()); + assertFalse(trie.getAsync(Bytes.of(2, 4)).get().isPresent()); + assertFalse(trie.getAsync(Bytes.of(3)).get().isPresent()); + } + + @Test + void testSplitBranchExtension() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + final Bytes key3 = Bytes.of(1, 9, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + } + + @Test + void testReplaceBranchChild() throws Exception { + final Bytes key1 = Bytes.of(0); + final Bytes key2 = Bytes.of(1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + + trie.putAsync(key1, "value3").join(); + assertEquals(Optional.of("value3"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testInlineBranchInBranch() throws Exception { + final Bytes key1 = Bytes.of(0); + final Bytes key2 = Bytes.of(1); + final Bytes key3 = Bytes.of(2); + final Bytes key4 = Bytes.of(0, 0); + final Bytes key5 = Bytes.of(0, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + trie.putAsync(key4, "value4").join(); + trie.putAsync(key5, "value5").join(); + + trie.removeAsync(key2).join(); + trie.removeAsync(key3).join(); + + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertFalse(trie.getAsync(key2).get().isPresent()); + assertFalse(trie.getAsync(key3).get().isPresent()); + assertEquals(Optional.of("value4"), trie.getAsync(key4).get()); + assertEquals(Optional.of("value5"), trie.getAsync(key5).get()); + } + + @Test + void testRemoveNodeInBranchExtensionHasNoEffect() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + + final Bytes hash = trie.rootHash(); + trie.removeAsync(Bytes.of(1, 4)).join(); + assertEquals(hash, trie.rootHash()); + } + + @Test + void testHashChangesWhenValueChanged() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 8, 9); + final Bytes key2 = Bytes.of(1, 6, 1, 2); + final Bytes key3 = Bytes.of(1, 6, 1, 3); + + trie.putAsync(key1, "value1").join(); + final Bytes32 hash1 = trie.rootHash(); + + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + final Bytes32 hash2 = trie.rootHash(); + + assertNotEquals(hash2, hash1); + + trie.putAsync(key1, "value4").join(); + final Bytes32 hash3 = trie.rootHash(); + + assertNotEquals(hash3, hash1); + assertNotEquals(hash3, hash2); + + trie.putAsync(key1, "value1").join(); + assertEquals(hash2, trie.rootHash()); + + trie.removeAsync(key2).join(); + trie.removeAsync(key3).join(); + assertEquals(hash1, trie.rootHash()); + } +} diff --git a/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieJavaTest.java b/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieJavaTest.java new file mode 100644 index 00000000..b8f39c5e --- /dev/null +++ b/merkle-trie/src/test/java/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieJavaTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental; + +import static org.junit.jupiter.api.Assertions.*; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; +import net.consensys.cava.concurrent.AsyncCompletion; +import net.consensys.cava.concurrent.AsyncResult; +import net.consensys.cava.junit.BouncyCastleExtension; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class StoredMerklePatriciaTrieJavaTest { + + private MerkleStorage merkleStorage; + private StoredMerklePatriciaTrie trie; + + @BeforeEach + void setup() { + Map storage = new HashMap<>(); + merkleStorage = new AsyncMerkleStorage() { + @Override + public @NotNull AsyncResult> getAsync(@NotNull Bytes32 hash) { + return AsyncResult.completed(Optional.ofNullable(storage.get(hash))); + } + + @Override + public @NotNull AsyncCompletion putAsync(@NotNull Bytes32 hash, @NotNull Bytes content) { + storage.put(hash, content); + return AsyncCompletion.completed(); + } + }; + + trie = StoredMerklePatriciaTrie.storingStrings(merkleStorage); + } + + @Test + void testEmptyTreeReturnsEmpty() throws Exception { + assertFalse(trie.getAsync(Bytes.EMPTY).get().isPresent()); + } + + @Test + void testEmptyTreeHasKnownRootHash() { + assertEquals("0x56E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421", trie.rootHash().toString()); + } + + @Test + void testDeletesEntryUpdateWithNull() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + trie.putAsync(key, null).join(); + assertFalse(trie.getAsync(key).get().isPresent()); + } + + @Test + void testReplaceSingleValue() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key).get()); + + trie.putAsync(key, "value2").join(); + assertEquals(Optional.of("value2"), trie.getAsync(key).get()); + } + + @Test + void testHashChangesWhenSingleValueReplaced() throws Exception { + final Bytes key = Bytes.of(1); + + trie.putAsync(key, "value1").join(); + final Bytes32 hash1 = trie.rootHash(); + + trie.putAsync(key, "value2").join(); + final Bytes32 hash2 = trie.rootHash(); + + assertNotEquals(hash2, hash1); + + trie.putAsync(key, "value1").join(); + assertEquals(hash1, trie.rootHash()); + } + + @Test + void testReadPastLeaf() throws Exception { + final Bytes key1 = Bytes.of(1); + final Bytes key2 = Bytes.of(1, 3); + trie.putAsync(key1, "value").join(); + assertFalse(trie.getAsync(key2).get().isPresent()); + } + + @Test + void testBranchValue() throws Exception { + final Bytes key1 = Bytes.of(1); + final Bytes key2 = Bytes.of(16); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testReadPastBranch() throws Exception { + final Bytes key1 = Bytes.of(12); + final Bytes key2 = Bytes.of(12, 54); + final Bytes key3 = Bytes.of(3); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertFalse(trie.getAsync(key3).get().isPresent()); + } + + @Test + void testBranchWithValue() throws Exception { + final Bytes key1 = Bytes.of(5); + final Bytes key2 = Bytes.EMPTY; + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testExtendAndBranch() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertFalse(trie.getAsync(Bytes.of(1, 4)).get().isPresent()); + } + + @Test + void testBranchFromTopOfExtend() throws Exception { + final Bytes key1 = Bytes.of(0xFE, 1); + final Bytes key2 = Bytes.of(0xFE, 2); + final Bytes key3 = Bytes.of(0xE1, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + assertFalse(trie.getAsync(Bytes.of(1, 4)).get().isPresent()); + assertFalse(trie.getAsync(Bytes.of(2, 4)).get().isPresent()); + assertFalse(trie.getAsync(Bytes.of(3)).get().isPresent()); + } + + @Test + void testSplitBranchExtension() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + final Bytes key3 = Bytes.of(1, 9, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + } + + @Test + void testReplaceBranchChild() throws Exception { + final Bytes key1 = Bytes.of(0); + final Bytes key2 = Bytes.of(1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + + trie.putAsync(key1, "value3").join(); + assertEquals(Optional.of("value3"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + } + + @Test + void testInlineBranchInBranch() throws Exception { + final Bytes key1 = Bytes.of(0); + final Bytes key2 = Bytes.of(1); + final Bytes key3 = Bytes.of(2); + final Bytes key4 = Bytes.of(0, 0); + final Bytes key5 = Bytes.of(0, 1); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + trie.putAsync(key4, "value4").join(); + trie.putAsync(key5, "value5").join(); + + trie.removeAsync(key2).join(); + trie.removeAsync(key3).join(); + + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertFalse(trie.getAsync(key2).get().isPresent()); + assertFalse(trie.getAsync(key3).get().isPresent()); + assertEquals(Optional.of("value4"), trie.getAsync(key4).get()); + assertEquals(Optional.of("value5"), trie.getAsync(key5).get()); + } + + @Test + void testRemoveNodeInBranchExtensionHasNoEffect() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 9); + final Bytes key2 = Bytes.of(1, 5, 2); + + trie.putAsync(key1, "value1").join(); + trie.putAsync(key2, "value2").join(); + + final Bytes hash = trie.rootHash(); + trie.removeAsync(Bytes.of(1, 4)).join(); + assertEquals(hash, trie.rootHash()); + } + + @Test + void testHashChangesWhenValueChanged() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 8, 9); + final Bytes key2 = Bytes.of(1, 6, 1, 2); + final Bytes key3 = Bytes.of(1, 6, 1, 3); + + trie.putAsync(key1, "value1").join(); + final Bytes32 hash1 = trie.rootHash(); + + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + final Bytes32 hash2 = trie.rootHash(); + + assertNotEquals(hash2, hash1); + + trie.putAsync(key1, "value4").join(); + final Bytes32 hash3 = trie.rootHash(); + + assertNotEquals(hash3, hash1); + assertNotEquals(hash3, hash2); + + trie.clearCache(); + + trie.putAsync(key1, "value1").join(); + assertEquals(hash2, trie.rootHash()); + + trie.removeAsync(key2).join(); + trie.removeAsync(key3).join(); + assertEquals(hash1, trie.rootHash()); + } + + @Test + void testCanReloadTrieFromHash() throws Exception { + final Bytes key1 = Bytes.of(1, 5, 8, 9); + final Bytes key2 = Bytes.of(1, 6, 1, 2); + final Bytes key3 = Bytes.of(1, 6, 1, 3); + + trie.putAsync(key1, "value1").join(); + final Bytes32 hash1 = trie.rootHash(); + + trie.putAsync(key2, "value2").join(); + trie.putAsync(key3, "value3").join(); + final Bytes32 hash2 = trie.rootHash(); + assertNotEquals(hash2, hash1); + + trie.putAsync(key1, "value4").join(); + final Bytes32 hash3 = trie.rootHash(); + assertNotEquals(hash3, hash1); + assertNotEquals(hash3, hash2); + + assertEquals(Optional.of("value4"), trie.getAsync(key1).get()); + + trie = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash1); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.empty(), trie.getAsync(key2).get()); + assertEquals(Optional.empty(), trie.getAsync(key3).get()); + + trie = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash2); + assertEquals(Optional.of("value1"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + + trie = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash3); + assertEquals(Optional.of("value4"), trie.getAsync(key1).get()); + assertEquals(Optional.of("value2"), trie.getAsync(key2).get()); + assertEquals(Optional.of("value3"), trie.getAsync(key3).get()); + } +} diff --git a/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrieKotlinTest.kt b/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrieKotlinTest.kt new file mode 100644 index 00000000..d1666e41 --- /dev/null +++ b/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/MerklePatriciaTrieKotlinTest.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import kotlinx.coroutines.experimental.runBlocking +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.junit.BouncyCastleExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class MerklePatriciaTrieKotlinTest { + + private lateinit var trie: MerkleTrie + + @BeforeEach + fun setup() { + trie = MerklePatriciaTrie.storingStrings() + } + + @Test + fun testEmptyTreeReturnsEmpty() { + runBlocking { + assertNull(trie.get(Bytes.EMPTY)) + } + } + + @Test + fun testEmptyTreeHasKnownRootHash() { + assertEquals("0x56E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421", trie.rootHash().toString()) + } + + @Test + fun testDeletesEntryUpdateWithNull() { + val key = Bytes.of(1) + runBlocking { + trie.put(key, "value1") + trie.put(key, null) + assertNull(trie.get(key)) + } + } + + @Test + fun testReplaceSingleValue() { + val key = Bytes.of(1) + val value1 = "value1" + val value2 = "value2" + runBlocking { + trie.put(key, value1) + assertEquals(value1, trie.get(key)) + + trie.put(key, value2) + assertEquals(value2, trie.get(key)) + } + } + + @Test + fun testHashChangesWhenSingleValueReplaced() { + val key = Bytes.of(1) + runBlocking { + trie.put(key, "value1") + val hash1 = trie.rootHash() + + trie.put(key, "value2") + val hash2 = trie.rootHash() + + assertNotEquals(hash2, hash1) + + trie.put(key, "value1") + assertEquals(hash1, trie.rootHash()) + } + } + + @Test + fun testReadPastLeaf() { + val key1 = Bytes.of(1) + val key2 = Bytes.of(1, 3) + runBlocking { + trie.put(key1, "value") + assertNull(trie.get(key2)) + } + } + + @Test + fun testBranchValue() { + val key1 = Bytes.of(1) + val key2 = Bytes.of(16) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testReadPastBranch() { + val key1 = Bytes.of(12) + val key2 = Bytes.of(12, 54) + val key3 = Bytes.of(3) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertNull(trie.get(key3)) + } + } + + @Test + fun testBranchWithValue() { + val key1 = Bytes.of(5) + val key2 = Bytes.EMPTY + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testExtendAndBranch() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + val key3 = Bytes.of(1, 4) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertNull(trie.get(key3)) + } + } + + @Test + fun testBranchFromTopOfExtend() { + val key1 = Bytes.of(0xFE, 1) + val key2 = Bytes.of(0xFE, 2) + val key3 = Bytes.of(0xE1, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertEquals("value3", trie.get(key3)) + assertNull(trie.get(Bytes.of(1, 4))) + assertNull(trie.get(Bytes.of(2, 4))) + assertNull(trie.get(Bytes.of(3))) + } + } + + @Test + fun testSplitBranchExtension() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + val key3 = Bytes.of(1, 9, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertEquals("value3", trie.get(key3)) + } + } + + @Test + fun testReplaceBranchChild() { + val key1 = Bytes.of(0) + val key2 = Bytes.of(1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + + trie.put(key1, "value3") + assertEquals("value3", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testInlineBranchInBranch() { + val key1 = Bytes.of(0) + val key2 = Bytes.of(1) + val key3 = Bytes.of(2) + val key4 = Bytes.of(0, 0) + val key5 = Bytes.of(0, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + trie.put(key4, "value4") + trie.put(key5, "value5") + + trie.remove(key2) + trie.remove(key3) + + assertEquals("value1", trie.get(key1)) + assertNull(trie.get(key2)) + assertNull(trie.get(key3)) + assertEquals("value4", trie.get(key4)) + assertEquals("value5", trie.get(key5)) + } + } + + @Test + fun testRemoveNodeInBranchExtensionHasNoEffect() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + + val hash = trie.rootHash() + trie.remove(Bytes.of(1, 4)) + assertEquals(hash, trie.rootHash()) + } + } + + @Test + fun testHashChangesWhenValueChanged() { + val key1 = Bytes.of(1, 5, 8, 9) + val key2 = Bytes.of(1, 6, 1, 2) + val key3 = Bytes.of(1, 6, 1, 3) + runBlocking { + trie.put(key1, "value1") + val hash1 = trie.rootHash() + + trie.put(key2, "value2") + trie.put(key3, "value3") + val hash2 = trie.rootHash() + + assertNotEquals(hash2, hash1) + + trie.put(key1, "value4") + val hash3 = trie.rootHash() + + assertNotEquals(hash3, hash1) + assertNotEquals(hash3, hash2) + + trie.put(key1, "value1") + assertEquals(hash2, trie.rootHash()) + + trie.remove(key2) + trie.remove(key3) + assertEquals(hash1, trie.rootHash()) + } + } +} diff --git a/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieKotlinTest.kt b/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieKotlinTest.kt new file mode 100644 index 00000000..1915d8b1 --- /dev/null +++ b/merkle-trie/src/test/kotlin/net/consensys/cava/trie/experimental/StoredMerklePatriciaTrieKotlinTest.kt @@ -0,0 +1,326 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.trie.experimental + +import kotlinx.coroutines.experimental.runBlocking +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes32 +import net.consensys.cava.junit.BouncyCastleExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class StoredMerklePatriciaTrieKotlinTest { + + private lateinit var storage: MutableMap + private val merkleStorage = object : MerkleStorage { + override suspend fun get(hash: Bytes32): Bytes? = storage[hash] + override suspend fun put(hash: Bytes32, content: Bytes) { + storage[hash] = content + } + } + private lateinit var trie: StoredMerklePatriciaTrie + + @BeforeEach + fun setup() { + storage = mutableMapOf() + trie = StoredMerklePatriciaTrie.storingStrings(merkleStorage) + } + + @Test + fun testEmptyTreeReturnsEmpty() { + runBlocking { + assertNull(trie.get(Bytes.EMPTY)) + } + } + + @Test + fun testEmptyTreeHasKnownRootHash() { + assertEquals("0x56E81F171BCC55A6FF8345E692C0F86E5B48E01B996CADC001622FB5E363B421", trie.rootHash().toString()) + } + + @Test + fun testDeletesEntryUpdateWithNull() { + val key = Bytes.of(1) + runBlocking { + trie.put(key, "value1") + trie.put(key, null) + assertNull(trie.get(key)) + } + } + + @Test + fun testReplaceSingleValue() { + val key = Bytes.of(1) + val value1 = "value1" + val value2 = "value2" + runBlocking { + trie.put(key, value1) + assertEquals(value1, trie.get(key)) + + trie.put(key, value2) + assertEquals(value2, trie.get(key)) + } + } + + @Test + fun testHashChangesWhenSingleValueReplaced() { + val key = Bytes.of(1) + runBlocking { + trie.put(key, "value1") + val hash1 = trie.rootHash() + + trie.put(key, "value2") + val hash2 = trie.rootHash() + + assertNotEquals(hash2, hash1) + + trie.put(key, "value1") + assertEquals(hash1, trie.rootHash()) + } + } + + @Test + fun testReadPastLeaf() { + val key1 = Bytes.of(1) + val key2 = Bytes.of(1, 3) + runBlocking { + trie.put(key1, "value") + assertNull(trie.get(key2)) + } + } + + @Test + fun testBranchValue() { + val key1 = Bytes.of(1) + val key2 = Bytes.of(16) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testReadPastBranch() { + val key1 = Bytes.of(12) + val key2 = Bytes.of(12, 54) + val key3 = Bytes.of(3) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertNull(trie.get(key3)) + } + } + + @Test + fun testBranchWithValue() { + val key1 = Bytes.of(5) + val key2 = Bytes.EMPTY + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testExtendAndBranch() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + val key3 = Bytes.of(1, 4) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertNull(trie.get(key3)) + } + } + + @Test + fun testBranchFromTopOfExtend() { + val key1 = Bytes.of(0xFE, 1) + val key2 = Bytes.of(0xFE, 2) + val key3 = Bytes.of(0xE1, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertEquals("value3", trie.get(key3)) + assertNull(trie.get(Bytes.of(1, 4))) + assertNull(trie.get(Bytes.of(2, 4))) + assertNull(trie.get(Bytes.of(3))) + } + } + + @Test + fun testSplitBranchExtension() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + val key3 = Bytes.of(1, 9, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + assertEquals("value3", trie.get(key3)) + } + } + + @Test + fun testReplaceBranchChild() { + val key1 = Bytes.of(0) + val key2 = Bytes.of(1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + assertEquals("value1", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + + trie.put(key1, "value3") + assertEquals("value3", trie.get(key1)) + assertEquals("value2", trie.get(key2)) + } + } + + @Test + fun testInlineBranchInBranch() { + val key1 = Bytes.of(0) + val key2 = Bytes.of(1) + val key3 = Bytes.of(2) + val key4 = Bytes.of(0, 0) + val key5 = Bytes.of(0, 1) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + trie.put(key3, "value3") + trie.put(key4, "value4") + trie.put(key5, "value5") + + trie.remove(key2) + trie.remove(key3) + + assertEquals("value1", trie.get(key1)) + assertNull(trie.get(key2)) + assertNull(trie.get(key3)) + assertEquals("value4", trie.get(key4)) + assertEquals("value5", trie.get(key5)) + } + } + + @Test + fun testRemoveNodeInBranchExtensionHasNoEffect() { + val key1 = Bytes.of(1, 5, 9) + val key2 = Bytes.of(1, 5, 2) + runBlocking { + trie.put(key1, "value1") + trie.put(key2, "value2") + + val hash = trie.rootHash() + trie.remove(Bytes.of(1, 4)) + assertEquals(hash, trie.rootHash()) + } + } + + @Test + fun testHashChangesWhenValueChanged() { + val key1 = Bytes.of(1, 5, 8, 9) + val key2 = Bytes.of(1, 6, 1, 2) + val key3 = Bytes.of(1, 6, 1, 3) + runBlocking { + trie.put(key1, "value1") + val hash1 = trie.rootHash() + + trie.put(key2, "value2") + trie.put(key3, "value3") + val hash2 = trie.rootHash() + + assertNotEquals(hash2, hash1) + + trie.put(key1, "value4") + val hash3 = trie.rootHash() + + assertNotEquals(hash3, hash1) + assertNotEquals(hash3, hash2) + + trie.clearCache() + + trie.put(key1, "value1") + assertEquals(hash2, trie.rootHash()) + + trie.remove(key2) + trie.remove(key3) + assertEquals(hash1, trie.rootHash()) + } + } + + @Test + fun testCanReloadTrieFromHash() { + val key1 = Bytes.of(1, 5, 8, 9) + val key2 = Bytes.of(1, 6, 1, 2) + val key3 = Bytes.of(1, 6, 1, 3) + runBlocking { + trie.put(key1, "value1") + } + val hash1 = trie.rootHash() + + runBlocking { + trie.put(key2, "value2") + trie.put(key3, "value3") + } + val hash2 = trie.rootHash() + assertNotEquals(hash2, hash1) + + runBlocking { + trie.put(key1, "value4") + assertEquals("value4", trie.get(key1)) + } + val hash3 = trie.rootHash() + assertNotEquals(hash3, hash1) + assertNotEquals(hash3, hash2) + + val trie1 = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash1) + runBlocking { + assertEquals("value1", trie1.get(key1)) + assertNull(trie1.get(key2)) + assertNull(trie1.get(key3)) + } + + val trie2 = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash2) + runBlocking { + assertEquals("value1", trie2.get(key1)) + assertEquals("value2", trie2.get(key2)) + assertEquals("value3", trie2.get(key3)) + } + + val trie3 = StoredMerklePatriciaTrie.storingStrings(merkleStorage, hash3) + runBlocking { + assertEquals("value4", trie3.get(key1)) + assertEquals("value2", trie3.get(key2)) + assertEquals("value3", trie3.get(key3)) + } + } +} diff --git a/net/build.gradle b/net/build.gradle new file mode 100644 index 00000000..0b914a01 --- /dev/null +++ b/net/build.gradle @@ -0,0 +1,21 @@ +description = 'Classes and utilities for working with networking.' + +dependencies { + compile project(':bytes') + compile project(':crypto') + compile project(':io') + compile 'com.google.guava:guava' + compileOnly 'io.vertx:vertx-core' + compileOnly 'org.bouncycastle:bcprov-jdk15on' + compileOnly 'org.bouncycastle:bcpkix-jdk15on' + + testCompile project(':junit') + testCompile 'com.squareup.okhttp3:okhttp:3.9.1' + testCompile 'io.vertx:vertx-core' + testCompile 'org.bouncycastle:bcprov-jdk15on' + testCompile 'org.bouncycastle:bcpkix-jdk15on' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/net/src/main/java/net/consensys/cava/net/package-info.java b/net/src/main/java/net/consensys/cava/net/package-info.java new file mode 100644 index 00000000..3839d816 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/package-info.java @@ -0,0 +1,8 @@ +/** + * Classes and utilities for working with networking. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-net' (cava-net.jar). + */ +package net.consensys.cava.net; diff --git a/net/src/main/java/net/consensys/cava/net/tls/DelegatingTrustManagerFactory.java b/net/src/main/java/net/consensys/cava/net/tls/DelegatingTrustManagerFactory.java new file mode 100644 index 00000000..c65ef393 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/DelegatingTrustManagerFactory.java @@ -0,0 +1,232 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.util.Objects.requireNonNull; + +import java.net.Socket; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; + +final class DelegatingTrustManagerFactory extends SimpleTrustManagerFactory { + + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0]; + + private final TrustManagerFactory delegate; + private final X509TrustManager fallback; + private final TrustManager[] trustManagers; + + DelegatingTrustManagerFactory(TrustManagerFactory delegate, X509TrustManager fallback) { + requireNonNull(delegate); + requireNonNull(fallback); + this.delegate = delegate; + this.fallback = fallback; + this.trustManagers = new TrustManager[] {new DelegatingTrustManager()}; + } + + @Override + protected void engineInit(KeyStore keyStore) throws Exception { + delegate.init(keyStore); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { + delegate.init(managerFactoryParameters); + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return trustManagers; + } + + private class DelegatingTrustManager extends X509ExtendedTrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkClientTrusted(chain, authType, socket); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + if (fallback instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) fallback).checkClientTrusted(chain, authType, socket); + } else { + fallback.checkClientTrusted(chain, authType); + } + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkServerTrusted(chain, authType, socket); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + if (fallback instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) fallback).checkServerTrusted(chain, authType, socket); + } else { + fallback.checkServerTrusted(chain, authType); + } + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkClientTrusted(chain, authType, engine); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + if (fallback instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) fallback).checkClientTrusted(chain, authType, engine); + } else { + fallback.checkClientTrusted(chain, authType); + } + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) trustManager).checkServerTrusted(chain, authType, engine); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + if (fallback instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) fallback).checkServerTrusted(chain, authType, engine); + } else { + fallback.checkServerTrusted(chain, authType); + } + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + ((X509TrustManager) trustManager).checkClientTrusted(chain, authType); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + fallback.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException caException = null; + try { + for (TrustManager trustManager : delegate.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + ((X509TrustManager) trustManager).checkServerTrusted(chain, authType); + return; + } + } + } catch (CertificateException e) { + caException = e; + } + + try { + fallback.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + if (caException != null) { + e.addSuppressed(caException); + } + throw e; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_X509_CERTIFICATES; + } + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/FingerprintRepository.java b/net/src/main/java/net/consensys/cava/net/tls/FingerprintRepository.java new file mode 100644 index 00000000..fd075994 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/FingerprintRepository.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.file.Files.createDirectories; +import static net.consensys.cava.io.file.Files.atomicReplace; +import static net.consensys.cava.io.file.Files.copy; +import static net.consensys.cava.io.file.Files.createFileIfMissing; + +import net.consensys.cava.bytes.Bytes; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class FingerprintRepository { + + private final Path fingerprintFile; + private final Set fingerprints; + + FingerprintRepository(Path fingerprintFile) { + try { + createDirectories(fingerprintFile.getParent()); + createFileIfMissing(fingerprintFile); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot create fingerprint file " + fingerprintFile, e); + } + try { + this.fingerprintFile = fingerprintFile; + try (Stream lines = Files.lines(fingerprintFile)) { + this.fingerprints = lines + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(Bytes::fromHexString) + .collect(Collectors.toSet()); + } + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot read fingerprint file " + fingerprintFile, e); + } + } + + boolean contains(Bytes fingerprint) { + return fingerprints.contains(fingerprint); + } + + void addFingerprint(Bytes fingerprint) { + try { + if (!contains(fingerprint)) { + synchronized (fingerprints) { + if (!contains(fingerprint)) { + atomicReplace(fingerprintFile, writer -> { + copy(fingerprintFile, writer); + writer.write(fingerprint.toHexString().substring(2).toLowerCase()); + writer.write(System.lineSeparator()); + }); + fingerprints.add(fingerprint); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/FingerprintTrustManager.java b/net/src/main/java/net/consensys/cava/net/tls/FingerprintTrustManager.java new file mode 100644 index 00000000..5237cfc2 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/FingerprintTrustManager.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import net.consensys.cava.bytes.Bytes; + +import java.nio.file.Path; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.X509TrustManager; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.EmptyArrays; + +final class FingerprintTrustManager implements X509TrustManager { + + static FingerprintTrustManager record(Path repositoryPath) { + return new FingerprintTrustManager(repositoryPath, true); + } + + static FingerprintTrustManager whitelist(Path repositoryPath) { + return new FingerprintTrustManager(repositoryPath, false); + } + + private final FingerprintRepository repository; + private final boolean acceptNewFingerprints; + + private FingerprintTrustManager(Path repositoryPath, boolean acceptNewFingerprints) { + this.repository = new FingerprintRepository(repositoryPath); + this.acceptNewFingerprints = acceptNewFingerprints; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkTrusted(chain, authType, true); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkTrusted(chain, authType, false); + } + + void checkTrusted(X509Certificate[] chain, String authType, boolean isClient) throws CertificateException { + X509Certificate cert = chain[0]; + Bytes fingerprint = Bytes.wrap(Hashing.sha256().hashBytes(cert.getEncoded()).asBytes()); + if (repository.contains(fingerprint)) { + return; + } + + if (acceptNewFingerprints) { + repository.addFingerprint(fingerprint); + return; + } + + throw new CertificateException( + "Certificate for " + + cert.getSubjectDN() + + " with unknown fingerprint " + + fingerprint.toHexString().substring(2).toLowerCase()); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EmptyArrays.EMPTY_X509_CERTIFICATES; + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintRepository.java b/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintRepository.java new file mode 100644 index 00000000..d9257f6d --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintRepository.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.file.Files.createDirectories; +import static net.consensys.cava.io.file.Files.atomicReplace; +import static net.consensys.cava.io.file.Files.copy; +import static net.consensys.cava.io.file.Files.createFileIfMissing; + +import net.consensys.cava.bytes.Bytes; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class HostFingerprintRepository { + + private final Path fingerprintFile; + private final Map fingerprints; + + HostFingerprintRepository(Path fingerprintFile) { + try { + createDirectories(fingerprintFile.getParent()); + createFileIfMissing(fingerprintFile); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot create fingerprint file " + fingerprintFile, e); + } + try { + this.fingerprintFile = fingerprintFile; + try (Stream lines = Files.lines(fingerprintFile)) { + this.fingerprints = lines + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(line -> line.split("\\s+", 2)) + .collect(Collectors.toMap(segments -> segments[0], segments -> Bytes.fromHexString(segments[1]))); + } + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot read fingerprint file " + fingerprintFile, e); + } + } + + boolean contains(String host, int port) { + return fingerprints.containsKey(hostIdentifier(host, port)); + } + + boolean contains(String host, int port, Bytes fingerprint) { + return contains(hostIdentifier(host, port), fingerprint); + } + + private boolean contains(String hostIdentifier, Bytes fingerprint) { + return fingerprint.equals(fingerprints.get(hostIdentifier)); + } + + void addHostFingerprint(String host, int port, Bytes fingerprint) { + String hostIdentifier = hostIdentifier(host, port); + try { + if (!contains(hostIdentifier, fingerprint)) { + synchronized (fingerprints) { + if (!contains(hostIdentifier, fingerprint)) { + atomicReplace(fingerprintFile, writer -> { + copy(fingerprintFile, writer); + writer.write(hostIdentifier); + writer.write(' '); + writer.write(fingerprint.toHexString().substring(2).toLowerCase()); + writer.write(System.lineSeparator()); + }); + fingerprints.put(hostIdentifier, fingerprint); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private String hostIdentifier(String host, int port) { + return host.trim().toLowerCase() + ":" + port; + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintTrustManager.java b/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintTrustManager.java new file mode 100644 index 00000000..cb0a2eee --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/HostFingerprintTrustManager.java @@ -0,0 +1,113 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import net.consensys.cava.bytes.Bytes; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.file.Path; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +import com.google.common.hash.Hashing; + +final class HostFingerprintTrustManager extends X509ExtendedTrustManager { + + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0]; + + static HostFingerprintTrustManager record(Path repository) { + return new HostFingerprintTrustManager(repository, true, true); + } + + static HostFingerprintTrustManager tofu(Path repository) { + return new HostFingerprintTrustManager(repository, true, false); + } + + static HostFingerprintTrustManager whitelist(Path repository) { + return new HostFingerprintTrustManager(repository, false, false); + } + + private final HostFingerprintRepository repository; + private final boolean acceptNewFingerprints; + private final boolean updateFingerprints; + + private HostFingerprintTrustManager(Path repository, boolean acceptNewFingerprints, boolean updateFingerprints) { + this.repository = new HostFingerprintRepository(repository); + this.acceptNewFingerprints = acceptNewFingerprints; + this.updateFingerprints = updateFingerprints; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); + checkTrusted(chain, socketAddress.getHostName(), socketAddress.getPort()); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + checkTrusted(chain, engine.getPeerHost(), engine.getPeerPort()); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + throw new UnsupportedOperationException(); + } + + private void checkTrusted(X509Certificate[] chain, String host, int port) throws CertificateException { + X509Certificate cert = chain[0]; + Bytes fingerprint = Bytes.wrap(Hashing.sha256().hashBytes(cert.getEncoded()).asBytes()); + if (repository.contains(host, port, fingerprint)) { + return; + } + + if (updateFingerprints || (acceptNewFingerprints && !repository.contains(host, port))) { + repository.addHostFingerprint(host, port, fingerprint); + return; + } + + throw new CertificateException( + "Certificate for " + + cert.getSubjectDN() + + " (" + + host + + ":" + + port + + ") with unknown fingerprint: " + + fingerprint); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_X509_CERTIFICATES; + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/SingleTrustManagerFactory.java b/net/src/main/java/net/consensys/cava/net/tls/SingleTrustManagerFactory.java new file mode 100644 index 00000000..a7e2021f --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/SingleTrustManagerFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import java.security.KeyStore; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; + +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; + +final class SingleTrustManagerFactory extends SimpleTrustManagerFactory { + + private final TrustManager[] trustManagers; + + SingleTrustManagerFactory(TrustManager trustManager) { + this.trustManagers = new TrustManager[] {trustManager}; + } + + @Override + protected void engineInit(KeyStore keyStore) {} + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) {} + + @Override + protected TrustManager[] engineGetTrustManagers() { + return trustManagers; + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/TLS.java b/net/src/main/java/net/consensys/cava/net/tls/TLS.java new file mode 100644 index 00000000..6fd499ce --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/TLS.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.createDirectories; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; + +/** + * Common utilities for TLS. + * + *

+ * This class depends upon the BouncyCastle library being available and added as a {@link java.security.Provider}. See + * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation. + * + *

+ * BouncyCastle can be included using the gradle dependencies org.bouncycastle:bcprov-jdk15on and + * org.bouncycastle:bcpkix-jdk15on. + */ +public final class TLS { + private TLS() {} + + /** + * Create a self-signed certificate, if it is not already present. + * + *

+ * If either the key or the certificate file are missing, both will be re-created as a self-signed certificate. + * + * @param key The key path. + * @param certificate The certificate path. + * @throws IOException If an IO error occurs creating the certificate. + */ + public static void createSelfSignedCertificateIfMissing(Path key, Path certificate) throws IOException { + if (Files.exists(certificate) && Files.exists(key)) { + return; + } + + createDirectories(certificate.getParent()); + createDirectories(key.getParent()); + + Path keyFile = Files.createTempFile(key.getParent(), "client-key", ".tmp"); + Path certFile = Files.createTempFile(certificate.getParent(), "client-cert", ".tmp"); + + try { + createSelfSignedCertificate(new Date(), keyFile, certFile); + } catch (CertificateException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new TLSEnvironmentException("Could not generate certificate: " + e.getMessage(), e); + } + + Files.move(keyFile, key, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + Files.move(certFile, certificate, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } + + private static void createSelfSignedCertificate(Date now, Path key, Path certificate) throws NoSuchAlgorithmException, + IOException, + OperatorCreationException, + CertificateException { + KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); + rsa.initialize(2048, new SecureRandom()); + + KeyPair keyPair = rsa.generateKeyPair(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(now); + cal.add(Calendar.YEAR, 1); + Date yearFromNow = cal.getTime(); + + X500Name dn = new X500Name("CN=example.com"); + + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + dn, + new BigInteger(64, new SecureRandom()), + now, + yearFromNow, + dn, + keyPair.getPublic()); + + ContentSigner signer = + new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider("BC").build(keyPair.getPrivate()); + X509Certificate x509Certificate = + new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); + + try (BufferedWriter writer = Files.newBufferedWriter(key, UTF_8); PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded())); + } + + try (BufferedWriter writer = Files.newBufferedWriter(certificate, UTF_8); + PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject("CERTIFICATE", x509Certificate.getEncoded())); + } + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/TLSEnvironmentException.java b/net/src/main/java/net/consensys/cava/net/tls/TLSEnvironmentException.java new file mode 100644 index 00000000..417f9451 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/TLSEnvironmentException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +final class TLSEnvironmentException extends RuntimeException { + + TLSEnvironmentException(String message, Throwable t) { + super(message, t); + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactories.java b/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactories.java new file mode 100644 index 00000000..946cba80 --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactories.java @@ -0,0 +1,317 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import javax.annotation.Nullable; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * Trust manager factories for fingerprinting clients and servers. + */ +public final class TrustManagerFactories { + private TrustManagerFactories() {} + + /** + * Accept all server certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a server presents a CA-signed certificate, the server host+port and the certificate fingerprint will + * be written to {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordServerFingerprints(Path knownServersFile) { + requireNonNull(knownServersFile); + return recordServerFingerprints(knownServersFile, true); + } + + /** + * Accept all server certificates, recording certificate fingerprints. + * + *

+ * For all connections, the server host+port and the fingerprint of the presented certificate will be written to + * {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @param skipCASigned If true, CA-signed certificates are not recorded. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordServerFingerprints(Path knownServersFile, boolean skipCASigned) { + requireNonNull(knownServersFile); + return wrap(HostFingerprintTrustManager.record(knownServersFile), skipCASigned); + } + + /** + * Accept all server certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a server presents a CA-signed certificate, the server host+port and the certificate fingerprint will + * be written to {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordServerFingerprints(Path knownServersFile, TrustManagerFactory tmf) { + requireNonNull(knownServersFile); + requireNonNull(tmf); + return wrap(HostFingerprintTrustManager.record(knownServersFile), tmf); + } + + /** + * Accept CA-signed certificates, and otherwise trust server certificates on first use. + * + *

+ * Except when a server presents a CA-signed certificate, on first connection to a server (identified by host+port) + * the fingerprint of the presented certificate will be recorded in {@code knownServersFile}. On subsequent + * connections, the presented certificate will be matched to the stored fingerprint to ensure it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @return A trust manager factory. + */ + public static TrustManagerFactory trustServerOnFirstUse(Path knownServersFile) { + requireNonNull(knownServersFile); + return trustServerOnFirstUse(knownServersFile, true); + } + + /** + * Trust server certificates on first use. + * + *

+ * On first connection to a server (identified by host+port) the fingerprint of the presented certificate will be + * recorded in {@code knownServersFile}. On subsequent connections, the presented certificate will be matched to the + * stored fingerprint to ensure it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param acceptCASigned If true, CA-signed certificates will always be accepted (and the fingerprint will + * not be recorded). + * @return A trust manager factory. + */ + public static TrustManagerFactory trustServerOnFirstUse(Path knownServersFile, boolean acceptCASigned) { + requireNonNull(knownServersFile); + return wrap(HostFingerprintTrustManager.tofu(knownServersFile), acceptCASigned); + } + + /** + * Accept CA-signed certificates, and otherwise trust server certificates on first use. + * + *

+ * Except when a server presents a CA-signed certificate, on first connection to a server (identified by host+port) + * the fingerprint of the presented certificate will be recorded in {@code knownServersFile}. On subsequent + * connections, the presented certificate will be matched to the stored fingerprint to ensure it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A trust manager factory. + */ + public static TrustManagerFactory trustServerOnFirstUse(Path knownServersFile, TrustManagerFactory tmf) { + requireNonNull(knownServersFile); + requireNonNull(tmf); + return wrap(HostFingerprintTrustManager.tofu(knownServersFile), tmf); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownServersFile}, associated + * with the server (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistServers(Path knownServersFile) { + requireNonNull(knownServersFile); + return whitelistServers(knownServersFile, true); + } + + /** + * Require servers to present known certificates. + * + *

+ * The fingerprint for a server certificate must be present in the {@code knownServersFile}, associated with the + * server (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param acceptCASigned If true, CA-signed certificates will always be accepted. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistServers(Path knownServersFile, boolean acceptCASigned) { + requireNonNull(knownServersFile); + return wrap(HostFingerprintTrustManager.whitelist(knownServersFile), acceptCASigned); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownServersFile}, associated + * with the server (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistServers(Path knownServersFile, TrustManagerFactory tmf) { + requireNonNull(knownServersFile); + requireNonNull(tmf); + return wrap(HostFingerprintTrustManager.whitelist(knownServersFile), tmf); + } + + /** + * Accept all client certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a client presents a CA-signed certificate, the certificate fingerprint will be written to + * {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordClientFingerprints(Path knownClientsFile) { + requireNonNull(knownClientsFile); + return recordClientFingerprints(knownClientsFile, true); + } + + /** + * Accept all client certificates, recording certificate fingerprints. + * + *

+ * For all connections, the fingerprint of the presented certificate will be written to {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @param skipCASigned If true, CA-signed certificates are not recorded. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordClientFingerprints(Path knownClientsFile, boolean skipCASigned) { + requireNonNull(knownClientsFile); + return wrap(FingerprintTrustManager.record(knownClientsFile), skipCASigned); + } + + /** + * Accept all client certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a client presents a CA-signed certificate, the certificate fingerprint will be written to + * {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A trust manager factory. + */ + public static TrustManagerFactory recordClientFingerprints(Path knownClientsFile, TrustManagerFactory tmf) { + requireNonNull(knownClientsFile); + requireNonNull(tmf); + return wrap(FingerprintTrustManager.record(knownClientsFile), tmf); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistClients(Path knownClientsFile) { + requireNonNull(knownClientsFile); + return whitelistClients(knownClientsFile, true); + } + + /** + * Require clients to present known certificates. + * + *

+ * The fingerprint for a client certificate must be present in {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @param acceptCASigned If true, CA-signed certificates will always be accepted. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistClients(Path knownClientsFile, boolean acceptCASigned) { + requireNonNull(knownClientsFile); + return wrap(FingerprintTrustManager.whitelist(knownClientsFile), acceptCASigned); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A trust manager factory. + */ + public static TrustManagerFactory whitelistClients(Path knownClientsFile, TrustManagerFactory tmf) { + requireNonNull(knownClientsFile); + requireNonNull(tmf); + return wrap(FingerprintTrustManager.whitelist(knownClientsFile), tmf); + } + + private static TrustManagerFactory wrap(X509TrustManager trustManager, boolean acceptCASigned) { + return wrap(trustManager, acceptCASigned ? defaultTrustManagerFactory() : null); + } + + private static TrustManagerFactory wrap(X509TrustManager trustManager, @Nullable TrustManagerFactory delegate) { + if (delegate != null) { + return new DelegatingTrustManagerFactory(delegate, trustManager); + } else { + return new SingleTrustManagerFactory(trustManager); + } + } + + private static TrustManagerFactory defaultTrustManagerFactory() { + TrustManagerFactory delegate; + try { + delegate = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (NoSuchAlgorithmException e) { + // not reachable + throw new RuntimeException(e); + } + try { + delegate.init((KeyStore) null); + } catch (KeyStoreException e) { + // not reachable + throw new RuntimeException(e); + } + return delegate; + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactoryWrapper.java b/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactoryWrapper.java new file mode 100644 index 00000000..a2f32b4d --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/TrustManagerFactoryWrapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import javax.net.ssl.TrustManagerFactory; + +import io.vertx.core.Vertx; +import io.vertx.core.net.TrustOptions; + +final class TrustManagerFactoryWrapper implements TrustOptions { + + private final TrustManagerFactory trustManagerFactory; + + TrustManagerFactoryWrapper(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + } + + @Override + public TrustOptions clone() { + return new TrustManagerFactoryWrapper(trustManagerFactory); + } + + @Override + public TrustManagerFactory getTrustManagerFactory(Vertx vertx) { + return trustManagerFactory; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TrustManagerFactoryWrapper)) { + return false; + } + TrustManagerFactoryWrapper other = (TrustManagerFactoryWrapper) obj; + return trustManagerFactory.equals(other.trustManagerFactory); + } + + @Override + public int hashCode() { + return trustManagerFactory.hashCode(); + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/VertxTrustOptions.java b/net/src/main/java/net/consensys/cava/net/tls/VertxTrustOptions.java new file mode 100644 index 00000000..3609864c --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/VertxTrustOptions.java @@ -0,0 +1,270 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import java.nio.file.Path; +import javax.net.ssl.TrustManagerFactory; + +import io.vertx.core.net.TrustOptions; + +/** + * Vert.x {@link TrustOptions} for fingerprinting clients and servers. + * + *

+ * This class depends upon the Vert.X library being available on the classpath, along with its dependencies. See + * https://vertx.io/download/. Vert.X can be included using the gradle dependency 'io.vertx:vertx-core'. + */ +public final class VertxTrustOptions { + private VertxTrustOptions() {} + + /** + * Accept all server certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a server presents a CA-signed certificate, the server host+port and the certificate fingerprint will + * be written to {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordServerFingerprints(Path knownServersFile) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.recordServerFingerprints(knownServersFile)); + } + + /** + * Accept all server certificates, recording certificate fingerprints. + * + *

+ * For all connections, the server host+port and the fingerprint of the presented certificate will be written to + * {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @param skipCASigned If true, CA-signed certificates are not recorded. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordServerFingerprints(Path knownServersFile, boolean skipCASigned) { + return new TrustManagerFactoryWrapper( + TrustManagerFactories.recordServerFingerprints(knownServersFile, skipCASigned)); + } + + /** + * Accept all server certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a server presents a CA-signed certificate, the server host+port and the certificate fingerprint will + * be written to {@code knownServersFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownServersFile The path to a file in which to record fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordServerFingerprints(Path knownServersFile, TrustManagerFactory tmf) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.recordServerFingerprints(knownServersFile, tmf)); + } + + /** + * Accept CA-signed certificates, and otherwise trust server certificates on first use. + * + *

+ * Except when a server presents a CA-signed certificate, on first connection to a server (identified by host+port) + * the fingerprint of the presented certificate will be recorded. On subsequent connections, the presented certificate + * will be matched to the stored fingerprint to ensure it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions trustServerOnFirstUse(Path knownServersFile) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.trustServerOnFirstUse(knownServersFile)); + } + + /** + * Trust server certificates on first use. + * + *

+ * On first connection to a server (identified by host+port) the fingerprint of the presented certificate will be + * recorded. On subsequent connections, the presented certificate will be matched to the stored fingerprint to ensure + * it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param acceptCASigned If true, CA-signed certificates will always be accepted (and the fingerprint will + * not be recorded). + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions trustServerOnFirstUse(Path knownServersFile, boolean acceptCASigned) { + return new TrustManagerFactoryWrapper( + TrustManagerFactories.trustServerOnFirstUse(knownServersFile, acceptCASigned)); + } + + /** + * Accept CA-signed certificates, and otherwise trust server certificates on first use. + * + *

+ * Except when a server presents a CA-signed certificate, on first connection to a server (identified by host+port) + * the fingerprint of the presented certificate will be recorded. On subsequent connections, the presented certificate + * will be matched to the stored fingerprint to ensure it has not changed. + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions trustServerOnFirstUse(Path knownServersFile, TrustManagerFactory tmf) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.trustServerOnFirstUse(knownServersFile, tmf)); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the known servers file, associated with + * the server (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistServers(Path knownServersFile) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistServers(knownServersFile)); + } + + /** + * Require servers to present known certificates. + * + *

+ * The fingerprint for a server certificate must be present in the known servers file, associated with the server + * (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param acceptCASigned If true, CA-signed certificates will always be accepted. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistServers(Path knownServersFile, boolean acceptCASigned) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistServers(knownServersFile, acceptCASigned)); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the known servers file, associated with + * the server (identified by host+port). + * + * @param knownServersFile The path to the file containing fingerprints by host. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistServers(Path knownServersFile, TrustManagerFactory tmf) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistServers(knownServersFile, tmf)); + } + + /** + * Accept all client certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a client presents a CA-signed certificate, the certificate fingerprint will be written to + * {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordClientFingerprints(Path knownClientsFile) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.recordClientFingerprints(knownClientsFile)); + } + + /** + * Accept all client certificates, recording certificate fingerprints. + * + *

+ * For all connections, the fingerprint of the presented certificate will be written to {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @param skipCASigned If true, CA-signed certificates are not recorded. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordClientFingerprints(Path knownClientsFile, boolean skipCASigned) { + return new TrustManagerFactoryWrapper( + TrustManagerFactories.recordClientFingerprints(knownClientsFile, skipCASigned)); + } + + /** + * Accept all client certificates, recording certificate fingerprints for those that are not CA-signed. + * + *

+ * Excepting when a client presents a CA-signed certificate, the certificate fingerprint will be written to + * {@code knownClientsFile}. + * + *

+ * Important: this provides no security as it is vulnerable to man-in-the-middle attacks. + * + * @param knownClientsFile The path to a file in which to record fingerprints. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions recordClientFingerprints(Path knownClientsFile, TrustManagerFactory tmf) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.recordClientFingerprints(knownClientsFile, tmf)); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistClients(Path knownClientsFile) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistClients(knownClientsFile)); + } + + /** + * Require clients to present known certificates. + * + *

+ * The fingerprint for a client certificate must be present in {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @param acceptCASigned If true, CA-signed certificates will always be accepted. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistClients(Path knownClientsFile, boolean acceptCASigned) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistClients(knownClientsFile, acceptCASigned)); + } + + /** + * Require servers to present known certificates, or CA-signed certificates. + * + *

+ * If a certificate is not CA-signed, then its fingerprint must be present in the {@code knownClientsFile}. + * + * @param knownClientsFile The path to the file containing fingerprints. + * @param tmf A {@link TrustManagerFactory} for checking server certificates against a CA. + * @return A Vert.x {@link TrustOptions}. + */ + public static TrustOptions whitelistClients(Path knownClientsFile, TrustManagerFactory tmf) { + return new TrustManagerFactoryWrapper(TrustManagerFactories.whitelistClients(knownClientsFile, tmf)); + } +} diff --git a/net/src/main/java/net/consensys/cava/net/tls/package-info.java b/net/src/main/java/net/consensys/cava/net/tls/package-info.java new file mode 100644 index 00000000..0a8a620a --- /dev/null +++ b/net/src/main/java/net/consensys/cava/net/tls/package-info.java @@ -0,0 +1,7 @@ +/** + * Utilities for doing fingerprint based TLS certificate checking. + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.net.tls; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrRecordTest.java b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrRecordTest.java new file mode 100644 index 00000000..9be42665 --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrRecordTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static net.consensys.cava.net.tls.SecurityTestUtils.startServer; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ClientCaOrRecordTest { + + private static String caValidFingerprint; + private static String fooFingerprint; + private static HttpServer caValidServer; + private static HttpServer fooServer; + private static HttpServer foobarServer; + + private Path knownServersFile; + private HttpClient client; + + @BeforeAll + static void startServers(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caSignedCert = SelfSignedCertificate.create("localhost"); + SecurityTestUtils.configureJDKTrustStore(tempDir, caSignedCert); + caValidFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(caSignedCert.keyCertOptions().getCertPath()))) + .asBytes()); + + caValidServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(caSignedCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(caValidServer); + + SelfSignedCertificate fooCert = SelfSignedCertificate.create("foo.com"); + fooFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(fooCert.keyCertOptions().getCertPath()))) + .asBytes()); + + fooServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(fooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(fooServer); + + foobarServer = vertx + .createHttpServer( + new HttpServerOptions().setSsl(true).setPemKeyCertOptions( + SelfSignedCertificate.create("foobar.com").keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(foobarServer); + } + + @BeforeEach + void setupClient(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownServersFile = tempDir.resolve("knownclients.txt"); + Files.write(knownServersFile, Collections.singletonList("#First line")); + + HttpClientOptions options = new HttpClientOptions(); + options + .setSsl(true) + .setTrustOptions(VertxTrustOptions.recordServerFingerprints(knownServersFile)) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + client = vertx.createHttpClient(options); + } + + @AfterEach + void cleanupClient() { + client.close(); + } + + @AfterAll + static void stopServers() { + caValidServer.close(); + fooServer.close(); + foobarServer.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldValidateUsingCertificate() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(1, knownServers.size(), "CA verified host should not have been recorded"); + assertEquals("#First line", knownServers.get(0)); + } + + @Test + void shouldFallbackToRecordingForInvalidName() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "127.0.0.1", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size(), String.join("\n", knownServers)); + assertEquals("#First line", knownServers.get(0)); + assertEquals("127.0.0.1:" + caValidServer.actualPort() + " " + caValidFingerprint, knownServers.get(1)); + } + + @Test + void shouldRecordMultipleHosts() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post(fooServer.actualPort(), "localhost", "/sample", response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size(), String.join("\n", knownServers)); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + fooServer.actualPort() + " " + fooFingerprint, knownServers.get(1)); + + CompletableFuture secondStatusCode = new CompletableFuture<>(); + client + .post( + foobarServer.actualPort(), + "localhost", + "/sample", + response -> secondStatusCode.complete(response.statusCode())) + .end(); + assertEquals((Integer) 200, secondStatusCode.join()); + + knownServers = Files.readAllLines(knownServersFile); + assertEquals(3, knownServers.size(), String.join("\n", knownServers)); + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrTofuTest.java b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrTofuTest.java new file mode 100644 index 00000000..3df14d7e --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrTofuTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static net.consensys.cava.net.tls.SecurityTestUtils.startServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ClientCaOrTofuTest { + + private static String caValidFingerprint; + private static HttpServer caValidServer; + private static String fooFingerprint; + private static HttpServer fooServer; + private static HttpServer otherFooServer; + + private Path knownServersFile; + private HttpClient client; + + @BeforeAll + static void startServers(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caSignedCert = SelfSignedCertificate.create("localhost"); + SecurityTestUtils.configureJDKTrustStore(tempDir, caSignedCert); + caValidFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(caSignedCert.keyCertOptions().getCertPath()))) + .asBytes()); + + caValidServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(caSignedCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(caValidServer); + + SelfSignedCertificate fooCert = SelfSignedCertificate.create("foo.com"); + fooFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(fooCert.keyCertOptions().getCertPath()))) + .asBytes()); + + fooServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(fooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(fooServer); + + SelfSignedCertificate otherFooCert = SelfSignedCertificate.create("foo.com"); + otherFooServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(otherFooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(otherFooServer); + } + + @BeforeEach + void setupClient(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownServersFile = tempDir.resolve("knownclients.txt"); + Files.deleteIfExists(knownServersFile); + Files.write(knownServersFile, Collections.singletonList("#First line")); + + HttpClientOptions options = new HttpClientOptions(); + options + .setSsl(true) + .setTrustOptions(VertxTrustOptions.trustServerOnFirstUse(knownServersFile)) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + client = vertx.createHttpClient(options); + } + + @AfterEach + void cleanupClient() { + client.close(); + } + + @AfterAll + static void stopServers() { + caValidServer.close(); + fooServer.close(); + otherFooServer.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldValidateUsingCertificate() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(1, knownServers.size(), "Host was verified via TOFU and not CA"); + assertEquals("#First line", knownServers.get(0)); + } + + @Test + void shouldFallbackToTOFUForInvalidName() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "127.0.0.1", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size()); + assertEquals("#First line", knownServers.get(0)); + assertEquals("127.0.0.1:" + caValidServer.actualPort() + " " + caValidFingerprint, knownServers.get(1)); + } + + @Test + void shouldValidateOnFirstUse() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post(fooServer.actualPort(), "localhost", "/sample", response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size()); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + fooServer.actualPort() + " " + fooFingerprint, knownServers.get(1)); + } + + @Test + void shouldRejectDifferentCertificate() throws Throwable { + // do a first connection + shouldValidateOnFirstUse(); + + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + otherFooServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + try { + statusCode.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrWhitelistTest.java b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrWhitelistTest.java new file mode 100644 index 00000000..62f7cfe1 --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ClientCaOrWhitelistTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static net.consensys.cava.net.tls.SecurityTestUtils.startServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ClientCaOrWhitelistTest { + + private static String whitelistedFingerprint; + private static HttpServer caValidServer; + private static HttpServer whitelistedServer; + private static HttpServer unknownServer; + + private Path knownServersFile; + private HttpClient client; + + @BeforeAll + static void startServers(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caSignedCert = SelfSignedCertificate.create("localhost"); + SecurityTestUtils.configureJDKTrustStore(tempDir, caSignedCert); + caValidServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(caSignedCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(caValidServer); + + SelfSignedCertificate fooCert = SelfSignedCertificate.create("foo.com"); + whitelistedFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(fooCert.keyCertOptions().getCertPath()))) + .asBytes()); + + whitelistedServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(fooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(whitelistedServer); + + SelfSignedCertificate unknownCert = SelfSignedCertificate.create(); + unknownServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(unknownCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(unknownServer); + } + + @BeforeEach + void setupClient(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownServersFile = tempDir.resolve("knownclients.txt"); + Files.write( + knownServersFile, + Arrays.asList("#First line", "localhost:" + whitelistedServer.actualPort() + " " + whitelistedFingerprint)); + + HttpClientOptions options = new HttpClientOptions(); + options + .setSsl(true) + .setTrustOptions(VertxTrustOptions.whitelistServers(knownServersFile)) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + client = vertx.createHttpClient(options); + } + + @AfterEach + void cleanupClient() throws Exception { + client.close(); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size(), "Host was verified via TOFU and not CA"); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + whitelistedServer.actualPort() + " " + whitelistedFingerprint, knownServers.get(1)); + } + + @AfterAll + static void stopServers() { + caValidServer.close(); + whitelistedServer.close(); + unknownServer.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldValidateUsingCertificate() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + } + + @Test + void shouldValidateWhitelisted() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + whitelistedServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + } + + @Test + void shouldRejectNonWhitelisted() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + unknownServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + try { + statusCode.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ClientTofuTest.java b/net/src/test/java/net/consensys/cava/net/tls/ClientTofuTest.java new file mode 100644 index 00000000..1f32d1cf --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ClientTofuTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static net.consensys.cava.net.tls.SecurityTestUtils.startServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ClientTofuTest { + + private static String caValidFingerprint; + private static String fooFingerprint; + private static HttpServer caValidServer; + private static HttpServer fooServer; + private static HttpServer otherFooServer; + + private Path knownServersFile; + private HttpClient client; + + @BeforeAll + static void startServers(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caSignedCert = SelfSignedCertificate.create("localhost"); + SecurityTestUtils.configureJDKTrustStore(tempDir, caSignedCert); + caValidFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(caSignedCert.keyCertOptions().getCertPath()))) + .asBytes()); + + caValidServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(caSignedCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(caValidServer); + + SelfSignedCertificate fooCert = SelfSignedCertificate.create("foo.com"); + fooFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(fooCert.keyCertOptions().getCertPath()))) + .asBytes()); + + fooServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(fooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(fooServer); + + SelfSignedCertificate otherFooCert = SelfSignedCertificate.create("foo.com"); + otherFooServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(otherFooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(otherFooServer); + } + + @BeforeEach + void setupClient(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownServersFile = tempDir.resolve("knownclients.txt"); + Files.deleteIfExists(knownServersFile); + Files.write(knownServersFile, Collections.singletonList("#First line")); + + HttpClientOptions options = new HttpClientOptions(); + options + .setSsl(true) + .setTrustOptions(VertxTrustOptions.trustServerOnFirstUse(knownServersFile, false)) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + client = vertx.createHttpClient(options); + } + + @AfterEach + void cleanupClient() { + client.close(); + } + + @AfterAll + static void stopServers() { + caValidServer.close(); + fooServer.close(); + otherFooServer.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldNotValidateUsingCertificate() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size()); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + caValidServer.actualPort() + " " + caValidFingerprint, knownServers.get(1)); + } + + @Test + void shouldValidateOnFirstUse() throws Exception { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post(fooServer.actualPort(), "localhost", "/sample", response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size()); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + fooServer.actualPort() + " " + fooFingerprint, knownServers.get(1)); + } + + @Test + void shouldRejectDifferentCertificate() throws Throwable { + // do a first connection + shouldValidateOnFirstUse(); + + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + otherFooServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + try { + statusCode.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ClientWhitelistTest.java b/net/src/test/java/net/consensys/cava/net/tls/ClientWhitelistTest.java new file mode 100644 index 00000000..30b6ac7c --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ClientWhitelistTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static net.consensys.cava.net.tls.SecurityTestUtils.startServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ClientWhitelistTest { + + private static String whitelistedFingerprint; + private static HttpServer caValidServer; + private static HttpServer whitelistedServer; + private static HttpServer unknownServer; + + private Path knownServersFile; + private HttpClient client; + + @BeforeAll + static void startServers(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caSignedCert = SelfSignedCertificate.create("localhost"); + SecurityTestUtils.configureJDKTrustStore(tempDir, caSignedCert); + caValidServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(caSignedCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(caValidServer); + + SelfSignedCertificate fooCert = SelfSignedCertificate.create("foo.com"); + whitelistedFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(fooCert.keyCertOptions().getCertPath()))) + .asBytes()); + + whitelistedServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(fooCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(whitelistedServer); + + SelfSignedCertificate unknownCert = SelfSignedCertificate.create(); + unknownServer = vertx + .createHttpServer(new HttpServerOptions().setSsl(true).setPemKeyCertOptions(unknownCert.keyCertOptions())) + .requestHandler(context -> context.response().end("OK")); + startServer(unknownServer); + } + + @BeforeEach + void setupClient(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownServersFile = tempDir.resolve("knownclients.txt"); + Files.write( + knownServersFile, + Arrays.asList("#First line", "localhost:" + whitelistedServer.actualPort() + " " + whitelistedFingerprint)); + + HttpClientOptions options = new HttpClientOptions(); + options + .setSsl(true) + .setTrustOptions(VertxTrustOptions.whitelistServers(knownServersFile, false)) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + client = vertx.createHttpClient(options); + } + + @AfterEach + void cleanupClient() throws Exception { + client.close(); + + List knownServers = Files.readAllLines(knownServersFile); + assertEquals(2, knownServers.size(), "Host was verified via TOFU and not CA"); + assertEquals("#First line", knownServers.get(0)); + assertEquals("localhost:" + whitelistedServer.actualPort() + " " + whitelistedFingerprint, knownServers.get(1)); + } + + @AfterAll + static void stopServers() { + caValidServer.close(); + whitelistedServer.close(); + unknownServer.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldNotValidateUsingCertificate() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + caValidServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + try { + statusCode.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } + + @Test + void shouldValidateWhitelisted() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + whitelistedServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + assertEquals((Integer) 200, statusCode.join()); + } + + @Test + void shouldRejectNonWhitelisted() { + CompletableFuture statusCode = new CompletableFuture<>(); + client + .post( + unknownServer.actualPort(), + "localhost", + "/sample", + response -> statusCode.complete(response.statusCode())) + .exceptionHandler(statusCode::completeExceptionally) + .end(); + try { + statusCode.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/InsecureTrustOptions.java b/net/src/test/java/net/consensys/cava/net/tls/InsecureTrustOptions.java new file mode 100644 index 00000000..fabee38a --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/InsecureTrustOptions.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import javax.net.ssl.TrustManagerFactory; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.vertx.core.Vertx; +import io.vertx.core.net.TrustOptions; + +final class InsecureTrustOptions implements TrustOptions { + + static InsecureTrustOptions INSTANCE = new InsecureTrustOptions(); + + private InsecureTrustOptions() {} + + @Override + public TrustOptions clone() { + return this; + } + + @Override + public TrustManagerFactory getTrustManagerFactory(Vertx vertx) { + return InsecureTrustManagerFactory.INSTANCE; + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/SecurityTestUtils.java b/net/src/test/java/net/consensys/cava/net/tls/SecurityTestUtils.java new file mode 100644 index 00000000..0ff26f7d --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/SecurityTestUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +import io.vertx.core.http.HttpServer; +import io.vertx.core.net.SelfSignedCertificate; +import org.bouncycastle.util.encoders.Base64; + +class SecurityTestUtils { + private SecurityTestUtils() {} + + static void configureJDKTrustStore(Path workDir, SelfSignedCertificate clientCert) throws Exception { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(loadPEM(new File(clientCert.privateKeyPath()).toPath())); + PrivateKey clientPrivateKey = kf.generatePrivate(keysp); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate certificate = cf.generateCertificate( + new ByteArrayInputStream(Files.readAllBytes(new File(clientCert.certificatePath()).toPath()))); + ks.setCertificateEntry("clientCert", certificate); + ks.setKeyEntry("client", clientPrivateKey, "changeit".toCharArray(), new Certificate[] {certificate}); + Path tempKeystore = Files.createTempFile(workDir, "keystore", ".jks"); + try (FileOutputStream output = new FileOutputStream(tempKeystore.toFile());) { + ks.store(output, "changeit".toCharArray()); + } + System.setProperty("javax.net.ssl.trustStore", tempKeystore.toString()); + System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + } + + static byte[] loadPEM(Path pemFilePath) throws IOException { + String pem = new String(Files.readAllBytes(pemFilePath), UTF_8); + Pattern parse = Pattern.compile("(?m)(?s)^---*BEGIN.*---*$(.*)^---*END.*---*$.*"); + String encoded = parse.matcher(pem).replaceFirst("$1").replace("\n", ""); + return Base64.decode(encoded); + } + + static void configureAndStartTestServer(HttpServer httpServer) { + httpServer.requestHandler(request -> { + request.response().setStatusCode(200).end("OK"); + }); + startServer(httpServer); + } + + static void startServer(HttpServer server) { + CompletableFuture future = new CompletableFuture<>(); + server.listen(0, result -> { + if (result.succeeded()) { + future.complete(true); + } else { + future.completeExceptionally(result.cause()); + } + }); + future.join(); + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrRecordTest.java b/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrRecordTest.java new file mode 100644 index 00000000..353f662b --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrRecordTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ServerCaOrRecordTest { + + private static HttpClient caClient; + private static String unknownClientFingerprint; + private static HttpClient unknownClient1; + private static HttpClient unknownClient2; + + private Path knownClientsFile; + private HttpServer httpServer; + + @BeforeAll + static void setupClients(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caClientCert = SelfSignedCertificate.create(); + SecurityTestUtils.configureJDKTrustStore(tempDir, caClientCert); + caClient = vertx.createHttpClient( + new HttpClientOptions().setTrustOptions(InsecureTrustOptions.INSTANCE).setSsl(true).setKeyCertOptions( + caClientCert.keyCertOptions())); + + SelfSignedCertificate nonCAClientCert = SelfSignedCertificate.create(); + unknownClientFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(nonCAClientCert.keyCertOptions().getCertPath()))) + .asBytes()); + + HttpClientOptions unknownClient1Options = new HttpClientOptions(); + unknownClient1Options + .setSsl(true) + .setKeyCertOptions(nonCAClientCert.keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient1 = vertx.createHttpClient(unknownClient1Options); + + HttpClientOptions unknownClient2Options = new HttpClientOptions(); + unknownClient2Options + .setSsl(true) + .setKeyCertOptions(SelfSignedCertificate.create().keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient2 = vertx.createHttpClient(unknownClient2Options); + } + + @BeforeEach + void startServer(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownClientsFile = tempDir.resolve("knownclients.txt"); + Files.write(knownClientsFile, Collections.singletonList("#First line")); + + SelfSignedCertificate serverCert = SelfSignedCertificate.create(); + HttpServerOptions options = new HttpServerOptions(); + options + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setPemKeyCertOptions(serverCert.keyCertOptions()) + .setTrustOptions(VertxTrustOptions.recordClientFingerprints(knownClientsFile)) + .setIdleTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + httpServer = vertx.createHttpServer(options); + SecurityTestUtils.configureAndStartTestServer(httpServer); + } + + @AfterEach + void stopServer() { + httpServer.close(); + } + + @AfterAll + static void cleanupClients() { + caClient.close(); + unknownClient1.close(); + unknownClient2.close(); + } + + @Test + void shouldValidateUsingCertificate() throws Exception { + HttpClientRequest req = caClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(1, knownClients.size(), "CA verified host should not have been recorded"); + assertEquals("#First line", knownClients.get(0)); + } + + @Test + void shouldRecordMultipleFingerprints() throws Exception { + HttpClientRequest req = unknownClient1.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(2, knownClients.size(), String.join("\n", knownClients)); + assertEquals("#First line", knownClients.get(0)); + assertEquals(unknownClientFingerprint, knownClients.get(1)); + + req = unknownClient2.get(httpServer.actualPort(), "localhost", "/upcheck"); + respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + knownClients = Files.readAllLines(knownClientsFile); + assertEquals(3, knownClients.size(), String.join("\n", knownClients)); + assertEquals("#First line", knownClients.get(0)); + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrWhitelistTest.java b/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrWhitelistTest.java new file mode 100644 index 00000000..0a392dc8 --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ServerCaOrWhitelistTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ServerCaOrWhitelistTest { + + private static HttpClient caClient; + private static String whitelistedFingerprint; + private static HttpClient whitelistClient; + private static HttpClient unknownClient; + + private Path knownClientsFile; + private HttpServer httpServer; + + @BeforeAll + static void setupClients(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caClientCert = SelfSignedCertificate.create(); + SecurityTestUtils.configureJDKTrustStore(tempDir, caClientCert); + caClient = vertx.createHttpClient( + new HttpClientOptions().setTrustOptions(InsecureTrustOptions.INSTANCE).setSsl(true).setKeyCertOptions( + caClientCert.keyCertOptions())); + + SelfSignedCertificate nonCAClientCert = SelfSignedCertificate.create(); + whitelistedFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(nonCAClientCert.keyCertOptions().getCertPath()))) + .asBytes()); + + HttpClientOptions whitelistClientOptions = new HttpClientOptions(); + whitelistClientOptions + .setSsl(true) + .setKeyCertOptions(nonCAClientCert.keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + whitelistClient = vertx.createHttpClient(whitelistClientOptions); + + HttpClientOptions unknownClientOptions = new HttpClientOptions(); + unknownClientOptions + .setSsl(true) + .setKeyCertOptions(SelfSignedCertificate.create().keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient = vertx.createHttpClient(unknownClientOptions); + } + + @BeforeEach + void startServer(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownClientsFile = tempDir.resolve("knownclients.txt"); + Files.write(knownClientsFile, whitelistedFingerprint.getBytes(UTF_8)); + + SelfSignedCertificate serverCert = SelfSignedCertificate.create(); + HttpServerOptions options = new HttpServerOptions(); + options + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setPemKeyCertOptions(serverCert.keyCertOptions()) + .setTrustOptions(VertxTrustOptions.whitelistClients(knownClientsFile)) + .setIdleTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + httpServer = vertx.createHttpServer(options); + SecurityTestUtils.configureAndStartTestServer(httpServer); + } + + @AfterEach + void stopServer() throws Exception { + httpServer.close(); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(1, knownClients.size()); + assertEquals(whitelistedFingerprint, knownClients.get(0)); + } + + @AfterAll + static void cleanupClients() { + caClient.close(); + whitelistClient.close(); + unknownClient.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldValidateUsingCertificate() { + HttpClientRequest req = caClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + } + + @Test + void shouldValidateWhitelisted() { + HttpClientRequest req = whitelistClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + } + + @Test + void shouldRejectNonWhitelisted() { + HttpClientRequest req = unknownClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + try { + respFuture.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ServerRecordTest.java b/net/src/test/java/net/consensys/cava/net/tls/ServerRecordTest.java new file mode 100644 index 00000000..6eed0b6e --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ServerRecordTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ServerRecordTest { + + private static String caClientFingerprint; + private static HttpClient caClient; + private static String unknownClientFingerprint; + private static HttpClient unknownClient1; + private static HttpClient unknownClient2; + + private Path knownClientsFile; + private HttpServer httpServer; + + @BeforeAll + static void setupClients(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caClientCert = SelfSignedCertificate.create(); + SecurityTestUtils.configureJDKTrustStore(tempDir, caClientCert); + caClientFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(caClientCert.keyCertOptions().getCertPath()))) + .asBytes()); + + caClient = vertx.createHttpClient( + new HttpClientOptions().setTrustOptions(InsecureTrustOptions.INSTANCE).setSsl(true).setKeyCertOptions( + caClientCert.keyCertOptions())); + + SelfSignedCertificate nonCAClientCert = SelfSignedCertificate.create(); + unknownClientFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(nonCAClientCert.keyCertOptions().getCertPath()))) + .asBytes()); + + HttpClientOptions unknownClient1Options = new HttpClientOptions(); + unknownClient1Options + .setSsl(true) + .setKeyCertOptions(nonCAClientCert.keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient1 = vertx.createHttpClient(unknownClient1Options); + + HttpClientOptions unknownClient2Options = new HttpClientOptions(); + unknownClient2Options + .setSsl(true) + .setKeyCertOptions(SelfSignedCertificate.create().keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient2 = vertx.createHttpClient(unknownClient2Options); + } + + @BeforeEach + void startServer(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownClientsFile = tempDir.resolve("knownclients.txt"); + Files.write(knownClientsFile, Collections.singletonList("#First line")); + + SelfSignedCertificate serverCert = SelfSignedCertificate.create(); + HttpServerOptions options = new HttpServerOptions(); + options + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setPemKeyCertOptions(serverCert.keyCertOptions()) + .setTrustOptions(VertxTrustOptions.recordClientFingerprints(knownClientsFile, false)) + .setIdleTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + httpServer = vertx.createHttpServer(options); + SecurityTestUtils.configureAndStartTestServer(httpServer); + } + + @AfterEach + void stopServer() { + httpServer.close(); + } + + @AfterAll + static void cleanupClients() { + caClient.close(); + unknownClient1.close(); + unknownClient2.close(); + } + + @Test + void shouldNotValidateUsingCertificate() throws Exception { + HttpClientRequest req = caClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(2, knownClients.size(), String.join("\n", knownClients)); + assertEquals("#First line", knownClients.get(0)); + assertEquals(caClientFingerprint, knownClients.get(1)); + } + + @Test + void shouldRecordMultipleFingerprints() throws Exception { + HttpClientRequest req = unknownClient1.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(2, knownClients.size(), String.join("\n", knownClients)); + assertEquals("#First line", knownClients.get(0)); + assertEquals(unknownClientFingerprint, knownClients.get(1)); + + req = unknownClient2.get(httpServer.actualPort(), "localhost", "/upcheck"); + respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + + knownClients = Files.readAllLines(knownClientsFile); + assertEquals(3, knownClients.size(), String.join("\n", knownClients)); + assertEquals("#First line", knownClients.get(0)); + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/ServerWhitelistTest.java b/net/src/test/java/net/consensys/cava/net/tls/ServerWhitelistTest.java new file mode 100644 index 00000000..7ad941d6 --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/ServerWhitelistTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; +import net.consensys.cava.junit.VertxExtension; +import net.consensys.cava.junit.VertxInstance; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.net.ssl.SSLException; + +import com.google.common.hash.Hashing; +import io.netty.util.internal.StringUtil; +import io.vertx.core.Vertx; +import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(VertxExtension.class) +class ServerWhitelistTest { + + private static HttpClient caClient; + private static String whitelistedFingerprint; + private static HttpClient whitelistClient; + private static HttpClient unknownClient; + + private Path knownClientsFile; + private HttpServer httpServer; + + @BeforeAll + static void setupClients(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + SelfSignedCertificate caClientCert = SelfSignedCertificate.create(); + SecurityTestUtils.configureJDKTrustStore(tempDir, caClientCert); + caClient = vertx.createHttpClient( + new HttpClientOptions().setTrustOptions(InsecureTrustOptions.INSTANCE).setSsl(true).setKeyCertOptions( + caClientCert.keyCertOptions())); + + SelfSignedCertificate nonCAClientCert = SelfSignedCertificate.create(); + whitelistedFingerprint = StringUtil.toHexStringPadded( + Hashing + .sha256() + .hashBytes(SecurityTestUtils.loadPEM(Paths.get(nonCAClientCert.keyCertOptions().getCertPath()))) + .asBytes()); + + HttpClientOptions whitelistClientOptions = new HttpClientOptions(); + whitelistClientOptions + .setSsl(true) + .setKeyCertOptions(nonCAClientCert.keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + whitelistClient = vertx.createHttpClient(whitelistClientOptions); + + HttpClientOptions unknownClientOptions = new HttpClientOptions(); + unknownClientOptions + .setSsl(true) + .setKeyCertOptions(SelfSignedCertificate.create().keyCertOptions()) + .setTrustOptions(InsecureTrustOptions.INSTANCE) + .setConnectTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + unknownClient = vertx.createHttpClient(unknownClientOptions); + } + + @BeforeEach + void startServer(@TempDirectory Path tempDir, @VertxInstance Vertx vertx) throws Exception { + knownClientsFile = tempDir.resolve("knownclients.txt"); + Files.write(knownClientsFile, whitelistedFingerprint.getBytes(UTF_8)); + + SelfSignedCertificate serverCert = SelfSignedCertificate.create(); + HttpServerOptions options = new HttpServerOptions(); + options + .setSsl(true) + .setClientAuth(ClientAuth.REQUIRED) + .setPemKeyCertOptions(serverCert.keyCertOptions()) + .setTrustOptions(VertxTrustOptions.whitelistClients(knownClientsFile, false)) + .setIdleTimeout(1500) + .setReuseAddress(true) + .setReusePort(true); + httpServer = vertx.createHttpServer(options); + SecurityTestUtils.configureAndStartTestServer(httpServer); + } + + @AfterEach + void stopServer() throws Exception { + httpServer.close(); + + List knownClients = Files.readAllLines(knownClientsFile); + assertEquals(1, knownClients.size()); + assertEquals(whitelistedFingerprint, knownClients.get(0)); + } + + @AfterAll + static void cleanupClients() { + caClient.close(); + whitelistClient.close(); + unknownClient.close(); + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + @Test + void shouldNotValidateUsingCertificate() { + HttpClientRequest req = caClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + try { + respFuture.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } + + @Test + void shouldValidateWhitelisted() { + HttpClientRequest req = whitelistClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + HttpClientResponse resp = respFuture.join(); + assertEquals(200, resp.statusCode()); + } + + @Test + void shouldRejectNonWhitelisted() { + HttpClientRequest req = unknownClient.get(httpServer.actualPort(), "localhost", "/upcheck"); + CompletableFuture respFuture = new CompletableFuture<>(); + req.handler(respFuture::complete).exceptionHandler(respFuture::completeExceptionally).end(); + try { + respFuture.join(); + } catch (CompletionException e) { + assertTrue(e.getCause() instanceof SSLException); + } + } +} diff --git a/net/src/test/java/net/consensys/cava/net/tls/TLSTest.java b/net/src/test/java/net/consensys/cava/net/tls/TLSTest.java new file mode 100644 index 00000000..aca91474 --- /dev/null +++ b/net/src/test/java/net/consensys/cava/net/tls/TLSTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.net.tls; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.junit.BouncyCastleExtension; +import net.consensys.cava.junit.TempDirectory; +import net.consensys.cava.junit.TempDirectoryExtension; + +import java.io.ByteArrayInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(BouncyCastleExtension.class) +class TLSTest { + + @Test + void createCertificateIfFilesAreNotThere(@TempDirectory Path tempDir) throws Exception { + Path certificate = tempDir.resolve("foo").resolve("server.crt"); + Path key = tempDir.resolve("foo").resolve("server.key"); + + TLS.createSelfSignedCertificateIfMissing(key, certificate); + + assertTrue(Files.exists(key)); + assertTrue(Files.exists(certificate)); + } + + @Test + void autoGeneratedCertsAreValid(@TempDirectory Path tempDir) throws Exception { + Path certificate = tempDir.resolve("server.crt"); + Path key = tempDir.resolve("server.key"); + + TLS.createSelfSignedCertificateIfMissing(key, certificate); + + checkKeyPair(key, certificate); + } + + private void checkKeyPair(Path key, Path cert) throws Exception { + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(SecurityTestUtils.loadPEM(key)); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate certificate = cf.generateCertificate(new ByteArrayInputStream(Files.readAllBytes(cert))); + KeyFactory kf = KeyFactory.getInstance("RSA"); + KeyPair keyPair = new KeyPair(certificate.getPublicKey(), kf.generatePrivate(pkcs8KeySpec)); + + byte[] challenge = new byte[10000]; + ThreadLocalRandom.current().nextBytes(challenge); + + // sign using the private key + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(keyPair.getPrivate()); + sig.update(challenge); + byte[] signature = sig.sign(); + + // verify signature using the public key + sig.initVerify(keyPair.getPublic()); + sig.update(challenge); + + assertTrue(sig.verify(signature)); + } +} diff --git a/release/.gitignore b/release/.gitignore new file mode 100644 index 00000000..f2668578 --- /dev/null +++ b/release/.gitignore @@ -0,0 +1 @@ +release.config \ No newline at end of file diff --git a/release/Dockerfile b/release/Dockerfile new file mode 100644 index 00000000..2fc91c90 --- /dev/null +++ b/release/Dockerfile @@ -0,0 +1,41 @@ +FROM ubuntu:18.04 as intermediate + +RUN apt-get update && apt-get install -y git openjdk-8-jdk gpg && apt-get clean + +ARG git_email +ARG git_name +ARG gpg_key + +RUN git config --global user.name $git_name \ + && git config --global user.email $git_email \ + && git config --global user.signingkey $gpg_key + +RUN mkdir -p /root/.ssh && touch /root/.ssh/known_hosts +RUN ssh-keyscan github.com >> /root/.ssh/known_hosts + +RUN echo "signing.keyId=$gpg_key\n\ + signing.gnupg.executable=gpg\n\ + signing.gnupg.useLegacyGpg=true\n\ + signing.gnupg.keyName=$gpg_key\n"\ + > /gradle.properties + +FROM ubuntu +COPY --from=intermediate /usr/lib /usr/lib +COPY --from=intermediate /usr/bin /usr/bin +COPY --from=intermediate /usr/local /usr/local +COPY --from=intermediate /usr/share/git-core/templates /usr/share/git-core/templates + +COPY --from=intermediate /etc /etc +COPY --from=intermediate /lib /lib + +COPY --from=intermediate /root/.ssh /root/.ssh + +COPY /checkout /checkout +COPY /run_release.sh /checkout +COPY --from=intermediate /gradle.properties /checkout + +COPY --from=intermediate /root/.gitconfig /root/.gitconfig + +WORKDIR "/checkout" + +ENTRYPOINT ["./run_release.sh"] \ No newline at end of file diff --git a/release/README.md b/release/README.md new file mode 100644 index 00000000..59735566 --- /dev/null +++ b/release/README.md @@ -0,0 +1,88 @@ +1. Install Docker on your machine +1.1. Share the location of your home folder (the program will mount your .gnupg and .ssh folders) + +2. Install gpg on your machine + +2.1. Edit the gpg agent to ensure pin entries use the tty: +> ~/.gnupg/gpg-agent.conf + +Add the line: `pinentry-program /usr/local/bin/pinentry-tty` (adapt depending on where the program is located) + +2.2. Reload gpg-agent: + +> gpg-connect-agent reloadagent /bye + +3. Install gpg-agent-forward + +3.1 Generate a SSH key/pair to ssh forward the gpg-agent. Give it a name such as 'releasekey' + +> ssh-keygen -t rsa + +Add the resulting identity to your ssh agent trusted identities: + +> ssh-add -K + +3.2. Follow the instructions at https://github.com/transifex/docker-gpg-agent-forward, reproduced here: + +> git clone git://github.com/transifex/docker-gpg-agent-forward +> cd docker-gpg-agent-forward +> make +> make install + +The script runs `pinata-gpg-forward` for you. + +4. Create your release configuration + +4.1. Copy the release.config.sample file into release.config + +> cp release.config.sample release.config + +Fill in the information in release.config + +5. Insert your Yubikey if you haven't already + +5.1. Prepare the Yubikey and cache its PIN (it won't work during the script execution): + +> gpg --status-fd=2 -bsau + +Enter some text, press ^D +Enter your PIN, then touch the key if needed. +Repeat. Notice the PIN prompt should not show. + +6. run the release + +> ./release.sh + +6.1. Follow the prompts and enter your Yubikey PINs when prompted + +7. Check the release took place + +7.1. Check the website was updated. + +7.2. Check bintray to make sure the files are there. + +You may need to OK the publication of the files. + +Check the .asc files are there. + +Download the main distribution jar and its associated .asc file. + +> gpg --verify .asc + +If this is the first time, from the output, copy the RSA key used for signing and drop it in: + +> gpg --keyserver pgp.mit.edu --recv-key + +Then run again: + +> gpg --verify .asc + +7.3. Check the tag on github. + +8. Prepare next iteration + +8.1. Edit the README with the new version (both the download badge and the Maven coordinates) + +8.2. Change the version in build.gradle + +8.3. Commit and push to the version branch. diff --git a/release/release.config.sample b/release/release.config.sample new file mode 100644 index 00000000..4dbdd6ed --- /dev/null +++ b/release/release.config.sample @@ -0,0 +1,22 @@ +# URI of the git repository +git_repo= +# Branch of the git repository to check out +git_branch= +#Location of SSH private key to access the git repository +ssh_private_key= +#Location of SSH public key to access the git repository +ssh_public_key= +#GPG signing key ID +signing_key_id= +#Git author name +git_name= +#Git author email +git_email= +#Bintray user +bintray_user= +#Bintray key +bintray_key= +#Git tag name +tag_name= +#Git tag comment +tag_comment= \ No newline at end of file diff --git a/release/release.sh b/release/release.sh new file mode 100755 index 00000000..92fb65d5 --- /dev/null +++ b/release/release.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -ue + +if ! ssh-add -l > /dev/null; then + echo "No identities in ssh-agent (run ssh-add?)" >&2 + exit 1 +fi + +pinata-gpg-forward + +workingdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +if [ ! -f $workingdir/release.config ] +then + echo "$workingdir/release.config is missing. Exiting now." + exit 1 +fi + +source $workingdir/release.config + +builddir=$(mktemp -d $TMPDIR/release-XXXXXX) +trap 'rm -rf $builddir' EXIT + +rm -Rf $builddir/build-checkout +mkdir $builddir/build-checkout +git clone $git_repo $builddir/checkout +exitstatus=$? + +if [ $exitstatus != 0 ]; then + echo "Error while checking out the git repository" + exit 1 +fi + +pushd $builddir/checkout +git checkout $git_branch +exitstatus=$? + +if [ $exitstatus != 0 ]; then + echo "Error while checking out the branch" + exit 1 +fi + +git submodule update --init --recursive +exitstatus=$? + +if [ $exitstatus != 0 ]; then + echo "Error while updating submodules" + exit 1 +fi + +popd + +cat <<-EOF > $builddir/run_release.sh +#!/bin/bash + +set -xe +echo "running release now" +chmod og-rwx /root/.gnupg +export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF8" +export GPG_TTY=/dev/console +export TMPDIR=/tmp +git tag -a $tag_name -m "$tag_comment" -s +./gradlew build :dokka :javadoc +./gradlew sign +./gradlew deploy publishSite +git push origin $git_branch --tags +EOF +chmod +x $builddir/run_release.sh + +docker build -t release_image:1.0 -f $workingdir/Dockerfile --build-arg git_name="$git_name" --build-arg git_email="$git_email" --build-arg gpg_key="$signing_key_id" $builddir +exitstatus=$? + +if [ $exitstatus != 0 ]; then + echo "Error while building the release image" + exit 1 +fi + +docker run -it -v $ssh_private_key:/root/.ssh/id_rsa \ + -v $ssh_public_key:/root/.ssh/id_rsa.pub \ + -v gpg-agent:/root/.gnupg \ + -e BUILD_TAG_MODE="release" \ + -e BINTRAY_USER=$bintray_user \ + -e BINTRAY_KEY=$bintray_key \ + -e ENABLE_SIGNING=true \ + -v ~/.gradle/caches:/root/.gradle/caches \ + release_image:1.0 + +# Remove docker pgp-agent: +docker rm -f "pinata-gpg-agent" >/dev/null 2>&1 || true diff --git a/rlp/build.gradle b/rlp/build.gradle new file mode 100644 index 00000000..e8390de0 --- /dev/null +++ b/rlp/build.gradle @@ -0,0 +1,15 @@ +description = 'Recursive Length Prefix (RLP) encoding and decoding.' + +dependencies { + compile project(':bytes') + compileOnly project(':units') + + testCompile project(':units') + testCompile 'com.winterbe:expekt' + testCompile 'org.jetbrains.spek:spek-api' + testCompile 'org.junit.jupiter:junit-jupiter-api' + testRuntime 'org.junit.jupiter:junit-jupiter-engine' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.jetbrains.spek:spek-junit-platform-engine' +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPReader.java b/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPReader.java new file mode 100644 index 00000000..75ea6382 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPReader.java @@ -0,0 +1,242 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +import net.consensys.cava.bytes.Bytes; + +import java.util.function.Function; + +final class BytesValueRLPReader implements RLPReader { + + private final Bytes content; + private int index = 0; + + BytesValueRLPReader(Bytes content) { + this.content = content; + } + + @Override + public Bytes readValue() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0x7f) { + return content.slice(index++, 1); + } + remaining--; + + if (prefix <= 0xb7) { + int length = prefix - 0x80; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + Bytes bytes = content.slice(index + 1, length); + if (length == 1 && (bytes.get(0) & 0xFF) <= 0x7f) { + throw new InvalidRLPEncodingException("Value should have been encoded as a single byte " + bytes.toHexString()); + } + index += 1 + length; + return bytes; + } + if (prefix <= 0xbf) { + int lengthOfLength = prefix - 0xb7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + remaining -= lengthOfLength; + Bytes lengthBytes = content.slice(index + 1, lengthOfLength); + if (lengthBytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("RLP value length contains leading zero bytes"); + } + int length; + try { + length = lengthBytes.intValue(); + } catch (IllegalArgumentException e) { + throw new InvalidRLPEncodingException(e.getMessage()); + } + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + + index += 1 + lengthOfLength; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } + throw new InvalidRLPTypeException("Attempted to read a value but next item is a list"); + } + + @Override + public boolean nextIsList() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + return prefix > 0xbf; + } + + @Override + public boolean nextIsEmpty() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + return prefix == 0x80; + } + + @Override + public T readList(Function fn) { + return fn.apply(new BytesValueRLPReader(readList())); + } + + @Override + public void skipNext() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0x7f) { + index++; + return; + } + remaining--; + + if (prefix <= 0xb7) { + int length = prefix - 0x80; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + index += 1 + length; + return; + } + if (prefix <= 0xbf) { + int lengthOfLength = prefix - 0xb7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + remaining -= lengthOfLength; + int length = content.slice(index + 1, lengthOfLength).intValue(); + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + + index += 1 + lengthOfLength + length; + return; + } + if (prefix <= 0xf7) { + int length = prefix - 0xc0; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + index += 1 + length; + return; + } + + int lengthOfLength = prefix - 0xf7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + remaining -= lengthOfLength; + int length = content.slice(index + 1, lengthOfLength).intValue(); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + index += 1 + lengthOfLength + length; + } + + @Override + public int remaining() { + int oldIndex = index; + try { + int count = 0; + while (!isComplete()) { + count++; + skipNext(); + } + return count; + } finally { + index = oldIndex; + } + } + + @Override + public boolean isComplete() { + return (content.size() - index) == 0; + } + + private Bytes readList() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0xbf) { + throw new InvalidRLPTypeException("Attempted to read a list but next item is a value"); + } + remaining--; + + if (prefix <= 0xf7) { + int length = prefix - 0xc0; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + index++; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } + + int lengthOfLength = prefix - 0xf7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + remaining -= lengthOfLength; + Bytes lengthBytes = content.slice(index + 1, lengthOfLength); + if (lengthBytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("RLP list length contains leading zero bytes"); + } + int length = lengthBytes.intValue(); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + lengthOfLength + " but have only " + remaining); + } + + index += 1 + lengthOfLength; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPWriter.java b/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPWriter.java new file mode 100644 index 00000000..fa0dbd09 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/BytesValueRLPWriter.java @@ -0,0 +1,155 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.function.Consumer; + +final class BytesValueRLPWriter implements RLPWriter { + + private static final byte[] EMPTY_VALUE = new byte[] {(byte) 0x80}; + private static final int COMBINE_THRESHOLD = 32; + + private ArrayDeque values = new ArrayDeque<>(); + + Bytes toBytes() { + if (values.isEmpty()) { + return Bytes.EMPTY; + } + return Bytes.wrap(values.stream().map(Bytes::wrap).toArray(Bytes[]::new)); + } + + @Override + public void writeRLP(Bytes value) { + requireNonNull(value); + appendBytes(value.toArrayUnsafe()); + } + + @Override + public void writeValue(Bytes value) { + requireNonNull(value); + writeByteArray(value.toArrayUnsafe()); + } + + @Override + public void writeByteArray(byte[] value) { + encodeByteArray(value, this::appendBytes); + } + + static Bytes encodeValue(byte[] value) { + int maxSize = value.length + 5; + ByteBuffer buffer = ByteBuffer.allocate(maxSize); + encodeByteArray(value, buffer::put); + return Bytes.wrap(buffer.array(), 0, buffer.position()); + } + + private static void encodeByteArray(byte[] value, Consumer appender) { + requireNonNull(value); + int size = value.length; + if (size == 0) { + appender.accept(EMPTY_VALUE); + return; + } + if (size == 1) { + byte b = value[0]; + if ((b & 0xFF) <= 0x7f) { + appender.accept(value); + return; + } + } + appender.accept(encodeLength(size, 0x80)); + appender.accept(value); + } + + @Override + public void writeLong(long value) { + appendBytes(encodeNumber(value)); + } + + static Bytes encodeLong(long value) { + return Bytes.wrap(encodeNumber(value)); + } + + @Override + public void writeList(Consumer fn) { + requireNonNull(fn); + BytesValueRLPWriter listWriter = new BytesValueRLPWriter(); + fn.accept(listWriter); + writeEncodedValuesAsList(listWriter.values); + } + + private void writeEncodedValuesAsList(Deque values) { + int totalSize = 0; + for (byte[] value : values) { + try { + totalSize = Math.addExact(totalSize, value.length); + } catch (ArithmeticException e) { + throw new IllegalArgumentException("Combined length of values is too long (> Integer.MAX_VALUE)"); + } + } + appendBytes(encodeLength(totalSize, 0xc0)); + this.values.addAll(values); + } + + private void appendBytes(byte[] bytes) { + if (bytes.length < COMBINE_THRESHOLD) { + if (!values.isEmpty()) { + byte[] last = values.getLast(); + if (last.length <= (COMBINE_THRESHOLD - bytes.length)) { + byte[] combined = new byte[last.length + bytes.length]; + System.arraycopy(last, 0, combined, 0, last.length); + System.arraycopy(bytes, 0, combined, last.length, bytes.length); + values.pollLast(); + values.add(combined); + return; + } + } + } + values.add(bytes); + } + + private static byte[] encodeNumber(long value) { + if (value <= 0x7f) { + return new byte[] {(byte) (value & 0xFF)}; + } + return encodeLongBytes(value, 0x80); + } + + private static byte[] encodeLength(int length, int offset) { + if (length <= 55) { + return new byte[] {(byte) ((offset + length) & 0xFF)}; + } + return encodeLongBytes(length, offset + 55); + } + + private static byte[] encodeLongBytes(long value, int offset) { + int zeros = Long.numberOfLeadingZeros(value); + int resultBytes = 8 - (zeros / 8); + + byte[] encoded = new byte[resultBytes + 1]; + encoded[0] = (byte) ((offset + resultBytes) & 0xFF); + + int shift = 0; + for (int i = 0; i < resultBytes; i++) { + encoded[resultBytes - i] = (byte) ((value >> shift) & 0xFF); + shift += 8; + } + return encoded; + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/EndOfRLPException.java b/rlp/src/main/java/net/consensys/cava/rlp/EndOfRLPException.java new file mode 100644 index 00000000..ff90448a --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/EndOfRLPException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +/** + * Indicates the end of the RLP source has been reached unexpectedly. + */ +public class EndOfRLPException extends RLPException { + public EndOfRLPException() { + super("End of RLP source reached"); + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPEncodingException.java b/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPEncodingException.java new file mode 100644 index 00000000..40c4a452 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPEncodingException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +/** + * Indicates that invalid RLP encoding was encountered. + */ +public class InvalidRLPEncodingException extends RLPException { + public InvalidRLPEncodingException(String message) { + super(message); + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPTypeException.java b/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPTypeException.java new file mode 100644 index 00000000..37d35891 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/InvalidRLPTypeException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +/** + * Indicates that an unexpected type was encountered when decoding RLP. + */ +public class InvalidRLPTypeException extends RLPException { + public InvalidRLPTypeException(String message) { + super(message); + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/RLP.java b/rlp/src/main/java/net/consensys/cava/rlp/RLP.java new file mode 100644 index 00000000..57f06878 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/RLP.java @@ -0,0 +1,253 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; + +import java.math.BigInteger; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.google.common.base.Charsets; + +/** + * Recursive Length Prefix (RLP) encoding and decoding. + */ +public final class RLP { + private RLP() {} + + /** + * Encode values to a {@link Bytes} value. + *

+ * Important: this method does not write any list prefix to the result. If you are writing a RLP encoded list of + * values, you usually want to use {@link #encodeList(Consumer)}. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encode(Consumer fn) { + requireNonNull(fn); + BytesValueRLPWriter writer = new BytesValueRLPWriter(); + fn.accept(writer); + return writer.toBytes(); + } + + /** + * Encode a list of values to a {@link Bytes} value. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeList(Consumer fn) { + requireNonNull(fn); + BytesValueRLPWriter writer = new BytesValueRLPWriter(); + writer.writeList(fn); + return writer.toBytes(); + } + + /** + * Encode a value to a {@link Bytes} value. + * + * @param value The value to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeValue(Bytes value) { + requireNonNull(value); + return BytesValueRLPWriter.encodeValue(value.toArrayUnsafe()); + } + + /** + * Encode a value to a {@link Bytes} value. + * + * @param value The value to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeByteArray(byte[] value) { + requireNonNull(value); + return BytesValueRLPWriter.encodeValue(value); + } + + /** + * Encode a integer to a {@link Bytes} value. + * + * @param value The integer to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeInt(int value) { + return BytesValueRLPWriter.encodeLong(value); + } + + /** + * Encode a long to a {@link Bytes} value. + * + * @param value The long to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeLong(long value) { + return BytesValueRLPWriter.encodeLong(value); + } + + /** + * Encode a big integer to a {@link Bytes} value. + * + * @param value The big integer to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeBigInteger(BigInteger value) { + requireNonNull(value); + return encode(writer -> writer.writeBigInteger(value)); + } + + /** + * Encode a string to a {@link Bytes} value. + * + * @param str The string to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeString(String str) { + requireNonNull(str); + return encodeByteArray(str.getBytes(Charsets.UTF_8)); + } + + /** + * Read and decode RLP from a {@link Bytes} value. + *

+ * Important: this method does not consume any list prefix from the source data. If you are reading a RLP encoded list + * of values, you usually want to use {@link #decodeList(Bytes, Function)}. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + */ + public static T decode(Bytes source, Function fn) { + requireNonNull(source); + requireNonNull(fn); + return fn.apply(new BytesValueRLPReader(source)); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static T decodeList(Bytes source, Function fn) { + requireNonNull(source); + requireNonNull(fn); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, reader -> reader.readList(fn)); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output list. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static List decodeList(Bytes source, BiConsumer> fn) { + requireNonNull(source); + requireNonNull(fn); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, reader -> reader.readList(fn)); + } + + /** + * Read an RLP encoded value from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return The bytes for the value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no RLP values to read. + */ + public static Bytes decodeValue(Bytes source) { + requireNonNull(source); + return decode(source, RLPReader::readValue); + } + + /** + * Read an RLP encoded integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static int decodeInt(Bytes source) { + requireNonNull(source); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, RLPReader::readInt); + } + + /** + * Read an RLP encoded long from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static long decodeLong(Bytes source) { + requireNonNull(source); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, RLPReader::readLong); + } + + /** + * Read an RLP encoded big integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A {@link BigInteger}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static BigInteger decodeBigInteger(Bytes source) { + requireNonNull(source); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, RLPReader::readBigInteger); + } + + /** + * Read an RLP encoded string from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static String decodeString(Bytes source) { + requireNonNull(source); + checkArgument(source.size() > 0, "source is empty"); + return decode(source, RLPReader::readString); + } + + /** + * Check if the {@link Bytes} value contains an RLP encoded list. + * + * @param value The value to check. + * @return true if the value contains a list. + */ + public static boolean isList(Bytes value) { + requireNonNull(value); + checkArgument(value.size() > 0, "value is empty"); + return decode(value, RLPReader::nextIsList); + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/RLPException.java b/rlp/src/main/java/net/consensys/cava/rlp/RLPException.java new file mode 100644 index 00000000..8182df5d --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/RLPException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +/** + * Base type for all RLP encoding and decoding exceptions. + */ +public class RLPException extends RuntimeException { + public RLPException(String message) { + super(message); + } +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/RLPReader.java b/rlp/src/main/java/net/consensys/cava/rlp/RLPReader.java new file mode 100644 index 00000000..b6ce887e --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/RLPReader.java @@ -0,0 +1,184 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import com.google.common.base.Charsets; + +/** + * A reader for consuming values from an RLP encoded source. + */ +public interface RLPReader { + + /** + * Read the next value from the RLP source. + * + * @return The bytes for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + Bytes readValue(); + + /** + * Read an integer value from the RLP source. + * + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default int readInt() { + Bytes bytes = readValue(); + try { + return bytes.intValue(); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Next value is too large to be represented as an int"); + } + } + + /** + * Read a long value from the RLP source. + * + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default long readLong() { + Bytes bytes = readValue(); + try { + return bytes.longValue(); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Next value is too large to be represented as a long"); + } + } + + /** + * Read a {@link UInt256} value from the RLP source. + * + * @return A {@link UInt256} value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default UInt256 readUInt256() { + Bytes bytes = readValue(); + try { + return UInt256.fromBytes(bytes); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Next value is too large to be represented as a UInt256"); + } + } + + /** + * Read a big integer value from the RLP source. + * + * @return A big integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default BigInteger readBigInteger() { + return readValue().unsignedBigIntegerValue(); + } + + /** + * Read a string value from the RLP source. + * + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default String readString() { + return new String(readValue().toArrayUnsafe(), Charsets.UTF_8); + } + + /** + * Check if the next item to be read is a list. + * + * @return true if the next item to be read is a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + boolean nextIsList(); + + /** + * Check if the next item to be read is empty. + * + * @return true if the next item to be read is empty. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + boolean nextIsEmpty(); + + /** + * Read a list of values from the RLP source. + * + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + T readList(Function fn); + + /** + * Read a list of values from the RLP source, populating a mutable output list. + * + * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default List readList(BiConsumer> fn) { + requireNonNull(fn); + return readList(reader -> { + List list = new ArrayList<>(); + fn.accept(reader, list); + return list; + }); + } + + /** + * Skip the next value or list in the RLP source. + * + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + void skipNext(); + + /** + * The number of remaining values to read. + * + * @return The number of remaining values to read. + */ + int remaining(); + + /** + * Check if all values have been read. + * + * @return true if all values have been read. + */ + boolean isComplete(); +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/RLPWriter.java b/rlp/src/main/java/net/consensys/cava/rlp/RLPWriter.java new file mode 100644 index 00000000..9e030e3f --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/RLPWriter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.function.Consumer; + +import com.google.common.base.Charsets; + +/** + * A writer for encoding values to RLP. + */ +public interface RLPWriter { + + /** + * Append an already RLP encoded value. + * + *

+ * Note that this method may not validate that {@code value} is a valid RLP sequence. Appending an invalid RLP + * sequence will cause the entire RLP encoding produced by this writer to also be invalid. + * + * @param value The RLP encoded bytes to append. + */ + void writeRLP(Bytes value); + + /** + * Encode a {@link Bytes} value to RLP. + * + * @param value The byte array to encode. + */ + void writeValue(Bytes value); + + /** + * Encode a byte array to RLP. + * + * @param value The byte array to encode. + */ + default void writeByteArray(byte[] value) { + writeValue(Bytes.wrap(value)); + } + + /** + * Write an integer to the output. + * + * @param value The integer to write. + */ + default void writeInt(int value) { + writeLong(value); + } + + /** + * Write a long to the output. + * + * @param value The long value to write. + */ + void writeLong(long value); + + /** + * Write a {@link UInt256} to the output. + * + * @param value The {@link UInt256} value to write. + */ + default void writeUInt256(UInt256 value) { + writeValue(value.toMinimalBytes()); + } + + /** + * Write a big integer to the output. + * + * @param value The integer to write. + */ + default void writeBigInteger(BigInteger value) { + if (value.signum() == 0) { + writeInt(0); + return; + } + byte[] byteArray = value.toByteArray(); + if (byteArray[0] == 0) { + writeValue(Bytes.wrap(byteArray).slice(1)); + } else { + writeByteArray(byteArray); + } + } + + /** + * Write a string to the output. + * + * @param str The string to write. + */ + default void writeString(String str) { + writeByteArray(str.getBytes(Charsets.UTF_8)); + } + + /** + * Write a list of values. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + */ + void writeList(Consumer fn); +} diff --git a/rlp/src/main/java/net/consensys/cava/rlp/package-info.java b/rlp/src/main/java/net/consensys/cava/rlp/package-info.java new file mode 100644 index 00000000..e2a7f695 --- /dev/null +++ b/rlp/src/main/java/net/consensys/cava/rlp/package-info.java @@ -0,0 +1,13 @@ +/** + * 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 standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-rlp' (cava-rlp.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.rlp; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPReaderSpec.kt b/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPReaderSpec.kt new file mode 100644 index 00000000..c9cc865d --- /dev/null +++ b/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPReaderSpec.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp + +import com.winterbe.expekt.should +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.bytes.Bytes.fromHexString +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import org.junit.jupiter.api.assertThrows +import java.math.BigInteger + +/* ktlint-disable max-line-length */ +val SHORT_LIST = + fromHexString("f784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572") +val LONG_LIST = + fromHexString("f90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376") +/* ktlint-disable max-line-length */ + +class RLPReaderSpec : Spek({ + + describe("A RLP reader") { + + data class SomeObject(val name: String, val number: Int, val longNumber: BigInteger) + + it("should allow to parse full objects") { + val bytes = Bytes.fromHexString("83426f620486011F71B70768") + val readObject: SomeObject = RLP.decode(bytes, { reader -> + SomeObject(reader.readString(), reader.readInt(), reader.readBigInteger()) + }) + + readObject.name.should.equal("Bob") + readObject.number.should.equal(4) + readObject.longNumber.should.equal(BigInteger.valueOf(1234563434344L)) + } + + describe("Reading integers") { + it("should read zero") { + RLP.decode(fromHexString("80"), { reader -> + reader.readInt().should.equal(0) + }) + } + + it("should read one") { + RLP.decode(fromHexString("01"), { reader -> + reader.readInt().should.equal(1) + }) + } + + it("should read 16") { + RLP.decode(fromHexString("10"), { reader -> + reader.readInt().should.equal(16) + }) + } + + it("should read 79") { + RLP.decode(fromHexString("4f"), { reader -> + reader.readInt().should.equal(79) + }) + } + + it("should read 127") { + RLP.decode(fromHexString("7f"), { reader -> + reader.readInt().should.equal(127) + }) + } + + it("should read 128") { + RLP.decode(fromHexString("8180"), { reader -> + reader.readInt().should.equal(128) + }) + } + + it("should read 1000") { + RLP.decode(fromHexString("8203e8"), { reader -> + reader.readInt().should.equal(1000) + }) + } + + it("should read 10000") { + RLP.decode(fromHexString("830186a0"), { reader -> + reader.readInt().should.equal(100000) + }) + } + + it("should throw when input exhausted") { + assertThrows { + RLP.decode(Bytes.EMPTY, { reader -> + reader.readInt() + }) + } + } + + it("should throw when next item is a list") { + val exception = assertThrows { + RLP.decode(SHORT_LIST, { reader -> + reader.readInt() + }) + } + exception.message.should.equal("Attempted to read a value but next item is a list") + } + + it("should throw when source is truncated") { + val exception = assertThrows { + RLP.decode(fromHexString("830186"), { reader -> + reader.readInt() + }) + } + exception.message.should.equal("Insufficient bytes in RLP encoding: expected 3 but have only 2") + } + } + + describe("Reading strings") { + it("should read empty strings") { + RLP.decodeString(fromHexString("80")).should.equal("") + } + + it("should read one byte long strings") { + RLP.decodeString(fromHexString("00")).should.equal("\u0000") + RLP.decodeString(fromHexString("01")).should.equal("\u0001") + RLP.decodeString(fromHexString("7f")).should.equal("\u007F") + } + + it("should read short strings") { + RLP.decodeString(fromHexString("83646f67")).should.equal("dog") + } + + it("should read long strings") { + val content1 = + fromHexString("b74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69") + RLP.decodeString(content1).should.equal( + "Lorem ipsum dolor sit amet, consectetur adipisicing eli" + ) + val content2 = + fromHexString("b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974") + RLP.decodeString(content2).should.equal( + "Lorem ipsum dolor sit amet, consectetur adipisicing elit" + ) + val content33 = + fromHexString("b904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174") + RLP.decodeString(content33).should.equal( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat" + ) + } + + it("should throw when input exhausted") { + assertThrows { + RLP.decode(Bytes.EMPTY, { reader -> + reader.readString() + }) + } + } + + it("should throw when next item is a list") { + val exception = assertThrows { + RLP.decode(SHORT_LIST, { reader -> + reader.readString() + }) + } + exception.message.should.equal("Attempted to read a value but next item is a list") + } + + it("should throw when source is truncated") { + val exception = assertThrows { + RLP.decode(fromHexString("830186"), { reader -> + reader.readString() + }) + } + exception.message.should.equal("Insufficient bytes in RLP encoding: expected 3 but have only 2") + } + + it("should throw when the value length contains leading zero bytes") { + val content2 = + fromHexString("b900384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974") + assertThrows { RLP.decodeString(content2) } + } + } + + describe("Reading lists") { + it("should read a short list") { + RLP.decodeList(SHORT_LIST, { subReader, list -> + subReader.remaining().should.equal(11) + for (i in 1..11) { + list.add(subReader.readString()) + } + }).should.equal(listOf("asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer")) + } + + it("should read a long list") { + val expected = mutableListOf>() + for (i in 1..31) { + expected.add(listOf("asdf", "qwer", "zxcv")) + } + + RLP.decodeList(LONG_LIST, { subReader, list -> + for (i in 1..31) { + list.add(subReader.readList({ subSubReader, subList -> + subList.add(subSubReader.readString()) + subList.add(subSubReader.readString()) + subList.add(subSubReader.readString()) + })) + } + }).should.equal(expected) + } + + it("should throw an exception if there are zero bytes in the list length") { + assertThrows { RLP.decodeList(Bytes.fromHexString("0xf9000112"), { _, _ -> }) } + } + } + } +}) diff --git a/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPWriterSpec.kt b/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPWriterSpec.kt new file mode 100644 index 00000000..52c3a3f6 --- /dev/null +++ b/rlp/src/test/kotlin/net/consensys/cava/rlp/RLPWriterSpec.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.rlp + +import com.winterbe.expekt.should +import net.consensys.cava.bytes.Bytes +import net.consensys.cava.units.bigints.UInt256 +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import java.math.BigInteger + +class RLPWriterSpec : Spek({ + + describe("A RLP writer") { + + data class SomeObject(val name: String, val number: Int, val longNumber: BigInteger) + + describe("The RLP Writer") { + it("should allow to write and read full objects") { + val bob = SomeObject("Bob", 4, BigInteger.valueOf(1234563434344L)) + val bytes = RLP.encode { writer -> + writer.writeString(bob.name) + writer.writeInt(bob.number) + writer.writeBigInteger(bob.longNumber) + } + val readObject: SomeObject = RLP.decode(bytes, { reader -> + SomeObject(reader.readString(), reader.readInt(), reader.readBigInteger()) + }) + readObject.should.equal(bob) + } + } + + describe("Writing integers") { + it("should write small integers") { + RLP.encode { writer -> + writer.writeInt(1000) + }.should.equal(Bytes.fromHexString("8203e8")) + } + + it("should write 100000") { + RLP.encode { writer -> + writer.writeInt(100000) + }.should.equal(Bytes.fromHexString("830186a0")) + } + + it("should write long integers") { + RLP.encode { writer -> + writer.writeLong(100000) + }.should.equal(Bytes.fromHexString("830186a0")) + } + + it("should write uint256 integers") { + RLP.encode { writer -> + writer.writeUInt256(UInt256.valueOf(100000)) + }.should.equal(Bytes.fromHexString("830186a0")) + } + + it("should write big uint256 integers") { + RLP.encode { writer -> + writer.writeUInt256( + UInt256.fromHexString("0x0400000000000000000000000000000000000000000000000000f100000000ab")) + }.should.equal(Bytes.fromHexString("a00400000000000000000000000000000000000000000000000000f100000000ab")) + } + + it("should write big integers") { + RLP.encode { writer -> + writer.writeBigInteger(BigInteger.valueOf(100000)) + }.should.equal(Bytes.fromHexString("830186a0")) + } + + it("should write big integers - very big ones") { + RLP.encode { writer -> + writer.writeBigInteger(BigInteger.valueOf(127).pow(16)) + }.should.equal(Bytes.fromHexString("8ee1ceefa5bbd9ed1c97f17a1df801")) + } + } + + describe("Writing strings") { + it("should write empty strings") { + RLP.encode { writer -> + writer.writeString("") + }.should.equal(Bytes.fromHexString("80")) + } + + it("should write dog") { + RLP.encode { writer -> + writer.writeString("dog") + }.should.equal(Bytes.fromHexString("83646f67")) + } + + it("should write one character long strings") { + RLP.encode { writer -> + writer.writeString("d") + }.should.equal(Bytes.fromHexString("64")) + } + } + + describe("Writing lists") { + it("should write a short list") { + val values = listOf("asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer") + RLP.encodeList { listWriter -> + values.forEach { value -> + listWriter.writeString(value) + } + /* ktlint-disable max-line-length */ + }.should.equal(Bytes.fromHexString("f784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572")) + /* ktlint-enable max-line-length */ + } + + it("should write nested lists") { + val bytes = RLP.encodeList { listWriter -> + listWriter.writeString("asdf") + listWriter.writeString("qwer") + for (i in 0..30) { + listWriter.writeList { subListWriter -> + subListWriter.writeString("zxcv") + subListWriter.writeString("asdf") + subListWriter.writeString("qwer") + } + } + } + + RLP.decodeList(bytes, { listReader -> + listReader.readString().should.equal("asdf") + listReader.readString().should.equal("qwer") + for (i in 0..30) { + listReader.readList { subListReader -> + subListReader.readString().should.equal("zxcv") + subListReader.readString().should.equal("asdf") + subListReader.readString().should.equal("qwer") + } + } + }) + } + + it("should write previously encoded values") { + val output = RLP.encode { it.writeRLP(RLP.encodeByteArray("abc".toByteArray())) } + RLP.decodeString(output).should.equal("abc") + } + } + } +}) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..426cbc95 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,19 @@ +rootProject.name='cava' +include 'bytes' +include 'concurrent' +include 'config' +include 'crypto' +include 'eth-domain' +include 'eth-reference-tests' +include 'io' +include 'junit' +include 'kv' +include 'merkle-trie' +include 'net' +include 'rlp' +include 'toml' +include 'units' + +////// +// Enable feature preview for compatibility with Gradle 5.0 +enableFeaturePreview('STABLE_PUBLISHING') diff --git a/site/deploy.sh b/site/deploy.sh new file mode 100755 index 00000000..4b5e0111 --- /dev/null +++ b/site/deploy.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# script copied from https://github.com/eldarlabs/ghpages-deploy-script, under MIT license. + +# abort the script if there is a non-zero error +set -ue + +if [ $# != 1 ]; then + echo "Usage: $0 version" 1>&2 + exit 1; +fi + +version=$1 + +rootdir=$(cd `dirname $0`/.. && pwd) +if ! [ -d "$rootdir/.git" ]; then + echo "Missing .git directory" 1>&2 + exit 1 +fi + +echo "Publishing for version $version" + +remote=$(git config remote.origin.url) +javadocSource="$rootdir/build/docs/javadoc" +kotlinSource="$rootdir/build/docs/dokka" +readmeFile="$rootdir/README.md" + +if [ ! -d "$javadocSource" ] +then + echo "$javadocSource missing" + exit 1 +fi + +if [ ! -d "$kotlinSource" ] +then + echo "$kotlinSource missing" + exit 1 +fi + +if [ ! -f "$readmeFile" ] +then + echo "$readmeFile missing" + exit 1 +fi + +# make a temporary directory to put the gp-pages branch +builddir=$(mktemp -d $TMPDIR/site-XXXXXX) +trap 'rm -rf $builddir' EXIT +cd $builddir + +# now lets setup a new repo so we can update the gh-pages branch +echo "Checking out into $builddir/cava" +git clone --reference "$rootdir" -n "$remote" +cd cava + +# switch into the gh-pages branch +if git rev-parse --verify origin/gh-pages > /dev/null 2>&1 +then + git checkout gh-pages + + # cleanup previous pushes + if [ -e docs/java/latest ]; then + git rm -rf docs/java/latest + fi + if [ -e "docs/java/${version}" ]; then + git rm -rf "docs/java/${version}" + fi + if [ -e docs/kotlin/latest ]; then + git rm -rf docs/kotlin/latest + fi + if [ -e "docs/kotlin/${version}" ]; then + git rm -rf "docs/kotlin/${version}" + fi +else + git checkout --orphan gh-pages + git reset --hard +fi + +# copy documents into place +mkdir -p docs/java +(cd docs/java && +cp -r "$javadocSource" "./$version" && +ln -s "$version" latest) + +mkdir -p docs/kotlin +(cd docs/kotlin && +cp -r "$kotlinSource" "./$version" && +ln -s "$version" latest) + +cp "${readmeFile}" "docs/kotlin/${version}/" + +# stage any changes and new files +git add -A > /dev/null +# now commit: +git commit --allow-empty -m "Deploy ${version} to GitHub pages" +# and push, but send any output to /dev/null to hide anything sensitive +git push --force --quiet origin gh-pages > /dev/null 2>&1 + +echo "Finished Deployment!" diff --git a/site/overview.html b/site/overview.html new file mode 100644 index 00000000..30722b12 --- /dev/null +++ b/site/overview.html @@ -0,0 +1,15 @@ + +

+ In the spirit of 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), 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. +

+ \ No newline at end of file diff --git a/toml/build.gradle b/toml/build.gradle new file mode 100644 index 00000000..3e754762 --- /dev/null +++ b/toml/build.gradle @@ -0,0 +1,22 @@ +description = 'A parser for Tom\'s Obvious, Minimal Language (TOML).' + +apply plugin: 'antlr' + +generateGrammarSource { + outputDirectory file("${project.buildDir}/generated-src/antlr/main/net/consensys/cava/toml/internal") + arguments << "-visitor" << "-long-messages" + arguments << "-Xexact-output-dir" +} + +javadoc { exclude '**/internal/**' } + +dependencies { + antlr 'org.antlr:antlr4' + + compile 'com.google.code.findbugs:jsr305' + + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlLexer.g4 b/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlLexer.g4 new file mode 100644 index 00000000..327c2ab4 --- /dev/null +++ b/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlLexer.g4 @@ -0,0 +1,189 @@ +lexer grammar TomlLexer; + +channels { COMMENTS, WHITESPACE } + +tokens { TripleQuotationMark, TripleApostrophe, StringChar, Comma } + +@header { +package net.consensys.cava.toml.internal; +} + +@members { + private final IntegerStack arrayDepthStack = new IntegerStack(); + private int arrayDepth = 0; + + private void resetArrayDepth() { + arrayDepthStack.clear(); + arrayDepth = 0; + } + + private void pushArrayDepth() { + arrayDepthStack.push(arrayDepth); + arrayDepth = 0; + } + + private void popArrayDepth() { + arrayDepth = arrayDepthStack.pop(); + } +} + +fragment WSChar : [ \t]; +fragment NL : '\r'? '\n'; +fragment COMMENT : '#' (~'\n')*; +fragment Alpha : [A-Za-z]; +fragment Digit : [0-9]; +fragment Digit1_9 : [1-9]; +fragment Digit0_7 : [0-7]; +fragment Digit0_1 : [0-1]; +fragment HexDig : Digit | [A-Fa-f]; + +fragment UNQUOTED_KEY : (Alpha | Digit | '-' | '_')+; + +Dot : '.'; +Equals : '=' { resetArrayDepth(); } -> pushMode(ValueMode); +QuotationMark : '"' -> pushMode(BasicStringMode); +Apostrophe : '\'' -> pushMode(LiteralStringMode); +TableKeyStart : '['; +TableKeyEnd : ']'; +ArrayTableKeyStart : '[['; +ArrayTableKeyEnd : ']]'; +UnquotedKey : UNQUOTED_KEY; + +WS : WSChar+ -> channel(WHITESPACE); +Comment : COMMENT -> channel(COMMENTS); +NewLine : NL { setText(System.lineSeparator()); }; +Error : .; + + +mode KeyMode; + +KeyDot : '.' -> type(Dot); +KeyQuotationMark : '"' -> type(QuotationMark), pushMode(BasicStringMode); +KeyApostrophe : '\'' -> type(Apostrophe), pushMode(LiteralStringMode); +KeyUnquotedKey : UNQUOTED_KEY -> type(UnquotedKey); + +KeyWS : WSChar+ -> type(WS), channel(WHITESPACE); +KeyError : . -> type(Error); + + +mode ValueMode; + +// Strings +ValueQuotationMark : '"' -> type(QuotationMark), mode(BasicStringMode); +ValueTripleQuotationMark : '"""' NL? -> type(TripleQuotationMark), mode(MLBasicStringMode); +ValueApostrophe : '\'' -> type(Apostrophe), mode(LiteralStringMode); +ValueTripleApostrophe : '\'\'\'' NL? -> type(TripleApostrophe), mode(MLLiteralStringMode); + +// Integers +fragment DecInt : [-+]? (Digit | Digit1_9 ('_'? Digit)+); +DecimalInteger : DecInt { "-:".indexOf(_input.LA(1)) < 0 }? -> popMode; +HexInteger : '0x' HexDig ('_'? HexDig)* -> popMode; +OctalInteger : '0o' Digit0_7 ('_'? Digit0_7)* -> popMode; +BinaryInteger : '0b' Digit0_1 ('_'? Digit0_1)* -> popMode; + +// Float +fragment Exp : [eE] DecInt; +fragment Frac : '.' Digit ('_'? Digit)*; +FloatingPoint : DecInt (Exp | Frac Exp?) -> popMode; +FloatingPointInf: [-+]? 'inf' -> popMode; +FloatingPointNaN : [-+]? 'nan' -> popMode; + +// Boolean +TrueBoolean : 'true' -> popMode; +FalseBoolean : 'false' -> popMode; + +// Date and Time +ValueDateStart : Digit+ { "-:".indexOf(_input.LA(1)) >= 0 }? -> type(DateDigits), mode(DateMode); + +// Array +ArrayStart : '[' { arrayDepth++; } -> pushMode(ValueMode); +ArrayEnd : ']' { arrayDepth--; } -> popMode; +ArrayComma : ',' { arrayDepth > 0 }? -> type(Comma), pushMode(ValueMode); + +// Table +InlineTableStart : '{' { pushArrayDepth(); } -> mode(InlineTableMode); + +ValueWS : WSChar+ -> type(WS), channel(WHITESPACE); +ValueComment : COMMENT -> type(Comment), channel(COMMENTS); +ArrayNewLine: NL { arrayDepth > 0}? -> type(NewLine); + +ValueNewLine: NL { arrayDepth == 0}? -> type(NewLine), popMode; +ValueError : . -> type(Error), popMode; + + +mode BasicStringMode; + +BasicStringEnd : '"' -> type(QuotationMark), popMode; +BasicStringUnescaped : ~[\u0000-\u001F"\\\u007F] -> type(StringChar); +EscapeSequence + : '\\' ~[\n] + | '\\u' HexDig HexDig HexDig HexDig + | '\\U' HexDig HexDig HexDig HexDig HexDig HexDig HexDig HexDig; + +BasicStringNewLine: NL { setText(System.lineSeparator()); } -> type(NewLine), popMode; +BasicStringError : . -> type(Error), popMode; + + +mode MLBasicStringMode; + +MLBasicStringEnd : '"""' -> type(TripleQuotationMark), popMode; +MLBasicStringLineEnd : '\\' [ \t]* NL { setText(System.lineSeparator()); } -> type(NewLine); +MLBasicStringUnescaped : ~[\u0000-\u001F\\\u007F] -> type(StringChar); +MLBasicStringEscape : + ('\\u' HexDig HexDig HexDig HexDig + | '\\U' HexDig HexDig HexDig HexDig HexDig HexDig HexDig HexDig + | '\\' .) -> type(EscapeSequence); +MLBasicStringNewLine: NL { setText(System.lineSeparator()); } -> type(NewLine); + +MLBasicStringError : . -> type(Error), popMode; + + +mode LiteralStringMode; + +LiteralStringEnd : '\'' -> type(Apostrophe), popMode; +LiteralStringChar : ~[\u0000-\u0008\u000A-\u001F'\u007F] -> type(StringChar); + +LiteralStringNewLine: NL { setText(System.lineSeparator()); } -> type(NewLine), popMode; +LiteralStringError : . -> type(Error), popMode; + + +mode MLLiteralStringMode; + +MLLiteralStringEnd : '\'\'\'' -> type(TripleApostrophe), popMode; +MLLiteralStringChar : ~[\u0000-\u0008\u000A-\u001F\u007F] -> type(StringChar); +MLLiteralStringNewLine: NL { setText(System.lineSeparator()); } -> type(NewLine); + +MLLiteralStringError : . -> type(Error), popMode; + + +mode DateMode; + +Dash : '-'; +Plus : '+'; +Colon : ':'; +DateDot : '.' -> type(Dot); +Z : 'Z'; +TimeDelimiter : [Tt] | (' ' { _input.LA(1) >= '0' && _input.LA(1) <= '9' }?); +DateDigits : Digit+; + +DateWS : WSChar+ -> type(WS), channel(WHITESPACE), popMode; +DateComment : COMMENT -> type(Comment), channel(COMMENTS), popMode; +DateNewLine: NL { setText(System.lineSeparator()); } -> type(NewLine), popMode; +DateComma: ',' -> type(Comma), popMode; +DateError : . -> type(Error), popMode; + + +mode InlineTableMode; + +InlineTableEnd : '}' { popArrayDepth(); } -> popMode; +InlineTableDot : '.' -> type(Dot); +InlineTableEquals : '=' -> type(Equals), pushMode(ValueMode); +InlineTableComma : ',' -> type(Comma); +InlineTableQuotationMark : '"' -> type(QuotationMark), pushMode(BasicStringMode); +InlineTableApostrophe : '\'' -> type(Apostrophe), pushMode(LiteralStringMode); +InlineTableUnquotedKey : UNQUOTED_KEY -> type(UnquotedKey); + +InlineTableWS : WSChar+ -> type(WS), channel(WHITESPACE); +InlineTableComment : COMMENT -> type(Comment), channel(COMMENTS), popMode; +InlineTableNewLine : NL { setText(System.lineSeparator()); } -> type(NewLine), popMode; +InlineTableError : . -> type(Error), popMode; diff --git a/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlParser.g4 b/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlParser.g4 new file mode 100644 index 00000000..f9dc122c --- /dev/null +++ b/toml/src/main/antlr/net/consensys/cava/toml/internal/TomlParser.g4 @@ -0,0 +1,174 @@ +parser grammar TomlParser; + +options { tokenVocab=TomlLexer; } + +@header { +package net.consensys.cava.toml.internal; +} + +// Document parser +toml : NewLine* (expression (NewLine+ expression)* NewLine*)? EOF; + +expression + : keyval + | table + ; + + +// Key string parser +tomlKey : key EOF; + + +// Key-Value pairs +keyval : key Equals val; + +key : simpleKey (Dot simpleKey)*; +simpleKey + : quotedKey + | unquotedKey + ; + +unquotedKey : UnquotedKey; +quotedKey + : basicString + | literalString + ; + +val + : string + | integer + | floatValue + | booleanValue + | dateTime + | array + | inlineTable + ; + + +// String +string + : mlBasicString + | basicString + | mlLiteralString + | literalString + ; + + +// Basic String +basicString : QuotationMark basicChar* QuotationMark; +basicChar + : basicUnescaped + | escaped + ; +basicUnescaped : StringChar; + +escaped : EscapeSequence; + + +// Multiline Basic String +mlBasicString : TripleQuotationMark mlBasicChar* TripleQuotationMark; +mlBasicChar + : mlBasicUnescaped + | escaped; +mlBasicUnescaped : StringChar | NewLine; + + +// Literal String +literalString : Apostrophe literalBody Apostrophe; +literalBody : StringChar*; + + +// Multiline Literal String +mlLiteralString : TripleApostrophe mlLiteralBody TripleApostrophe; +mlLiteralBody : (StringChar | NewLine)*; + + +// Integer +integer + : decInt + | hexInt + | octInt + | binInt + ; + +decInt : DecimalInteger; +hexInt : HexInteger; +octInt : OctalInteger; +binInt : BinaryInteger; + + +// Float +floatValue + : regularFloat + | regularFloatInf + | regularFloatNaN + ; +regularFloat : FloatingPoint; +regularFloatInf : FloatingPointInf; +regularFloatNaN : FloatingPointNaN; + + +// Boolean +booleanValue + : trueBool + | falseBool + ; + +trueBool : TrueBoolean; +falseBool : FalseBoolean; + + +// Date and Time +dateTime + : offsetDateTime + | localDateTime + | localDate + | localTime + ; + +offsetDateTime : date TimeDelimiter time timeOffset; +localDateTime : date TimeDelimiter time; +localDate : date; +localTime : time; + +date : year Dash month Dash day; +time : hour Colon minute Colon second (Dot secondFraction)?; +timeOffset + : Z + | hourOffset Colon minuteOffset + ; +hourOffset : (Dash | Plus) hour; +minuteOffset : DateDigits; +secondFraction : DateDigits; +year : DateDigits; +month : DateDigits; +day : DateDigits; +hour : DateDigits; +minute : DateDigits; +second : DateDigits; + + +// Array +array : ArrayStart (arrayValues Comma?)? NewLine* ArrayEnd; +arrayValues : arrayValue (Comma arrayValue)*; +arrayValue : NewLine* val; + + +// Table +table + : standardTable + | arrayTable + ; + + +// Standard Table +standardTable : TableKeyStart key? TableKeyEnd; + + +// Inline Table +inlineTable : InlineTableStart inlineTableValues? InlineTableEnd; +inlineTableValues : keyval (Comma keyval)*; + + +// Array Table +arrayTable : ArrayTableKeyStart key? ArrayTableKeyEnd; diff --git a/toml/src/main/java/net/consensys/cava/toml/AccumulatingErrorListener.java b/toml/src/main/java/net/consensys/cava/toml/AccumulatingErrorListener.java new file mode 100644 index 00000000..2f082b54 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/AccumulatingErrorListener.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.TomlPosition.positionAt; + +import net.consensys.cava.toml.internal.TomlLexer; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.InputMismatchException; +import org.antlr.v4.runtime.NoViableAltException; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.misc.IntervalSet; + +final class AccumulatingErrorListener extends BaseErrorListener implements ErrorReporter { + + private final List errors = new ArrayList<>(); + + @Override + public void syntaxError( + Recognizer recognizer, + Object offendingSymbol, + int line, + int charPosition, + String msg, + RecognitionException e) { + + TomlPosition position = positionAt(line, charPosition + 1); + + if (e instanceof InputMismatchException || e instanceof NoViableAltException) { + String message = getMessage(e.getOffendingToken(), getExpected(e)); + reportError(message, position); + return; + } + + if (offendingSymbol instanceof Token && recognizer instanceof Parser) { + String message = getMessage((Token) offendingSymbol, getExpected(((Parser) recognizer).getExpectedTokens())); + reportError(message, position); + return; + } + + reportError(msg, position); + } + + @Override + public void reportError(TomlParseError error) { + errors.add(error); + } + + private void reportError(String message, TomlPosition position) { + reportError(new TomlParseError(message, position)); + } + + List errors() { + return errors; + } + + private String getMessage(Token token, String expected) { + return "Unexpected " + getTokenName(token) + ", expected " + expected; + } + + private static String getTokenName(Token token) { + int tokenType = token.getType(); + switch (tokenType) { + case TomlLexer.NewLine: + return "end of line"; + case TomlLexer.EOF: + return "end of input"; + default: + return "'" + Toml.tomlEscape(token.getText()) + '\''; + } + } + + private static String getExpected(RecognitionException e) { + IntervalSet expectedTokens = e.getExpectedTokens(); + return getExpected(expectedTokens); + } + + private static String getExpected(IntervalSet expectedTokens) { + List sortedNames = expectedTokens + .getIntervals() + .stream() + .flatMap(i -> IntStream.rangeClosed(i.a, i.b).boxed()) + .flatMap(TokenName::namesForToken) + .sorted() + .distinct() + .map(TokenName::displayName) + .collect(Collectors.toList()); + + StringBuilder builder = new StringBuilder(); + int count = sortedNames.size(); + for (int i = 0; i < count; ++i) { + builder.append(sortedNames.get(i)); + if (i < (count - 2)) { + builder.append(", "); + } else if (i == (count - 2)) { + if (count >= 3) { + builder.append(','); + } + builder.append(" or "); + } + } + + return builder.toString(); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/ArrayVisitor.java b/toml/src/main/java/net/consensys/cava/toml/ArrayVisitor.java new file mode 100644 index 00000000..2fe0b87d --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/ArrayVisitor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.ArrayValueContext; +import net.consensys.cava.toml.internal.TomlParser.ValContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +final class ArrayVisitor extends TomlParserBaseVisitor { + + private final MutableTomlArray array = new MutableTomlArray(true); + + @Override + public MutableTomlArray visitArrayValue(ArrayValueContext ctx) { + ValContext valContext = ctx.val(); + if (valContext != null) { + Object value = valContext.accept(new ValueVisitor()); + if (value != null) { + TomlPosition position = new TomlPosition(ctx); + try { + array.append(value, position); + } catch (TomlInvalidTypeException e) { + throw new TomlParseError(e.getMessage(), position); + } + } + } + return array; + } + + @Override + protected MutableTomlArray aggregateResult(MutableTomlArray aggregate, MutableTomlArray nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected MutableTomlArray defaultResult() { + return array; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/ErrorReporter.java b/toml/src/main/java/net/consensys/cava/toml/ErrorReporter.java new file mode 100644 index 00000000..fc10cf8e --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/ErrorReporter.java @@ -0,0 +1,17 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +interface ErrorReporter { + void reportError(TomlParseError error); +} diff --git a/toml/src/main/java/net/consensys/cava/toml/InlineTableVisitor.java b/toml/src/main/java/net/consensys/cava/toml/InlineTableVisitor.java new file mode 100644 index 00000000..5c1642f7 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/InlineTableVisitor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.KeyContext; +import net.consensys.cava.toml.internal.TomlParser.KeyvalContext; +import net.consensys.cava.toml.internal.TomlParser.ValContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.util.List; + +final class InlineTableVisitor extends TomlParserBaseVisitor { + + private final MutableTomlTable table = new MutableTomlTable(); + + @Override + public MutableTomlTable visitKeyval(KeyvalContext ctx) { + KeyContext keyContext = ctx.key(); + ValContext valContext = ctx.val(); + if (keyContext != null && valContext != null) { + List path = keyContext.accept(new KeyVisitor()); + if (path != null && !path.isEmpty()) { + Object value = valContext.accept(new ValueVisitor()); + if (value != null) { + table.set(path, value, new TomlPosition(ctx)); + } + } + } + return table; + } + + @Override + protected MutableTomlTable aggregateResult(MutableTomlTable aggregate, MutableTomlTable nextResult) { + return table; + } + + @Override + protected MutableTomlTable defaultResult() { + return table; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/JsonSerializer.java b/toml/src/main/java/net/consensys/cava/toml/JsonSerializer.java new file mode 100644 index 00000000..3c41bd01 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/JsonSerializer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.toml.TomlType.typeFor; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Optional; + +final class JsonSerializer { + private JsonSerializer() {} + + static void toJson(TomlTable table, Appendable appendable) throws IOException { + requireNonNull(table); + requireNonNull(appendable); + toJson(table, appendable, 0); + appendable.append(System.lineSeparator()); + } + + private static void toJson(TomlTable table, Appendable appendable, int indent) throws IOException { + if (table.isEmpty()) { + appendable.append("{}"); + return; + } + appendLine(appendable, "{"); + for (Iterator iterator = table.keySet().stream().sorted().iterator(); iterator.hasNext();) { + String key = iterator.next(); + append(appendable, indent + 2, "\"" + escape(key) + "\" : "); + Object value = table.get(Collections.singletonList(key)); + assert value != null; + appendTomlValue(value, appendable, indent); + if (iterator.hasNext()) { + appendable.append(","); + appendable.append(System.lineSeparator()); + } + } + appendable.append(System.lineSeparator()); + append(appendable, indent, "}"); + } + + static void toJson(TomlArray array, Appendable appendable) throws IOException { + toJson(array, appendable, 0); + appendable.append(System.lineSeparator()); + } + + private static void toJson(TomlArray array, Appendable appendable, int indent) throws IOException { + if (array.isEmpty()) { + appendable.append("[]"); + return; + } + if (array.containsTables()) { + append(appendable, 0, "["); + for (Iterator iterator = array.toList().iterator(); iterator.hasNext();) { + toJson((TomlTable) iterator.next(), appendable, indent); + if (iterator.hasNext()) { + appendable.append(","); + } + } + append(appendable, 0, "]"); + } else { + appendLine(appendable, "["); + for (Iterator iterator = array.toList().iterator(); iterator.hasNext();) { + indentLine(appendable, indent + 2); + appendTomlValue(iterator.next(), appendable, indent); + if (iterator.hasNext()) { + appendable.append(","); + appendable.append(System.lineSeparator()); + } + } + appendable.append(System.lineSeparator()); + append(appendable, indent, "]"); + } + } + + private static void appendTomlValue(Object value, Appendable appendable, int indent) throws IOException { + Optional tomlType = typeFor(value); + assert tomlType.isPresent(); + switch (tomlType.get()) { + case STRING: + append(appendable, 0, "\"" + escape((String) value) + "\""); + break; + case INTEGER: + case FLOAT: + append(appendable, 0, value.toString()); + break; + case BOOLEAN: + append(appendable, 0, ((Boolean) value) ? "true" : "false"); + break; + case OFFSET_DATE_TIME: + case LOCAL_DATE_TIME: + case LOCAL_DATE: + case LOCAL_TIME: + append(appendable, 0, "\"" + value.toString() + "\""); + break; + case ARRAY: + toJson((TomlArray) value, appendable, indent + 2); + break; + case TABLE: + toJson((TomlTable) value, appendable, indent + 2); + break; + } + } + + private static void append(Appendable appendable, int indent, String line) throws IOException { + indentLine(appendable, indent); + appendable.append(line); + } + + private static void appendLine(Appendable appendable, String line) throws IOException { + appendable.append(line); + appendable.append(System.lineSeparator()); + } + + private static void indentLine(Appendable appendable, int indent) throws IOException { + for (int i = 0; i < indent; ++i) { + appendable.append(' '); + } + } + + private static StringBuilder escape(String text) { + StringBuilder out = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (ch == '\'') { + out.append("\\'"); + continue; + } + if (ch >= 0x20) { + out.append(ch); + continue; + } + + switch (ch) { + case '\t': + out.append("\\t"); + break; + case '\b': + out.append("\\b"); + break; + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\f': + out.append("\\f"); + break; + default: + out.append("\\u").append(String.format("%04x", text.codePointAt(i))); + } + } + return out; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/KeyVisitor.java b/toml/src/main/java/net/consensys/cava/toml/KeyVisitor.java new file mode 100644 index 00000000..94a51f1a --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/KeyVisitor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.QuotedKeyContext; +import net.consensys.cava.toml.internal.TomlParser.UnquotedKeyContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.util.ArrayList; +import java.util.List; + +final class KeyVisitor extends TomlParserBaseVisitor> { + + private final List keys = new ArrayList<>(); + + @Override + public List visitUnquotedKey(UnquotedKeyContext ctx) { + keys.add(ctx.getText()); + return keys; + } + + @Override + public List visitQuotedKey(QuotedKeyContext ctx) { + StringBuilder builder = ctx.accept(new QuotedStringVisitor()); + keys.add(builder.toString()); + return keys; + } + + @Override + protected List aggregateResult(List aggregate, List nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected List defaultResult() { + return keys; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/LineVisitor.java b/toml/src/main/java/net/consensys/cava/toml/LineVisitor.java new file mode 100644 index 00000000..cf69f252 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/LineVisitor.java @@ -0,0 +1,112 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.TomlVersion.V0_4_0; + +import net.consensys.cava.toml.internal.TomlParser.ArrayTableContext; +import net.consensys.cava.toml.internal.TomlParser.KeyContext; +import net.consensys.cava.toml.internal.TomlParser.KeyvalContext; +import net.consensys.cava.toml.internal.TomlParser.StandardTableContext; +import net.consensys.cava.toml.internal.TomlParser.ValContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.util.List; + +final class LineVisitor extends TomlParserBaseVisitor { + + private final MutableTomlTable table = new MutableTomlTable(); + private final ErrorReporter errorReporter; + private final TomlVersion version; + private MutableTomlTable currentTable = table; + + LineVisitor(ErrorReporter errorReporter, TomlVersion version) { + this.errorReporter = errorReporter; + this.version = version; + } + + @Override + public MutableTomlTable visitKeyval(KeyvalContext ctx) { + KeyContext keyContext = ctx.key(); + ValContext valContext = ctx.val(); + if (keyContext == null || valContext == null) { + return table; + } + try { + List path = keyContext.accept(new KeyVisitor()); + if (path == null || path.isEmpty()) { + return table; + } + // TOML 0.4.0 doesn't support dotted keys + if (!version.after(V0_4_0) && path.size() > 1) { + throw new TomlParseError("Dotted keys are not supported", new TomlPosition(keyContext)); + } + Object value = valContext.accept(new ValueVisitor()); + if (value != null) { + currentTable.set(path, value, new TomlPosition(ctx)); + } + return table; + } catch (TomlParseError e) { + errorReporter.reportError(e); + return table; + } + } + + @Override + public MutableTomlTable visitStandardTable(StandardTableContext ctx) { + KeyContext keyContext = ctx.key(); + if (keyContext == null) { + errorReporter.reportError(new TomlParseError("Empty table key", new TomlPosition(ctx))); + return table; + } + List path = keyContext.accept(new KeyVisitor()); + if (path == null) { + return table; + } + try { + currentTable = table.createTable(path, new TomlPosition(ctx)); + } catch (TomlParseError e) { + errorReporter.reportError(e); + } + return table; + } + + @Override + public MutableTomlTable visitArrayTable(ArrayTableContext ctx) { + KeyContext keyContext = ctx.key(); + if (keyContext == null) { + errorReporter.reportError(new TomlParseError("Empty table key", new TomlPosition(ctx))); + return table; + } + List path = keyContext.accept(new KeyVisitor()); + if (path == null) { + return table; + } + try { + currentTable = table.createArrayTable(path, new TomlPosition(ctx)); + } catch (TomlParseError e) { + errorReporter.reportError(e); + } + return table; + } + + @Override + protected MutableTomlTable aggregateResult(MutableTomlTable aggregate, MutableTomlTable nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected MutableTomlTable defaultResult() { + return table; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/LocalDateVisitor.java b/toml/src/main/java/net/consensys/cava/toml/LocalDateVisitor.java new file mode 100644 index 00000000..75f657df --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/LocalDateVisitor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.DayContext; +import net.consensys.cava.toml.internal.TomlParser.MonthContext; +import net.consensys.cava.toml.internal.TomlParser.YearContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.time.DateTimeException; +import java.time.LocalDate; + +import org.antlr.v4.runtime.tree.ErrorNode; + +final class LocalDateVisitor extends TomlParserBaseVisitor { + + private static LocalDate INITIAL = LocalDate.parse("1900-01-01"); + private LocalDate date = INITIAL; + + @Override + public LocalDate visitYear(YearContext ctx) { + String text = ctx.getText(); + if (text.length() != 4) { + throw new TomlParseError("Invalid year (valid range 0000..9999)", new TomlPosition(ctx)); + } + int year; + try { + year = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid year", new TomlPosition(ctx), e); + } + date = date.withYear(year); + return date; + } + + @Override + public LocalDate visitMonth(MonthContext ctx) { + String text = ctx.getText(); + if (text.length() != 2) { + throw new TomlParseError("Invalid month (valid range 01..12)", new TomlPosition(ctx)); + } + int month; + try { + month = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid month", new TomlPosition(ctx), e); + } + if (month < 1 || month > 12) { + throw new TomlParseError("Invalid month (valid range 01..12)", new TomlPosition(ctx)); + } + date = date.withMonth(month); + return date; + } + + @Override + public LocalDate visitDay(DayContext ctx) { + String text = ctx.getText(); + if (text.length() != 2) { + throw new TomlParseError("Invalid day (valid range 01..28/31)", new TomlPosition(ctx)); + } + int day; + try { + day = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid day", new TomlPosition(ctx), e); + } + if (day < 1 || day > 31) { + throw new TomlParseError("Invalid day (valid range 01..28/31)", new TomlPosition(ctx)); + } + try { + date = date.withDayOfMonth(day); + } catch (DateTimeException e) { + throw new TomlParseError(e.getMessage(), new TomlPosition(ctx), e); + } + return date; + } + + @Override + public LocalDate visitErrorNode(ErrorNode node) { + return null; + } + + @Override + protected LocalDate aggregateResult(LocalDate aggregate, LocalDate nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected LocalDate defaultResult() { + return date; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/LocalTimeVisitor.java b/toml/src/main/java/net/consensys/cava/toml/LocalTimeVisitor.java new file mode 100644 index 00000000..dec9b6d5 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/LocalTimeVisitor.java @@ -0,0 +1,119 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.HourContext; +import net.consensys.cava.toml.internal.TomlParser.MinuteContext; +import net.consensys.cava.toml.internal.TomlParser.SecondContext; +import net.consensys.cava.toml.internal.TomlParser.SecondFractionContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.time.LocalTime; + +import org.antlr.v4.runtime.tree.ErrorNode; + +final class LocalTimeVisitor extends TomlParserBaseVisitor { + + private LocalTime time = LocalTime.MIN; + + @Override + public LocalTime visitHour(HourContext ctx) { + String text = ctx.getText(); + if (text.length() != 2) { + throw new TomlParseError("Invalid hour (valid range 00..23)", new TomlPosition(ctx)); + } + int hour; + try { + hour = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid hour", new TomlPosition(ctx), e); + } + if (hour < 0 || hour > 23) { + throw new TomlParseError("Invalid hour (valid range 00..23)", new TomlPosition(ctx)); + } + time = time.withHour(hour); + return time; + } + + @Override + public LocalTime visitMinute(MinuteContext ctx) { + String text = ctx.getText(); + if (text.length() != 2) { + throw new TomlParseError("Invalid minutes (valid range 00..59)", new TomlPosition(ctx)); + } + int minute; + try { + minute = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid minutes", new TomlPosition(ctx), e); + } + if (minute < 0 || minute > 59) { + throw new TomlParseError("Invalid minutes (valid range 00..59)", new TomlPosition(ctx)); + } + time = time.withMinute(minute); + return time; + } + + @Override + public LocalTime visitSecond(SecondContext ctx) { + String text = ctx.getText(); + if (text.length() != 2) { + throw new TomlParseError("Invalid seconds (valid range 00..59)", new TomlPosition(ctx)); + } + int second; + try { + second = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid seconds", new TomlPosition(ctx), e); + } + if (second < 0 || second > 59) { + throw new TomlParseError("Invalid seconds (valid range 00..59)", new TomlPosition(ctx)); + } + time = time.withSecond(second); + return time; + } + + @Override + public LocalTime visitSecondFraction(SecondFractionContext ctx) { + String text = ctx.getText(); + if (text.isEmpty() || text.length() > 9) { + throw new TomlParseError("Invalid nanoseconds (valid range 0..999999999)", new TomlPosition(ctx)); + } + if (text.length() < 9) { + text = text + "000000000".substring(text.length()); + } + int nano; + try { + nano = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid nanoseconds", new TomlPosition(ctx), e); + } + time = time.withNano(nano); + return time; + } + + @Override + public LocalTime visitErrorNode(ErrorNode node) { + return null; + } + + @Override + protected LocalTime aggregateResult(LocalTime aggregate, LocalTime nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected LocalTime defaultResult() { + return time; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/MutableTomlArray.java b/toml/src/main/java/net/consensys/cava/toml/MutableTomlArray.java new file mode 100644 index 00000000..6dc56a51 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/MutableTomlArray.java @@ -0,0 +1,156 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.toml.TomlType.typeFor; +import static net.consensys.cava.toml.TomlType.typeNameFor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +final class MutableTomlArray implements TomlArray { + + private static class Element { + final Object value; + final TomlPosition position; + + private Element(Object value, TomlPosition position) { + this.value = value; + this.position = position; + } + } + + static final TomlArray EMPTY = new MutableTomlArray(true); + private final List elements = new ArrayList<>(); + private final boolean definedAsLiteral; + private TomlType type = null; + + MutableTomlArray() { + this(false); + } + + MutableTomlArray(boolean definedAsLiteral) { + this.definedAsLiteral = definedAsLiteral; + } + + boolean wasDefinedAsLiteral() { + return definedAsLiteral; + } + + @Override + public int size() { + return elements.size(); + } + + @Override + public boolean isEmpty() { + return type == null; + } + + @Override + public boolean containsStrings() { + return type == null || type == TomlType.STRING; + } + + @Override + public boolean containsLongs() { + return type == null || type == TomlType.INTEGER; + } + + @Override + public boolean containsDoubles() { + return type == null || type == TomlType.FLOAT; + } + + @Override + public boolean containsBooleans() { + return type == null || type == TomlType.BOOLEAN; + } + + @Override + public boolean containsOffsetDateTimes() { + return type == null || type == TomlType.OFFSET_DATE_TIME; + } + + @Override + public boolean containsLocalDateTimes() { + return type == null || type == TomlType.LOCAL_DATE_TIME; + } + + @Override + public boolean containsLocalDates() { + return type == null || type == TomlType.LOCAL_DATE; + } + + @Override + public boolean containsLocalTimes() { + return type == null || type == TomlType.LOCAL_TIME; + } + + @Override + public boolean containsArrays() { + return type == null || type == TomlType.ARRAY; + } + + @Override + public boolean containsTables() { + return type == null || type == TomlType.TABLE; + } + + @Override + public Object get(int index) { + return elements.get(index).value; + } + + @Override + public TomlPosition inputPositionOf(int index) { + return elements.get(index).position; + } + + MutableTomlArray append(Object value, TomlPosition position) { + requireNonNull(value); + if (value instanceof Integer) { + value = ((Integer) value).longValue(); + } + + TomlType origType = type; + Optional valueType = typeFor(value); + if (!valueType.isPresent()) { + throw new IllegalArgumentException("Unsupported type " + value.getClass().getSimpleName()); + } + if (type != null) { + if (valueType.get() != type) { + throw new TomlInvalidTypeException( + "Cannot add a " + typeNameFor(value) + " to an array containing " + type.typeName() + "s"); + } + } else { + type = valueType.get(); + } + + try { + elements.add(new Element(value, position)); + } catch (Throwable e) { + type = origType; + throw e; + } + return this; + } + + @Override + public List toList() { + return elements.stream().map(e -> e.value).collect(Collectors.toList()); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/MutableTomlTable.java b/toml/src/main/java/net/consensys/cava/toml/MutableTomlTable.java new file mode 100644 index 00000000..16b38182 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/MutableTomlTable.java @@ -0,0 +1,236 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.Parser.parseDottedKey; +import static net.consensys.cava.toml.TomlPosition.positionAt; +import static net.consensys.cava.toml.TomlType.typeFor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +final class MutableTomlTable implements TomlTable { + + private static class Element { + final Object value; + final TomlPosition position; + + private Element(Object value, TomlPosition position) { + this.value = value; + this.position = position; + } + } + + static final TomlTable EMPTY = new MutableTomlTable(true); + private Map properties = new HashMap<>(); + private boolean implicitlyDefined; + + MutableTomlTable() { + this(false); + } + + private MutableTomlTable(boolean implicitlyDefined) { + this.implicitlyDefined = implicitlyDefined; + } + + @Override + public int size() { + return properties.size(); + } + + @Override + public boolean isEmpty() { + return properties.isEmpty(); + } + + @Override + public Set keySet() { + return properties.keySet(); + } + + @Override + public Set> keyPathSet(boolean includeTables) { + return properties.entrySet().stream().flatMap(entry -> { + String key = entry.getKey(); + List basePath = Collections.singletonList(key); + + Element element = entry.getValue(); + if (!(element.value instanceof TomlTable)) { + return Stream.of(basePath); + } + + Stream> subKeys = ((TomlTable) element.value).keyPathSet(includeTables).stream().map(subPath -> { + List path = new ArrayList<>(subPath.size() + 1); + path.add(key); + path.addAll(subPath); + return path; + }); + + if (includeTables) { + return Stream.concat(Stream.of(basePath), subKeys); + } else { + return subKeys; + } + }).collect(Collectors.toSet()); + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public Object get(List path) { + if (path.isEmpty()) { + return this; + } + Element element = getElement(path); + return (element != null) ? element.value : null; + } + + @Override + @Nullable + public TomlPosition inputPositionOf(List path) { + if (path.isEmpty()) { + return positionAt(1, 1); + } + Element element = getElement(path); + return (element != null) ? element.position : null; + } + + private Element getElement(List path) { + MutableTomlTable table = this; + int depth = path.size(); + assert depth > 0; + for (int i = 0; i < (depth - 1); ++i) { + Element element = table.properties.get(path.get(i)); + if (element == null) { + return null; + } + if (element.value instanceof MutableTomlTable) { + table = (MutableTomlTable) element.value; + continue; + } + return null; + } + return table.properties.get(path.get(depth - 1)); + } + + @Override + public Map toMap() { + return properties.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> e.getValue().value)); + } + + MutableTomlTable createTable(List path, TomlPosition position) { + if (path.isEmpty()) { + return this; + } + + int depth = path.size(); + MutableTomlTable table = ensureTable(path.subList(0, depth - 1), position, true); + + String key = path.get(depth - 1); + Element element = table.properties.get(key); + if (element == null) { + MutableTomlTable newTable = new MutableTomlTable(); + table.properties.put(key, new Element(newTable, position)); + return newTable; + } + if (element.value instanceof MutableTomlTable) { + table = (MutableTomlTable) element.value; + if (table.implicitlyDefined) { + table.implicitlyDefined = false; + table.properties.put(key, new Element(table, position)); + return table; + } + } + String message = Toml.joinKeyPath(path) + " previously defined at " + element.position; + throw new TomlParseError(message, position); + } + + MutableTomlTable createArrayTable(List path, TomlPosition position) { + if (path.isEmpty()) { + throw new IllegalArgumentException("empty path"); + } + + int depth = path.size(); + MutableTomlTable table = ensureTable(path.subList(0, depth - 1), position, true); + + String key = path.get(depth - 1); + Element element = table.properties.computeIfAbsent(key, k -> new Element(new MutableTomlArray(), position)); + if (!(element.value instanceof MutableTomlArray)) { + String message = Toml.joinKeyPath(path) + " is not an array (previously defined at " + element.position + ")"; + throw new TomlParseError(message, position); + } + MutableTomlArray array = (MutableTomlArray) element.value; + if (array.wasDefinedAsLiteral()) { + String message = Toml.joinKeyPath(path) + " previously defined as a literal array at " + element.position; + throw new TomlParseError(message, position); + } + MutableTomlTable newTable = new MutableTomlTable(); + array.append(newTable, position); + return newTable; + } + + MutableTomlTable set(String keyPath, Object value, TomlPosition position) { + return set(parseDottedKey(keyPath), value, position); + } + + MutableTomlTable set(List path, Object value, TomlPosition position) { + int depth = path.size(); + assert (depth > 0); + assert (value != null); + if (value instanceof Integer) { + value = ((Integer) value).longValue(); + } + assert (typeFor(value).isPresent()) : "Unexpected value of type " + value.getClass(); + + MutableTomlTable table = ensureTable(path.subList(0, depth - 1), position, false); + Element prevElem = table.properties.putIfAbsent(path.get(depth - 1), new Element(value, position)); + if (prevElem != null) { + String pathString = Toml.joinKeyPath(path); + String message = pathString + " previously defined at " + prevElem.position; + throw new TomlParseError(message, position); + } + return this; + } + + private MutableTomlTable ensureTable(List path, TomlPosition position, boolean followArrayTables) { + MutableTomlTable table = this; + int depth = path.size(); + for (int i = 0; i < depth; ++i) { + Element element = + table.properties.computeIfAbsent(path.get(i), k -> new Element(new MutableTomlTable(true), position)); + if (element.value instanceof MutableTomlTable) { + table = (MutableTomlTable) element.value; + continue; + } + if (followArrayTables && element.value instanceof MutableTomlArray) { + MutableTomlArray array = (MutableTomlArray) element.value; + if (!array.wasDefinedAsLiteral() && !array.isEmpty() && array.containsTables()) { + table = (MutableTomlTable) array.get(array.size() - 1); + continue; + } + } + String message = + Toml.joinKeyPath(path.subList(0, i + 1)) + " is not a table (previously defined at " + element.position + ")"; + throw new TomlParseError(message, position); + } + return table; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/Parser.java b/toml/src/main/java/net/consensys/cava/toml/Parser.java new file mode 100644 index 00000000..5a8c1f4e --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/Parser.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlLexer; +import net.consensys.cava.toml.internal.TomlParser; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; + +final class Parser { + private Parser() {} + + static TomlParseResult parse(CharStream stream, TomlVersion version) { + TomlLexer lexer = new TomlLexer(stream); + TomlParser parser = new TomlParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + AccumulatingErrorListener errorListener = new AccumulatingErrorListener(); + parser.addErrorListener(errorListener); + ParseTree tree = parser.toml(); + TomlTable table = tree.accept(new LineVisitor(errorListener, version)); + + return new TomlParseResult() { + @Override + public int size() { + return table.size(); + } + + @Override + public boolean isEmpty() { + return table.isEmpty(); + } + + @Override + public Set keySet() { + return table.keySet(); + } + + @Override + public Set> keyPathSet(boolean includeTables) { + return table.keyPathSet(includeTables); + } + + @Override + @Nullable + public Object get(List path) { + return table.get(path); + } + + @Override + @Nullable + public TomlPosition inputPositionOf(List path) { + return table.inputPositionOf(path); + } + + @Override + public Map toMap() { + return table.toMap(); + } + + @Override + public List errors() { + return errorListener.errors(); + } + }; + } + + static List parseDottedKey(String dottedKey) { + TomlLexer lexer = new TomlLexer(CharStreams.fromString(dottedKey)); + lexer.mode(TomlLexer.KeyMode); + TomlParser parser = new TomlParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + AccumulatingErrorListener errorListener = new AccumulatingErrorListener(); + parser.addErrorListener(errorListener); + List keyList = parser.tomlKey().accept(new KeyVisitor()); + List errors = errorListener.errors(); + if (!errors.isEmpty()) { + TomlParseError e = errors.get(0); + throw new IllegalArgumentException("Invalid key: " + e.getMessage(), e); + } + return keyList; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/QuotedStringVisitor.java b/toml/src/main/java/net/consensys/cava/toml/QuotedStringVisitor.java new file mode 100644 index 00000000..2c2ed32d --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/QuotedStringVisitor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.BasicUnescapedContext; +import net.consensys.cava.toml.internal.TomlParser.EscapedContext; +import net.consensys.cava.toml.internal.TomlParser.LiteralBodyContext; +import net.consensys.cava.toml.internal.TomlParser.MlBasicUnescapedContext; +import net.consensys.cava.toml.internal.TomlParser.MlLiteralBodyContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +final class QuotedStringVisitor extends TomlParserBaseVisitor { + + private final StringBuilder builder = new StringBuilder(); + + @Override + public StringBuilder visitLiteralBody(LiteralBodyContext ctx) { + return builder.append(ctx.getText()); + } + + @Override + public StringBuilder visitMlLiteralBody(MlLiteralBodyContext ctx) { + return builder.append(ctx.getText()); + } + + @Override + public StringBuilder visitBasicUnescaped(BasicUnescapedContext ctx) { + return builder.append(ctx.getText()); + } + + @Override + public StringBuilder visitMlBasicUnescaped(MlBasicUnescapedContext ctx) { + return builder.append(ctx.getText()); + } + + @Override + public StringBuilder visitEscaped(EscapedContext ctx) { + String text = ctx.getText(); + if (text.isEmpty()) { + return builder; + } + assert (text.charAt(0) == '\\'); + if (text.length() == 1) { + return builder.append('\\'); + } + switch (text.charAt(1)) { + case '"': + return builder.append('"'); + case '\\': + return builder.append('\\'); + case 'b': + return builder.append('\b'); + case 'f': + return builder.append('\f'); + case 'n': + return builder.append('\n'); + case 'r': + return builder.append('\r'); + case 't': + return builder.append('\t'); + case 'u': + assert (text.length() == 6); + return builder.append(convertUnicodeEscape(text.substring(2), ctx)); + case 'U': + assert (text.length() == 10); + return builder.append(convertUnicodeEscape(text.substring(2), ctx)); + default: + throw new TomlParseError("Invalid escape sequence '" + text + "'", new TomlPosition(ctx)); + } + } + + private char[] convertUnicodeEscape(String hexChars, EscapedContext ctx) { + try { + return Character.toChars(Integer.parseInt(hexChars, 16)); + } catch (IllegalArgumentException e) { + throw new TomlParseError("Invalid unicode escape sequence", new TomlPosition(ctx)); + } + } + + @Override + protected StringBuilder aggregateResult(StringBuilder aggregate, StringBuilder nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected StringBuilder defaultResult() { + return builder; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TokenName.java b/toml/src/main/java/net/consensys/cava/toml/TokenName.java new file mode 100644 index 00000000..683d4186 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TokenName.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlLexer; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.stream.Stream; + +enum TokenName { + // Ordered by display preference + LOWER_ALPHA("a-z", TomlLexer.UnquotedKey), + UPPER_ALPHA("A-Z", TomlLexer.UnquotedKey), + DIGITS("0-9", TomlLexer.UnquotedKey), + ARRAY_END("]", TomlLexer.ArrayEnd, TomlLexer.TableKeyEnd), + ARRAY_TABLE_END("]]", TomlLexer.ArrayTableKeyEnd), + INLINE_TABLE_END("}", TomlLexer.InlineTableEnd), + DOT(".", TomlLexer.Dot), + DASH("-", TomlLexer.Dash), + PLUS("+", TomlLexer.Plus), + COLON(":", TomlLexer.Colon), + EQUALS("=", TomlLexer.Equals), + COMMA("a comma", TomlLexer.Comma), + Z("Z", TomlLexer.Z), + APOSTROPHE("'", TomlLexer.Apostrophe, TomlLexer.MLLiteralStringEnd), + QUOTATION_MARK("\"", TomlLexer.QuotationMark, TomlLexer.MLBasicStringEnd), + TRIPLE_APOSTROPHE("'''", TomlLexer.TripleApostrophe), + TRIPLE_QUOTATION_MARK("\"\"\"", TomlLexer.TripleQuotationMark), + CHARACTER("a character", TomlLexer.EscapeSequence, TomlLexer.StringChar), + NUMBER("a number", TomlLexer.DecimalInteger, TomlLexer.BinaryInteger, TomlLexer.OctalInteger, TomlLexer.HexInteger, + TomlLexer.FloatingPoint, TomlLexer.FloatingPointInf, TomlLexer.FloatingPointNaN), + BOOLEAN("a boolean", TomlLexer.TrueBoolean, TomlLexer.FalseBoolean), + DATETIME("a date/time", TomlLexer.DateDigits), + TIME("a time", TomlLexer.TimeDelimiter), + ARRAY("an array", TomlLexer.ArrayStart), + INLINE_TABLE("a table", TomlLexer.InlineTableStart), + TABLE("a table key", TomlLexer.TableKeyStart, TomlLexer.ArrayTableKeyStart), + NEWLINE("a newline", TomlLexer.NewLine), + EOF("end-of-input", TomlLexer.EOF), + NULL("NULL", 0, TomlLexer.WS, TomlLexer.Comment, TomlLexer.Error); + + private final String displayName; + @SuppressWarnings("ImmutableEnumChecker") + private final BitSet tokenTypes; + + TokenName(String displayName, int... tokenTypes) { + this.displayName = displayName; + // offset by 1 to account for EOF being -1 (moves it to zero) + this.tokenTypes = new BitSet(TomlLexer.VOCABULARY.getMaxTokenType() + 1); + for (int type : tokenTypes) { + this.tokenTypes.set(type + 1); + } + } + + static Stream namesForToken(int tokenType) { + return Arrays.stream(TokenName.values()).filter(n -> n.tokenTypes.get(tokenType + 1)); + } + + public String displayName() { + return displayName; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/Toml.java b/toml/src/main/java/net/consensys/cava/toml/Toml.java new file mode 100644 index 00000000..7c167aa5 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/Toml.java @@ -0,0 +1,246 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; +import java.util.List; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; + +/** + * Methods for parsing data stored in Tom's Obvious, Minimal Language (TOML). + */ +public final class Toml { + private static final Pattern simpleKeyPattern = Pattern.compile("^[A-Za-z0-9_-]+$"); + + private Toml() {} + + /** + * Parse a TOML string. + * + * @param input The input to parse. + * @return The parse result. + */ + public static TomlParseResult parse(String input) { + return parse(input, TomlVersion.LATEST); + } + + /** + * Parse a TOML string. + * + * @param input The input to parse. + * @param version The version level to parse at. + * @return The parse result. + */ + public static TomlParseResult parse(String input, TomlVersion version) { + CharStream stream = CharStreams.fromString(input); + return Parser.parse(stream, version.canonical); + } + + /** + * Parse a TOML file. + * + * @param file The input file to parse. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(Path file) throws IOException { + return parse(file, TomlVersion.LATEST); + } + + /** + * Parse a TOML file. + * + * @param file The input file to parse. + * @param version The version level to parse at. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(Path file, TomlVersion version) throws IOException { + CharStream stream = CharStreams.fromPath(file); + return Parser.parse(stream, version.canonical); + } + + /** + * Parse a TOML input stream. + * + * @param is The input stream to read the TOML document from. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(InputStream is) throws IOException { + return parse(is, TomlVersion.LATEST); + } + + /** + * Parse a TOML input stream. + * + * @param is The input stream to read the TOML document from. + * @param version The version level to parse at. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(InputStream is, TomlVersion version) throws IOException { + CharStream stream = CharStreams.fromStream(is); + return Parser.parse(stream, version.canonical); + } + + /** + * Parse a TOML reader. + * + * @param reader The reader to obtain the TOML document from. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(Reader reader) throws IOException { + return parse(reader, TomlVersion.LATEST); + } + + /** + * Parse a TOML input stream. + * + * @param reader The reader to obtain the TOML document from. + * @param version The version level to parse at. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(Reader reader, TomlVersion version) throws IOException { + CharStream stream = CharStreams.fromReader(reader); + return Parser.parse(stream, version.canonical); + } + + /** + * Parse a TOML reader. + * + * @param channel The channel to read the TOML document from. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(ReadableByteChannel channel) throws IOException { + return parse(channel, TomlVersion.LATEST); + } + + /** + * Parse a TOML input stream. + * + * @param channel The channel to read the TOML document from. + * @param version The version level to parse at. + * @return The parse result. + * @throws IOException If an IO error occurs. + */ + public static TomlParseResult parse(ReadableByteChannel channel, TomlVersion version) throws IOException { + CharStream stream = CharStreams.fromChannel(channel); + return Parser.parse(stream, version.canonical); + } + + /** + * Parse a dotted key into individual parts. + * + * @param dottedKey A dotted key (e.g. {@code server.address.port}). + * @return A list of individual keys in the path. + * @throws IllegalArgumentException If the dotted key cannot be parsed. + */ + public static List parseDottedKey(String dottedKey) { + requireNonNull(dottedKey); + return Parser.parseDottedKey(dottedKey); + } + + /** + * Join a list of keys into a single dotted key string. + * + * @param path The list of keys that form the path. + * @return The path string. + */ + public static String joinKeyPath(List path) { + requireNonNull(path); + + StringJoiner joiner = new StringJoiner("."); + for (String key : path) { + if (simpleKeyPattern.matcher(key).matches()) { + joiner.add(key); + } else { + joiner.add("\"" + tomlEscape(key) + '\"'); + } + } + return joiner.toString(); + } + + /** + * Get the canonical form of the dotted key. + * + * @param dottedKey A dotted key (e.g. {@code server.address.port}). + * @return The canonical form of the dotted key. + * @throws IllegalArgumentException If the dotted key cannot be parsed. + */ + public static String canonicalDottedKey(String dottedKey) { + return joinKeyPath(parseDottedKey(dottedKey)); + } + + /** + * Escape a text string using the TOML escape sequences. + * + * @param text The text string to escape. + * @return A {@link StringBuilder} holding the results of escaping the text. + */ + public static StringBuilder tomlEscape(String text) { + final StringBuilder out = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + int codepoint = text.codePointAt(i); + if (Character.charCount(codepoint) > 1) { + out.append("\\U").append(String.format("%08x", codepoint)); + ++i; + continue; + } + + char ch = Character.toChars(codepoint)[0]; + if (ch == '\'') { + out.append("\\'"); + continue; + } + if (ch >= 0x20 && ch < 0x7F) { + out.append(ch); + continue; + } + + switch (ch) { + case '\t': + out.append("\\t"); + break; + case '\b': + out.append("\\b"); + break; + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\f': + out.append("\\f"); + break; + default: + out.append("\\u").append(String.format("%04x", codepoint)); + } + } + return out; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlArray.java b/toml/src/main/java/net/consensys/cava/toml/TomlArray.java new file mode 100644 index 00000000..f6435453 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlArray.java @@ -0,0 +1,304 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.TomlType.typeNameFor; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.util.List; + +/** + * An array of TOML values. + */ +public interface TomlArray { + + /** + * @return The size of the array. + */ + int size(); + + /** + * @return true if the array is empty. + */ + boolean isEmpty(); + + /** + * @return true if the array contains strings. + */ + boolean containsStrings(); + + /** + * @return true if the array contains longs. + */ + boolean containsLongs(); + + /** + * @return true if the array contains doubles. + */ + boolean containsDoubles(); + + /** + * @return true if the array contains booleans. + */ + boolean containsBooleans(); + + /** + * @return true if the array contains {@link OffsetDateTime}s. + */ + boolean containsOffsetDateTimes(); + + /** + * @return true if the array contains {@link LocalDateTime}s. + */ + boolean containsLocalDateTimes(); + + /** + * @return true if the array contains {@link LocalDate}s. + */ + boolean containsLocalDates(); + + /** + * @return true if the array contains {@link LocalTime}s. + */ + boolean containsLocalTimes(); + + /** + * @return true if the array contains arrays. + */ + boolean containsArrays(); + + /** + * @return true if the array contains tables. + */ + boolean containsTables(); + + /** + * Get a value at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + */ + Object get(int index); + + /** + * Get the position where a value is defined in the TOML document. + * + * @param index The array index. + * @return The input position. + * @throws IndexOutOfBoundsException If the index is out of bounds. + */ + TomlPosition inputPositionOf(int index); + + /** + * Get a string at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not a long. + */ + default String getString(int index) { + Object value = get(index); + if (!(value instanceof String)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (String) value; + } + + /** + * Get a long at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not a long. + */ + default long getLong(int index) { + Object value = get(index); + if (!(value instanceof Long)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (Long) value; + } + + /** + * Get a double at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not a long. + */ + default double getDouble(int index) { + Object value = get(index); + if (!(value instanceof Double)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (Double) value; + } + + /** + * Get a boolean at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not a long. + */ + default boolean getBoolean(int index) { + Object value = get(index); + if (!(value instanceof Boolean)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (Boolean) value; + } + + /** + * Get an offset date time at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not an {@link OffsetDateTime}. + */ + default OffsetDateTime getOffsetDateTime(int index) { + Object value = get(index); + if (!(value instanceof OffsetDateTime)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (OffsetDateTime) value; + } + + /** + * Get a local date time at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not an {@link LocalDateTime}. + */ + default LocalDateTime getLocalDateTime(int index) { + Object value = get(index); + if (!(value instanceof LocalDateTime)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (LocalDateTime) value; + } + + /** + * Get a local date at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not an {@link LocalDate}. + */ + default LocalDate getLocalDate(int index) { + Object value = get(index); + if (!(value instanceof LocalDate)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (LocalDate) value; + } + + /** + * Get a local time at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not an {@link LocalTime}. + */ + default LocalTime getLocalTime(int index) { + Object value = get(index); + if (!(value instanceof LocalTime)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (LocalTime) value; + } + + /** + * Get an array at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not an array. + */ + default TomlArray getArray(int index) { + Object value = get(index); + if (!(value instanceof TomlArray)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (TomlArray) value; + } + + /** + * Get a table at a specified index. + * + * @param index The array index. + * @return The value. + * @throws IndexOutOfBoundsException If the index is out of bounds. + * @throws TomlInvalidTypeException If the value is not a table. + */ + default TomlTable getTable(int index) { + Object value = get(index); + if (!(value instanceof TomlTable)) { + throw new TomlInvalidTypeException("key at index " + index + " is a " + typeNameFor(value)); + } + return (TomlTable) value; + } + + /** + * Get the elements of this array as a {@link List}. + * + *

+ * Note that this does not do a deep conversion. If this array contains tables or arrays, they will be of type + * {@link TomlTable} or {@link TomlArray} respectively. + * + * @return The elements of this array as a {@link List}. + */ + List toList(); + + /** + * Return a representation of this array using JSON. + * + * @return A JSON representation of this array. + */ + default String toJson() { + StringBuilder builder = new StringBuilder(); + try { + toJson(builder); + } catch (IOException e) { + // not reachable + throw new UncheckedIOException(e); + } + return builder.toString(); + } + + /** + * Append a JSON representation of this array to the appendable output. + * + * @param appendable The appendable output. + * @throws IOException If an IO error occurs. + */ + default void toJson(Appendable appendable) throws IOException { + JsonSerializer.toJson(this, appendable); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlInvalidTypeException.java b/toml/src/main/java/net/consensys/cava/toml/TomlInvalidTypeException.java new file mode 100644 index 00000000..11b9438d --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlInvalidTypeException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +/** + * An exception thrown when an invalid type is encountered. + */ +public class TomlInvalidTypeException extends RuntimeException { + + TomlInvalidTypeException(String message) { + super(message); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlParseError.java b/toml/src/main/java/net/consensys/cava/toml/TomlParseError.java new file mode 100644 index 00000000..18d4f54c --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlParseError.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +/** + * An error that occurred while parsing. + */ +public final class TomlParseError extends RuntimeException { + + private final TomlPosition position; + + TomlParseError(String message, TomlPosition position) { + super(message); + this.position = position; + } + + TomlParseError(String message, TomlPosition position, Throwable cause) { + super(message, cause); + this.position = position; + } + + /** + * @return The position in the input where the error occurred. + */ + public TomlPosition position() { + return position; + } + + @Override + public String toString() { + return getMessage() + " (" + position + ")"; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlParseResult.java b/toml/src/main/java/net/consensys/cava/toml/TomlParseResult.java new file mode 100644 index 00000000..9594888c --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlParseResult.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import java.util.List; + +/** + * The result from parsing a TOML document. + */ +public interface TomlParseResult extends TomlTable { + + /** + * @return true if the TOML document contained errors. + */ + default boolean hasErrors() { + return !(errors().isEmpty()); + } + + /** + * The errors that occurred during parsing. + * + * @return A list of errors. + */ + List errors(); +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlPosition.java b/toml/src/main/java/net/consensys/cava/toml/TomlPosition.java new file mode 100644 index 00000000..b3537ddc --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlPosition.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; + +/** + * A position in an input document. + */ +public final class TomlPosition { + private final int line; + private final int column; + + /** + * Create a position. + * + * @param line The line. + * @param column The column. + * @return A position. + */ + public static TomlPosition positionAt(int line, int column) { + if (line < 1) { + throw new IllegalArgumentException("line must be >= 1"); + } + if (column < 1) { + throw new IllegalArgumentException("column must be >= 1"); + } + return new TomlPosition(line, column); + } + + private TomlPosition(int line, int column) { + this.line = line; + this.column = column; + } + + TomlPosition(ParserRuleContext ctx) { + this(ctx, 0); + } + + TomlPosition(ParserRuleContext ctx, int offset) { + Token token = ctx.getStart(); + this.line = token.getLine(); + this.column = token.getCharPositionInLine() + 1 + offset; + } + + /** + * The line number. + * + *

+ * The first line of the document is line 1. + * + * @return The line number (1..). + */ + public int line() { + return line; + } + + /** + * The column number. + * + *

+ * The first column of the document is column 1. + * + * @return The column number (1..). + */ + public int column() { + return column; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TomlPosition)) { + return false; + } + TomlPosition other = (TomlPosition) obj; + return this.line == other.line && this.column == other.column; + } + + @Override + public int hashCode() { + return 31 * line + column; + } + + @Override + public String toString() { + return "line " + line + ", column " + column; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlTable.java b/toml/src/main/java/net/consensys/cava/toml/TomlTable.java new file mode 100644 index 00000000..c9d7a554 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlTable.java @@ -0,0 +1,1168 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static java.util.Objects.requireNonNull; +import static net.consensys.cava.toml.Parser.parseDottedKey; +import static net.consensys.cava.toml.Toml.joinKeyPath; +import static net.consensys.cava.toml.TomlType.typeNameFor; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.DoubleSupplier; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * An interface for accessing data stored in Tom's Obvious, Minimal Language (TOML). + */ +public interface TomlTable { + + /** + * @return The number of entries in tis table. + */ + int size(); + + /** + * @return true if there are no entries in this table. + */ + boolean isEmpty(); + + /** + * Check if a key was set in the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.port"}). + * @return true if the key was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean contains(String dottedKey) { + requireNonNull(dottedKey); + return contains(parseDottedKey(dottedKey)); + } + + /** + * Check if a key was set in the TOML document. + * + * @param path The key path. + * @return true if the key was set in the TOML document. + */ + default boolean contains(List path) { + try { + return get(path) != null; + } catch (TomlInvalidTypeException e) { + return false; + } + } + + /** + * Get the keys of this table. + * + *

+ * The returned set contains only immediate keys to this table, and not dotted keys or key paths. For a complete view + * of keys available in the TOML document, use {@link #dottedKeySet()} or {@link #keyPathSet()}. + * + * @return A set containing the keys of this table. + */ + Set keySet(); + + /** + * Get all the dotted keys of this table. + * + *

+ * Paths to intermediary and empty tables are not returned. To include these, use {@link #dottedKeySet(boolean)}. + * + * @return A set containing all the dotted keys of this table. + */ + default Set dottedKeySet() { + return keyPathSet().stream().map(Toml::joinKeyPath).collect(Collectors.toSet()); + } + + /** + * Get all the dotted keys of this table. + * + * @param includeTables If true, also include paths to intermediary and empty tables. + * @return A set containing all the dotted keys of this table. + */ + default Set dottedKeySet(boolean includeTables) { + return keyPathSet(includeTables).stream().map(Toml::joinKeyPath).collect(Collectors.toSet()); + } + + /** + * Get all the paths in this table. + * + *

+ * Paths to intermediary and empty tables are not returned. To include these, use {@link #keyPathSet(boolean)}. + * + * @return A set containing all the key paths of this table. + */ + default Set> keyPathSet() { + return keyPathSet(false); + } + + /** + * Get all the paths in this table. + * + * @param includeTables If true, also include paths to intermediary and empty tables. + * @return A set containing all the key paths of this table. + */ + Set> keyPathSet(boolean includeTables); + + /** + * Get a value from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If any element of the path preceding the final key is not a table. + */ + @Nullable + default Object get(String dottedKey) { + requireNonNull(dottedKey); + return get(parseDottedKey(dottedKey)); + } + + /** + * Get a value from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If any element of the path preceding the final key is not a table. + */ + @Nullable + Object get(List path); + + /** + * Get the position where a key is defined in the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The input position, or null if the key was not set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If any element of the path preceding the final key is not a table. + */ + @Nullable + default TomlPosition inputPositionOf(String dottedKey) { + requireNonNull(dottedKey); + return inputPositionOf(parseDottedKey(dottedKey)); + } + + /** + * Get the position where a key is defined in the TOML document. + * + * @param path The key path. + * @return The input position, or null if the key was not set in the TOML document. + * @throws TomlInvalidTypeException If any element of the path preceding the final key is not a table. + */ + @Nullable + TomlPosition inputPositionOf(List path); + + /** + * Check if a value in the TOML document is a string. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.hostname"}). + * @return true if the value can be obtained as a string. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isString(String dottedKey) { + requireNonNull(dottedKey); + return isString(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a string. + * + * @param path The key path. + * @return true if the value can be obtained as a string. + */ + default boolean isString(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof String; + } + + /** + * Get a string from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.hostname"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a string, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default String getString(String dottedKey) { + requireNonNull(dottedKey); + return getString(parseDottedKey(dottedKey)); + } + + /** + * Get a string from the TOML document. + * + * @param path A dotted key (e.g. {@code "server.address.hostname"}). + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a string, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default String getString(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof String)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (String) value; + } + + /** + * Get a string from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.hostname"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a string, or any element of the path preceding the + * final key is not a table. + */ + default String getString(String dottedKey, Supplier defaultValue) { + requireNonNull(dottedKey); + return getString(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a string from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a string, or any element of the path preceding the + * final key is not a table. + */ + default String getString(List path, Supplier defaultValue) { + requireNonNull(defaultValue); + String value = getString(path); + if (value != null) { + return value; + } + return defaultValue.get(); + } + + /** + * Check if a value in the TOML document is a long. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a long. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isLong(String dottedKey) { + requireNonNull(dottedKey); + return isLong(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a long. + * + * @param path The key path. + * @return true if the value can be obtained as a long. + */ + default boolean isLong(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof Long; + } + + /** + * Get a long from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a long, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default Long getLong(String dottedKey) { + requireNonNull(dottedKey); + return getLong(parseDottedKey(dottedKey)); + } + + /** + * Get a long from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a long, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default Long getLong(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof Long)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (Long) value; + } + + /** + * Get a long from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a long, or any element of the path preceding the + * final key is not a table. + */ + default long getLong(String dottedKey, LongSupplier defaultValue) { + requireNonNull(dottedKey); + return getLong(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a long from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a long, or any element of the path preceding the + * final key is not a table. + */ + default long getLong(List path, LongSupplier defaultValue) { + requireNonNull(defaultValue); + Long value = getLong(path); + if (value != null) { + return value; + } + return defaultValue.getAsLong(); + } + + /** + * Check if a value in the TOML document is a double. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a double. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isDouble(String dottedKey) { + requireNonNull(dottedKey); + return isDouble(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a double. + * + * @param path The key path. + * @return true if the value can be obtained as a double. + */ + default boolean isDouble(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof Double; + } + + /** + * Get a double from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a double, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default Double getDouble(String dottedKey) { + requireNonNull(dottedKey); + return getDouble(parseDottedKey(dottedKey)); + } + + /** + * Get a double from the TOML document. + * + * @param path A dotted key. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a double, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default Double getDouble(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof Double)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (Double) value; + } + + /** + * Get a double from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a double, or any element of the path preceding the + * final key is not a table. + */ + default double getDouble(String dottedKey, DoubleSupplier defaultValue) { + requireNonNull(dottedKey); + return getDouble(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a double from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a double, or any element of the path preceding the + * final key is not a table. + */ + default double getDouble(List path, DoubleSupplier defaultValue) { + requireNonNull(defaultValue); + Double value = getDouble(path); + if (value != null) { + return value; + } + return defaultValue.getAsDouble(); + } + + /** + * Check if a value in the TOML document is a boolean. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a boolean. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isBoolean(String dottedKey) { + requireNonNull(dottedKey); + return isBoolean(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a boolean. + * + * @param path The key path. + * @return true if the value can be obtained as a boolean. + */ + default boolean isBoolean(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof Boolean; + } + + /** + * Get a boolean from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a boolean, or any element of the path preceding + * the final key is not a table. + */ + @Nullable + default Boolean getBoolean(String dottedKey) { + requireNonNull(dottedKey); + return getBoolean(parseDottedKey(dottedKey)); + } + + /** + * Get a boolean from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a boolean, or any element of the path preceding + * the final key is not a table. + */ + @Nullable + default Boolean getBoolean(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof Boolean)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (Boolean) value; + } + + /** + * Get a boolean from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a boolean, or any element of the path preceding + * the final key is not a table. + */ + default boolean getBoolean(String dottedKey, BooleanSupplier defaultValue) { + requireNonNull(dottedKey); + return getBoolean(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a boolean from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a boolean, or any element of the path preceding + * the final key is not a table. + */ + default boolean getBoolean(List path, BooleanSupplier defaultValue) { + requireNonNull(defaultValue); + Boolean value = getBoolean(path); + if (value != null) { + return value; + } + return defaultValue.getAsBoolean(); + } + + /** + * Check if a value in the TOML document is an {@link OffsetDateTime}. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as an {@link OffsetDateTime}. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isOffsetDateTime(String dottedKey) { + requireNonNull(dottedKey); + return isOffsetDateTime(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is an {@link OffsetDateTime}. + * + * @param path The key path. + * @return true if the value can be obtained as an {@link OffsetDateTime}. + */ + default boolean isOffsetDateTime(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof OffsetDateTime; + } + + /** + * Get an offset date time from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not an {@link OffsetDateTime}, or any element of the + * path preceding the final key is not a table. + */ + @Nullable + default OffsetDateTime getOffsetDateTime(String dottedKey) { + requireNonNull(dottedKey); + return getOffsetDateTime(parseDottedKey(dottedKey)); + } + + /** + * Get an offset date time from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not an {@link OffsetDateTime}, or any element of the + * path preceding the final key is not a table. + */ + @Nullable + default OffsetDateTime getOffsetDateTime(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof OffsetDateTime)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (OffsetDateTime) value; + } + + /** + * Get an offset date time from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not an {@link OffsetDateTime}, or any element of the + * path preceding the final key is not a table. + */ + default OffsetDateTime getOffsetDateTime(String dottedKey, Supplier defaultValue) { + requireNonNull(dottedKey); + return getOffsetDateTime(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get an offset date time from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not an {@link OffsetDateTime}, or any element of the + * path preceding the final key is not a table. + */ + default OffsetDateTime getOffsetDateTime(List path, Supplier defaultValue) { + requireNonNull(defaultValue); + OffsetDateTime value = getOffsetDateTime(path); + if (value != null) { + return value; + } + return defaultValue.get(); + } + + /** + * Check if a value in the TOML document is a {@link LocalDateTime}. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a {@link LocalDateTime}. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isLocalDateTime(String dottedKey) { + requireNonNull(dottedKey); + return isLocalDateTime(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a {@link LocalDateTime}. + * + * @param path The key path. + * @return true if the value can be obtained as a {@link LocalDateTime}. + */ + default boolean isLocalDateTime(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof LocalDateTime; + } + + /** + * Get a local date time from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDateTime}, or any element of the + * path preceding the final key is not a table. + */ + @Nullable + default LocalDateTime getLocalDateTime(String dottedKey) { + requireNonNull(dottedKey); + return getLocalDateTime(parseDottedKey(dottedKey)); + } + + /** + * Get a local date time from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDateTime}, or any element of the + * path preceding the final key is not a table. + */ + @Nullable + default LocalDateTime getLocalDateTime(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof LocalDateTime)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (LocalDateTime) value; + } + + /** + * Get a local date time from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDateTime}, or any element of the + * path preceding the final key is not a table. + */ + default LocalDateTime getLocalDateTime(String dottedKey, Supplier defaultValue) { + requireNonNull(dottedKey); + return getLocalDateTime(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a local date time from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDateTime}, or any element of the + * path preceding the final key is not a table. + */ + default LocalDateTime getLocalDateTime(List path, Supplier defaultValue) { + requireNonNull(defaultValue); + LocalDateTime value = getLocalDateTime(path); + if (value != null) { + return value; + } + return defaultValue.get(); + } + + /** + * Check if a value in the TOML document is a {@link LocalDate}. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a {@link LocalDate}. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isLocalDate(String dottedKey) { + requireNonNull(dottedKey); + return isLocalDate(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a {@link LocalDate}. + * + * @param path The key path. + * @return true if the value can be obtained as a {@link LocalDate}. + */ + default boolean isLocalDate(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof LocalDate; + } + + /** + * Get a local date from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDate}, or any element of the path + * preceding the final key is not a table. + */ + @Nullable + default LocalDate getLocalDate(String dottedKey) { + requireNonNull(dottedKey); + return getLocalDate(parseDottedKey(dottedKey)); + } + + /** + * Get a local date from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDate}, or any element of the path + * preceding the final key is not a table. + */ + @Nullable + default LocalDate getLocalDate(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof LocalDate)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (LocalDate) value; + } + + /** + * Get a local date from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDate}, or any element of the path + * preceding the final key is not a table. + */ + default LocalDate getLocalDate(String dottedKey, Supplier defaultValue) { + requireNonNull(dottedKey); + return getLocalDate(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a local date from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalDate}, or any element of the path + * preceding the final key is not a table. + */ + default LocalDate getLocalDate(List path, Supplier defaultValue) { + requireNonNull(defaultValue); + LocalDate value = getLocalDate(path); + if (value != null) { + return value; + } + return defaultValue.get(); + } + + /** + * Check if a value in the TOML document is a {@link LocalTime}. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return true if the value can be obtained as a {@link LocalTime}. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isLocalTime(String dottedKey) { + requireNonNull(dottedKey); + return isLocalTime(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a {@link LocalTime}. + * + * @param path The key path. + * @return true if the value can be obtained as a {@link LocalTime}. + */ + default boolean isLocalTime(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof LocalTime; + } + + /** + * Get a local time from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalTime}, or any element of the path + * preceding the final key is not a table. + */ + @Nullable + default LocalTime getLocalTime(String dottedKey) { + requireNonNull(dottedKey); + return getLocalTime(parseDottedKey(dottedKey)); + } + + /** + * Get a local time from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalTime}, or any element of the path + * preceding the final key is not a table. + */ + @Nullable + default LocalTime getLocalTime(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof LocalTime)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (LocalTime) value; + } + + /** + * Get a local time from the TOML document, or return a default. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalTime}, or any element of the path + * preceding the final key is not a table. + */ + default LocalTime getLocalTime(String dottedKey, Supplier defaultValue) { + requireNonNull(dottedKey); + return getLocalTime(parseDottedKey(dottedKey), defaultValue); + } + + /** + * Get a local time from the TOML document, or return a default. + * + * @param path The key path. + * @param defaultValue A supplier for the default value. + * @return The value, or the default. + * @throws TomlInvalidTypeException If the value is present but not a {@link LocalTime}, or any element of the path + * preceding the final key is not a table. + */ + default LocalTime getLocalTime(List path, Supplier defaultValue) { + requireNonNull(defaultValue); + LocalTime value = getLocalTime(path); + if (value != null) { + return value; + } + return defaultValue.get(); + } + + /** + * Check if a value in the TOML document is an array. + * + * @param dottedKey A dotted key (e.g. {@code "server.addresses"}). + * @return true if the value can be obtained as an array. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isArray(String dottedKey) { + requireNonNull(dottedKey); + return isArray(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is an array. + * + * @param path The key path. + * @return true if the value can be obtained as an array. + */ + default boolean isArray(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof TomlArray; + } + + /** + * Get an array from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.addresses"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not an array, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default TomlArray getArray(String dottedKey) { + requireNonNull(dottedKey); + return getArray(parseDottedKey(dottedKey)); + } + + /** + * Get an array from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not an array, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default TomlArray getArray(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof TomlArray)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (TomlArray) value; + } + + /** + * Get an array from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.addresses"}). + * @return The value, or an empty list if no list was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not an array, or any element of the path preceding the + * final key is not a table. + */ + default TomlArray getArrayOrEmpty(String dottedKey) { + requireNonNull(dottedKey); + return getArrayOrEmpty(parseDottedKey(dottedKey)); + } + + /** + * Get an array from the TOML document. + * + * @param path The key path. + * @return The value, or an empty list if no list was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not an array, or any element of the path preceding the + * final key is not a table. + */ + default TomlArray getArrayOrEmpty(List path) { + TomlArray value = getArray(path); + if (value != null) { + return value; + } + return MutableTomlArray.EMPTY; + } + + /** + * Check if a value in the TOML document is a table. + * + * @param dottedKey A dotted key (e.g. {@code "server.address"}). + * @return true if the value can be obtained as a table. + * @throws IllegalArgumentException If the key cannot be parsed. + */ + default boolean isTable(String dottedKey) { + requireNonNull(dottedKey); + return isTable(parseDottedKey(dottedKey)); + } + + /** + * Check if a value in the TOML document is a table. + * + * @param path The key path. + * @return true if the value can be obtained as a table. + */ + default boolean isTable(List path) { + Object value; + try { + value = get(path); + } catch (TomlInvalidTypeException e) { + return false; + } + return value instanceof TomlTable; + } + + /** + * Get a table from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address"}). + * @return The value, or null if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a table, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default TomlTable getTable(String dottedKey) { + requireNonNull(dottedKey); + return getTable(parseDottedKey(dottedKey)); + } + + /** + * Get a table from the TOML document. + * + * @param path The key path. + * @return The value, or null if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a table, or any element of the path preceding the + * final key is not a table. + */ + @Nullable + default TomlTable getTable(List path) { + Object value = get(path); + if (value == null) { + return null; + } + if (!(value instanceof TomlTable)) { + throw new TomlInvalidTypeException("Value of '" + joinKeyPath(path) + "' is a " + typeNameFor(value)); + } + return (TomlTable) value; + } + + /** + * Get a table from the TOML document. + * + * @param dottedKey A dotted key (e.g. {@code "server.address.port"}). + * @return The value, or an empty table if no value was set in the TOML document. + * @throws IllegalArgumentException If the key cannot be parsed. + * @throws TomlInvalidTypeException If the value is present but not a table, or any element of the path preceding the + * final key is not a table. + */ + default TomlTable getTableOrEmpty(String dottedKey) { + requireNonNull(dottedKey); + return getTableOrEmpty(parseDottedKey(dottedKey)); + } + + /** + * Get a table from the TOML document. + * + * @param path The key path. + * @return The value, or an empty table if no value was set in the TOML document. + * @throws TomlInvalidTypeException If the value is present but not a table, or any element of the path preceding the + * final key is not a table. + */ + default TomlTable getTableOrEmpty(List path) { + TomlTable value = getTable(path); + if (value != null) { + return value; + } + return MutableTomlTable.EMPTY; + } + + /** + * Get the elements of this array as a {@link Map}. + * + *

+ * Note that this does not do a deep conversion. If this array contains tables or arrays, they will be of type + * {@link TomlTable} or {@link TomlArray} respectively. + * + * @return The elements of this array as a {@link Map}. + */ + Map toMap(); + + /** + * Return a representation of this table using JSON. + * + * @return A JSON representation of this table. + */ + default String toJson() { + StringBuilder builder = new StringBuilder(); + try { + toJson(builder); + } catch (IOException e) { + // not reachable + throw new UncheckedIOException(e); + } + return builder.toString(); + } + + /** + * Append a JSON representation of this table to the appendable output. + * + * @param appendable The appendable output. + * @throws IOException If an IO error occurs. + */ + default void toJson(Appendable appendable) throws IOException { + JsonSerializer.toJson(this, appendable); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlType.java b/toml/src/main/java/net/consensys/cava/toml/TomlType.java new file mode 100644 index 00000000..3602f7d4 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlType.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Optional; + +enum TomlType { + STRING("string", String.class), + INTEGER("integer", Long.class), + FLOAT("float", Double.class), + BOOLEAN("boolean", Boolean.class), + OFFSET_DATE_TIME("offset date-time", OffsetDateTime.class), + LOCAL_DATE_TIME("local date-time", LocalDateTime.class), + LOCAL_DATE("local date", LocalDate.class), + LOCAL_TIME("local time", LocalTime.class), + ARRAY("array", TomlArray.class), + TABLE("table", TomlTable.class); + + private final String name; + private final Class clazz; + + TomlType(String name, Class clazz) { + this.name = name; + this.clazz = clazz; + } + + static Optional typeFor(Object obj) { + return typeForClass(obj.getClass()); + } + + static Optional typeForClass(Class clazz) { + return Arrays.stream(values()).filter(t -> t.clazz.isAssignableFrom(clazz)).findAny(); + } + + static String typeNameFor(Object obj) { + return typeNameForClass(obj.getClass()); + } + + static String typeNameForClass(Class clazz) { + return typeForClass(clazz).map(t -> t.name).orElseGet(clazz::getSimpleName); + } + + public String typeName() { + return name; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/TomlVersion.java b/toml/src/main/java/net/consensys/cava/toml/TomlVersion.java new file mode 100644 index 00000000..30a917f0 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/TomlVersion.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +/** + * Supported TOML specification versions. + */ +public enum TomlVersion { + /** + * The 0.4.0 version of TOML. + * + *

+ * This specification can be found at https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md. + */ + V0_4_0(null), + /** + * The latest stable specification of TOML. + */ + LATEST(V0_4_0), + /** + * The head (development) specification of TOML. + * + *

+ * The latest specification can be found at https://github.com/toml-lang/toml/blob/master/README.md. + * + *

+ * Note: As the specification is under active development, this implementation may not match the latest changes. + */ + HEAD(null); + + final TomlVersion canonical; + + TomlVersion(TomlVersion canonical) { + this.canonical = canonical != null ? canonical : this; + } + + boolean after(TomlVersion other) { + return this.ordinal() > other.ordinal(); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/ValueVisitor.java b/toml/src/main/java/net/consensys/cava/toml/ValueVisitor.java new file mode 100644 index 00000000..10af0db2 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/ValueVisitor.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.ArrayContext; +import net.consensys.cava.toml.internal.TomlParser.ArrayValuesContext; +import net.consensys.cava.toml.internal.TomlParser.BinIntContext; +import net.consensys.cava.toml.internal.TomlParser.DecIntContext; +import net.consensys.cava.toml.internal.TomlParser.FalseBoolContext; +import net.consensys.cava.toml.internal.TomlParser.HexIntContext; +import net.consensys.cava.toml.internal.TomlParser.InlineTableContext; +import net.consensys.cava.toml.internal.TomlParser.InlineTableValuesContext; +import net.consensys.cava.toml.internal.TomlParser.LocalDateContext; +import net.consensys.cava.toml.internal.TomlParser.LocalDateTimeContext; +import net.consensys.cava.toml.internal.TomlParser.LocalTimeContext; +import net.consensys.cava.toml.internal.TomlParser.OctIntContext; +import net.consensys.cava.toml.internal.TomlParser.OffsetDateTimeContext; +import net.consensys.cava.toml.internal.TomlParser.RegularFloatContext; +import net.consensys.cava.toml.internal.TomlParser.StringContext; +import net.consensys.cava.toml.internal.TomlParser.TrueBoolContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.regex.Pattern; + +import org.antlr.v4.runtime.ParserRuleContext; + +final class ValueVisitor extends TomlParserBaseVisitor { + + private static final Pattern zeroFloat = Pattern.compile("[+-]?0+(\\.[+-]?0*)?([eE].*)?"); + + @Override + public Object visitString(StringContext ctx) { + return ctx.accept(new QuotedStringVisitor()).toString(); + } + + @Override + public Object visitDecInt(DecIntContext ctx) { + return toLong(ctx.getText().replaceAll("_", ""), 10, ctx); + } + + @Override + public Object visitHexInt(HexIntContext ctx) { + return toLong(ctx.getText().substring(2).replaceAll("_", ""), 16, ctx); + } + + @Override + public Object visitOctInt(OctIntContext ctx) { + return toLong(ctx.getText().substring(2).replaceAll("_", ""), 8, ctx); + } + + @Override + public Object visitBinInt(BinIntContext ctx) { + return toLong(ctx.getText().substring(2).replaceAll("_", ""), 2, ctx); + } + + private Long toLong(String s, int radix, ParserRuleContext ctx) { + try { + return Long.valueOf(s, radix); + } catch (NumberFormatException e) { + throw new TomlParseError("Integer is too large", new TomlPosition(ctx)); + } + } + + @Override + public Object visitRegularFloat(RegularFloatContext ctx) { + return toDouble(ctx.getText().replaceAll("_", ""), ctx); + } + + private Double toDouble(String s, ParserRuleContext ctx) { + try { + Double value = Double.valueOf(s); + if (value == Double.POSITIVE_INFINITY || value == Double.NEGATIVE_INFINITY) { + throw new TomlParseError("Float is too large", new TomlPosition(ctx)); + } + if (value == 0d && !zeroFloat.matcher(s).matches()) { + throw new TomlParseError("Float is too small", new TomlPosition(ctx)); + } + return value; + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid floating point number: " + e.getMessage(), new TomlPosition(ctx)); + } + } + + @Override + public Object visitTrueBool(TrueBoolContext ctx) { + return Boolean.TRUE; + } + + @Override + public Object visitFalseBool(FalseBoolContext ctx) { + return Boolean.FALSE; + } + + @Override + public Object visitOffsetDateTime(OffsetDateTimeContext ctx) { + LocalDate date = ctx.date().accept(new LocalDateVisitor()); + LocalTime time = ctx.time().accept(new LocalTimeVisitor()); + ZoneOffset offset = ctx.timeOffset().accept(new ZoneOffsetVisitor()); + return OffsetDateTime.of(date, time, offset); + } + + @Override + public Object visitLocalDateTime(LocalDateTimeContext ctx) { + LocalDate date = ctx.date().accept(new LocalDateVisitor()); + LocalTime time = ctx.time().accept(new LocalTimeVisitor()); + return LocalDateTime.of(date, time); + } + + @Override + public Object visitLocalDate(LocalDateContext ctx) { + return ctx.date().accept(new LocalDateVisitor()); + } + + @Override + public Object visitLocalTime(LocalTimeContext ctx) { + return ctx.time().accept(new LocalTimeVisitor()); + } + + @Override + public Object visitArray(ArrayContext ctx) { + ArrayValuesContext valuesContext = ctx.arrayValues(); + if (valuesContext == null) { + return MutableTomlArray.EMPTY; + } + return valuesContext.accept(new ArrayVisitor()); + } + + @Override + public Object visitInlineTable(InlineTableContext ctx) { + InlineTableValuesContext valuesContext = ctx.inlineTableValues(); + if (valuesContext == null) { + return MutableTomlTable.EMPTY; + } + return valuesContext.accept(new InlineTableVisitor()); + } + + @Override + protected Object aggregateResult(Object aggregate, Object nextResult) { + return nextResult; + } + + @Override + protected Object defaultResult() { + return null; + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/ZoneOffsetVisitor.java b/toml/src/main/java/net/consensys/cava/toml/ZoneOffsetVisitor.java new file mode 100644 index 00000000..947ba99b --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/ZoneOffsetVisitor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import net.consensys.cava.toml.internal.TomlParser.HourOffsetContext; +import net.consensys.cava.toml.internal.TomlParser.MinuteOffsetContext; +import net.consensys.cava.toml.internal.TomlParserBaseVisitor; + +import java.time.DateTimeException; +import java.time.ZoneOffset; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ErrorNode; + +final class ZoneOffsetVisitor extends TomlParserBaseVisitor { + + private int hours = 0; + private int minutes = 0; + + @Override + public ZoneOffset visitHourOffset(HourOffsetContext ctx) { + int hours; + try { + hours = Integer.parseInt(ctx.getText()); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid zone offset", new TomlPosition(ctx), e); + } + if (hours < -18 || hours > 18) { + throw new TomlParseError("Invalid zone offset hours (valid range -18..+18)", new TomlPosition(ctx)); + } + ZoneOffset offset = toZoneOffset(hours, minutes, ctx, 0); + this.hours = hours; + return offset; + } + + @Override + public ZoneOffset visitMinuteOffset(MinuteOffsetContext ctx) { + int minutes; + try { + minutes = Integer.parseInt(ctx.getText()); + } catch (NumberFormatException e) { + throw new TomlParseError("Invalid zone offset", new TomlPosition(ctx), e); + } + if (minutes < 0 || minutes > 59) { + throw new TomlParseError("Invalid zone offset minutes (valid range 0..59)", new TomlPosition(ctx)); + } + ZoneOffset offset = toZoneOffset(hours, minutes, ctx, -4); + this.minutes = minutes; + return offset; + } + + private static ZoneOffset toZoneOffset(int hours, int minutes, ParserRuleContext ctx, int offset) { + try { + return ZoneOffset.ofHoursMinutes(hours, (hours < 0) ? -minutes : minutes); + } catch (DateTimeException e) { + throw new TomlParseError("Invalid zone offset (valid range -18:00..+18:00)", new TomlPosition(ctx, offset), e); + } + } + + @Override + public ZoneOffset visitErrorNode(ErrorNode node) { + return null; + } + + @Override + protected ZoneOffset aggregateResult(ZoneOffset aggregate, ZoneOffset nextResult) { + return aggregate == null ? null : nextResult; + } + + @Override + protected ZoneOffset defaultResult() { + return ZoneOffset.ofHoursMinutes(this.hours, (this.hours < 0) ? -this.minutes : this.minutes); + } +} diff --git a/toml/src/main/java/net/consensys/cava/toml/package-info.java b/toml/src/main/java/net/consensys/cava/toml/package-info.java new file mode 100644 index 00000000..c4478124 --- /dev/null +++ b/toml/src/main/java/net/consensys/cava/toml/package-info.java @@ -0,0 +1,13 @@ +/** + * 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 standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-toml' (cava-toml.jar). + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.toml; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/toml/src/test/java/net/consensys/cava/toml/MutableTomlArrayTest.java b/toml/src/test/java/net/consensys/cava/toml/MutableTomlArrayTest.java new file mode 100644 index 00000000..af1f2f0f --- /dev/null +++ b/toml/src/test/java/net/consensys/cava/toml/MutableTomlArrayTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.TomlPosition.positionAt; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MutableTomlArrayTest { + + @Test + void emptyArrayContainsAllTypes() { + TomlArray array = new MutableTomlArray(); + assertTrue(array.isEmpty()); + assertEquals(0, array.size()); + assertTrue(array.containsStrings()); + assertTrue(array.containsLongs()); + assertTrue(array.containsDoubles()); + assertTrue(array.containsBooleans()); + assertTrue(array.containsOffsetDateTimes()); + assertTrue(array.containsLocalDateTimes()); + assertTrue(array.containsLocalDates()); + assertTrue(array.containsLocalTimes()); + assertTrue(array.containsArrays()); + assertTrue(array.containsTables()); + } + + @Test + void arrayContainsTypeAfterAddingItem() { + MutableTomlArray array = new MutableTomlArray().append("foo", positionAt(2, 3)); + assertFalse(array.isEmpty()); + assertEquals(1, array.size()); + assertTrue(array.containsStrings()); + assertFalse(array.containsLongs()); + assertFalse(array.containsDoubles()); + assertFalse(array.containsBooleans()); + assertFalse(array.containsOffsetDateTimes()); + assertFalse(array.containsLocalDateTimes()); + assertFalse(array.containsLocalDates()); + assertFalse(array.containsLocalTimes()); + assertFalse(array.containsArrays()); + assertFalse(array.containsTables()); + } + + @Test + void cannotAppendUnsupportedType() { + MutableTomlArray array = new MutableTomlArray(); + assertThrows(IllegalArgumentException.class, () -> array.append(this, positionAt(1, 1))); + assertThrows(NullPointerException.class, () -> array.append(null, positionAt(1, 1))); + } + + @Test + void cannotAppendDifferentTypes() { + MutableTomlArray array = new MutableTomlArray(); + array.append("Foo", positionAt(1, 1)); + assertThrows(TomlInvalidTypeException.class, () -> array.append(1L, positionAt(1, 1))); + array.append("Bar", positionAt(1, 1)); + assertEquals(2, array.size()); + } + + @Test + void shouldReturnNullForUnknownIndex() { + MutableTomlArray array = new MutableTomlArray(); + assertThrows(IndexOutOfBoundsException.class, () -> array.get(0)); + } + + @Test + void shouldReturnInputPosition() { + MutableTomlArray array = new MutableTomlArray(); + array.append("Foo", positionAt(4, 3)); + array.append("Bar", positionAt(9, 5)); + assertEquals(positionAt(4, 3), array.inputPositionOf(0)); + assertEquals(positionAt(9, 5), array.inputPositionOf(1)); + assertThrows(IndexOutOfBoundsException.class, () -> array.get(2)); + } +} diff --git a/toml/src/test/java/net/consensys/cava/toml/MutableTomlTableTest.java b/toml/src/test/java/net/consensys/cava/toml/MutableTomlTableTest.java new file mode 100644 index 00000000..0060f811 --- /dev/null +++ b/toml/src/test/java/net/consensys/cava/toml/MutableTomlTableTest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static net.consensys.cava.toml.TomlPosition.positionAt; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +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 MutableTomlTableTest { + + @Test + void emptyTableIsEmpty() { + TomlTable table = new MutableTomlTable(); + assertTrue(table.isEmpty()); + assertEquals(0, table.size()); + } + + @Test + void getMissingPropertyReturnsNull() { + MutableTomlTable table = new MutableTomlTable(); + table.set("bar", "one", positionAt(1, 1)); + table.set("foo.baz", "two", positionAt(1, 1)); + assertNull(table.get("baz")); + assertNull(table.get("foo.bar")); + assertNull(table.get("foo.bar.baz")); + } + + @Test + void getStringProperty() { + MutableTomlTable table = new MutableTomlTable(); + table.set("foo.bar", "one", positionAt(1, 1)); + assertTrue(table.isString("foo.bar")); + assertEquals("one", table.getString("foo.bar")); + } + + @Test + void shouldCreateParentTables() { + MutableTomlTable table = new MutableTomlTable(); + table.set("foo.bar", "one", positionAt(1, 1)); + assertTrue(table.isTable("foo")); + assertNotNull(table.getTable("foo")); + } + + @Test + void cannotReplaceProperty() { + MutableTomlTable table = new MutableTomlTable(); + table.set("foo.bar", "one", positionAt(1, 3)); + TomlParseError e = assertThrows(TomlParseError.class, () -> { + table.set("foo.bar", "two", positionAt(2, 5)); + }); + + assertEquals("foo.bar previously defined at line 1, column 3", e.getMessage()); + } + + @ParameterizedTest + @MethodSource("quotesComplexKeyInErrorSupplier") + void quotesComplexKeysInError(List path, String expected) { + MutableTomlTable table = new MutableTomlTable(); + table.set(path, "one", positionAt(1, 3)); + TomlParseError e = assertThrows(TomlParseError.class, () -> { + table.set(path, "two", positionAt(2, 5)); + }); + assertEquals(expected + " previously defined at line 1, column 3", e.getMessage()); + } + + private static Stream quotesComplexKeyInErrorSupplier() { + return Stream.of( + Arguments.of(Arrays.asList("", "bar"), "\"\".bar"), + Arguments.of(Arrays.asList("foo ", "bar"), "\"foo \".bar"), + Arguments.of(Arrays.asList("foo\n", "bar"), "\"foo\\n\".bar")); + } + + @Test + void cannotTreatNonTableAsTable() { + MutableTomlTable table = new MutableTomlTable(); + table.set("foo.bar", "one", positionAt(5, 3)); + TomlParseError e = assertThrows(TomlParseError.class, () -> { + table.set("foo.bar.baz", "two", positionAt(2, 5)); + }); + + assertEquals("foo.bar is not a table (previously defined at line 5, column 3)", e.getMessage()); + } + + @Test + void ignoresWhitespaceInUnquotedKeys() { + MutableTomlTable table = new MutableTomlTable(); + table.set("foo.bar", 4, positionAt(5, 3)); + assertEquals(Long.valueOf(4), table.getLong(" foo . bar")); + table.set(Arrays.asList(" Bar ", " B A Z "), 9, positionAt(5, 3)); + assertEquals(Long.valueOf(9), table.getLong("' Bar '. \" B A Z \"")); + } + + @Test + void throwsForInvalidKey() { + MutableTomlTable table = new MutableTomlTable(); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> { + table.get("foo.=bar"); + }); + assertEquals("Invalid key: Unexpected '=', expected a-z, A-Z, 0-9, ', or \"", e.getMessage()); + } + + @Test + void shouldReturnInputPosition() { + MutableTomlTable table = new MutableTomlTable(); + table.set("bar", "one", positionAt(4, 3)); + table.set("foo.baz", "two", positionAt(15, 2)); + assertEquals(positionAt(4, 3), table.inputPositionOf("bar")); + assertEquals(positionAt(15, 2), table.inputPositionOf("foo.baz")); + assertNull(table.inputPositionOf("baz")); + assertNull(table.inputPositionOf("foo.bar")); + assertNull(table.inputPositionOf("foo.bar.baz")); + } + + @Test + void shouldReturnKeySet() { + MutableTomlTable table = new MutableTomlTable(); + table.set("bar", "one", positionAt(4, 3)); + table.set("foo.baz", "two", positionAt(15, 2)); + assertEquals(new HashSet<>(Arrays.asList("bar", "foo")), table.keySet()); + } + + @Test + void shouldReturnDottedKeySet() { + MutableTomlTable table = new MutableTomlTable(); + table.set("bar", "one", positionAt(4, 3)); + table.set("foo.baz", "two", positionAt(15, 2)); + table.set("foo.buz.bar", "three", positionAt(15, 2)); + assertEquals( + new HashSet<>(Arrays.asList("bar", "foo", "foo.baz", "foo.buz", "foo.buz.bar")), + table.dottedKeySet(true)); + assertEquals(new HashSet<>(Arrays.asList("bar", "foo.baz", "foo.buz.bar")), table.dottedKeySet()); + } + + @Test + void shouldSerializeToJSON() { + MutableTomlTable table = new MutableTomlTable(); + table.set("bar", "one", positionAt(2, 1)); + table.set("foo.baz", "two", positionAt(3, 2)); + table.set("foo.buz", MutableTomlArray.EMPTY, positionAt(3, 2)); + table.set("foo.foo", MutableTomlTable.EMPTY, positionAt(3, 2)); + MutableTomlArray array = new MutableTomlArray(); + array.append("hello\nthere", positionAt(5, 2)); + array.append("goodbye", positionAt(5, 2)); + table.set("foo.blah", array, positionAt(5, 2)); + table.set("buz", OffsetDateTime.parse("1937-07-18T03:25:43-04:00"), positionAt(5, 2)); + table.set("glad", LocalDateTime.parse("1937-07-18T03:25:43"), positionAt(5, 2)); + table.set("zoo", LocalDate.parse("1937-07-18"), positionAt(5, 2)); + table.set("alpha", LocalTime.parse("03:25:43"), positionAt(5, 2)); + String expected = "{\n" + + " \"alpha\" : \"03:25:43\",\n" + + " \"bar\" : \"one\",\n" + + " \"buz\" : \"1937-07-18T03:25:43-04:00\",\n" + + " \"foo\" : {\n" + + " \"baz\" : \"two\",\n" + + " \"blah\" : [\n" + + " \"hello\\nthere\",\n" + + " \"goodbye\"\n" + + " ],\n" + + " \"buz\" : [],\n" + + " \"foo\" : {}\n" + + " },\n" + + " \"glad\" : \"1937-07-18T03:25:43\",\n" + + " \"zoo\" : \"1937-07-18\"\n" + + "}\n"; + assertEquals(expected.replace("\n", System.lineSeparator()), table.toJson()); + } +} diff --git a/toml/src/test/java/net/consensys/cava/toml/TokenNameTest.java b/toml/src/test/java/net/consensys/cava/toml/TokenNameTest.java new file mode 100644 index 00000000..5a27757c --- /dev/null +++ b/toml/src/test/java/net/consensys/cava/toml/TokenNameTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.consensys.cava.toml.internal.TomlLexer; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class TokenNameTest { + + @Test + void shouldHaveTokenNameForAllTokens() { + List missing = new ArrayList<>(); + for (int i = 0; i < TomlLexer.VOCABULARY.getMaxTokenType(); ++i) { + if (!TokenName.namesForToken(i).findFirst().isPresent()) { + missing.add(TomlLexer.VOCABULARY.getSymbolicName(i)); + } + } + assertTrue(missing.isEmpty(), () -> "No TokenName's for " + String.join(", ", missing)); + } +} diff --git a/toml/src/test/java/net/consensys/cava/toml/TomlTest.java b/toml/src/test/java/net/consensys/cava/toml/TomlTest.java new file mode 100644 index 00000000..bcd5693d --- /dev/null +++ b/toml/src/test/java/net/consensys/cava/toml/TomlTest.java @@ -0,0 +1,526 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.toml; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.InputStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +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 TomlTest { + + @Test + void shouldParseEmptyDocument() { + TomlParseResult result = Toml.parse("\n"); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + } + + @Test + void shouldParseSimpleKey() { + TomlParseResult result = Toml.parse("foo = 'bar'"); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals("bar", result.getString("foo")); + } + + @Test + void shouldParseQuotedKey() { + TomlParseResult result = Toml.parse("\"foo\\nba\\\"r\" = 0b11111111"); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(Long.valueOf(255), result.getLong(Collections.singletonList("foo\nba\"r"))); + } + + @Test + void shouldParseDottedKey() { + TomlParseResult result = Toml.parse(" foo . \" bar\\t\" . -baz = 0x000a", TomlVersion.HEAD); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(Long.valueOf(10), result.getLong(Arrays.asList("foo", " bar\t", "-baz"))); + } + + @Test + void shouldNotParseDottedKeysAtV0_4_0OrEarlier() { + TomlParseResult result = Toml.parse("[foo]\n bar.baz = 1"); + assertTrue(result.hasErrors()); + TomlParseError error = result.errors().get(0); + assertEquals("Dotted keys are not supported", error.getMessage()); + assertEquals(2, error.position().line()); + assertEquals(2, error.position().column()); + } + + @ParameterizedTest + @MethodSource("stringSupplier") + void shouldParseString(String input, String expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected.replaceAll("\n", System.lineSeparator()), result.getString("foo")); + } + + private static Stream stringSupplier() { + return Stream.of( + Arguments.of("foo = \"\"", ""), + Arguments.of("foo = \"\\\"\"", "\""), + Arguments.of("foo = \"bar \\b \\f \\n \\\\ \\u0053 \\U0010FfFf baz\"", "bar \b \f \n \\ S \uDBFF\uDFFF baz"), + Arguments.of("foo = \"\"\"\"\"\"", ""), + Arguments.of("foo = \"\"\" foo\nbar\"\"\"", " foo\nbar"), + Arguments.of("foo = \"\"\"\n foobar\"\"\"", " foobar"), + Arguments.of("foo = \"\"\"\n foo\nbar\"\"\"", " foo\nbar"), + Arguments.of("foo = \"\"\"\\n foo\nbar\"\"\"", "\n foo\nbar"), + Arguments.of("foo = \"\"\"\n\n foo\nbar\"\"\"", "\n foo\nbar"), + Arguments.of("foo = \"\"\" foo \\ \nbar\"\"\"", " foo \nbar"), + Arguments.of("foo = \"\"\" foo \\\nbar\"\"\"", " foo \nbar"), + Arguments.of("foo = \"\"\" foo \\ \nbar\"\"\"", " foo \nbar"), + Arguments.of("foo = \"foobar#\" # comment", "foobar#"), + Arguments.of("foo = \"foobar#\"", "foobar#"), + Arguments.of("foo = \"foo \\\" bar #\" # \"baz\"", "foo \" bar #"), + Arguments.of("foo = ''", ""), + Arguments.of("foo = '\"'", "\""), + Arguments.of("foo = 'foobar \\'", "foobar \\"), + Arguments.of("foo = '''foobar \n'''", "foobar \n"), + Arguments.of("foo = '''\nfoobar \n'''", "foobar \n"), + Arguments.of("foo = '''\nfoobar \\ \n'''", "foobar \\ \n"), + Arguments.of("# I am a comment. Hear me roar. Roar.\nfoo = \"value\" # Yeah, you can do this.", "value"), + Arguments.of( + "foo = \"I'm a string. \\\"You can quote me\\\". Name\\tJos\\u00E9\\nLocation\\tSF.\"", + "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."), + Arguments.of("foo=\"\"\"\nRoses are red\nViolets are blue\"\"\"", "Roses are red\nViolets are blue")); + } + + @Test + void shouldFailForInvalidUnicodeEscape() { + TomlParseResult result = Toml.parse("foo = \"\\UFFFF00FF\""); + assertTrue(result.hasErrors()); + TomlParseError error = result.errors().get(0); + assertEquals("Invalid unicode escape sequence", error.getMessage()); + assertEquals(1, error.position().line()); + assertEquals(8, error.position().column()); + } + + @ParameterizedTest + @MethodSource("integerSupplier") + void shouldParseInteger(String input, Long expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getLong("foo")); + } + + private static Stream integerSupplier() { + return Stream.of( + Arguments.of("foo = 1", 1L), + Arguments.of("foo = 0", 0L), + Arguments.of("foo = 100", 100L), + Arguments.of("foo = -9876", -9876L), + Arguments.of("foo = +5_433", 5433L), + Arguments.of("foo = 0xff", 255L), + Arguments.of("foo = 0xffbccd34", 4290563380L), + Arguments.of("foo = 0o7656", 4014L), + Arguments.of("foo = 0o0007_6543_21", 2054353L), + Arguments.of("foo = 0b11111100010101_0100000000111111111", 8466858495L), + Arguments.of("foo = 0b0000000_00000000000000000000000000", 0L), + Arguments.of("foo = 0b111111111111111111111111111111111", 8589934591L)); + } + + @ParameterizedTest + @MethodSource("floatSupplier") + void shouldParseFloat(String input, Double expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getDouble("foo")); + } + + private static Stream floatSupplier() { + // @formatter:off + return Stream.of( + Arguments.of("foo = 0.0", 0D), + Arguments.of("foo = 0E100", 0D), + Arguments.of("foo = 0.00e+100", 0D), + Arguments.of("foo = 0.00e-100", 0D), + Arguments.of("foo = +0.0", 0D), + Arguments.of("foo = -0.0", -0D), + Arguments.of("foo = 0.000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000", 0D), + Arguments.of("foo = -0.0E999999999999999999999999999999999999999", -0D), + Arguments.of("foo = 1.0", 1D), + Arguments.of("foo = 43.55E34", 43.55E34D), + Arguments.of("foo = 43.557_654E-34", 43.557654E-34D) + ); + // @formatter:on + } + + @Test + void shouldParseBoolean() { + TomlParseResult result = Toml.parse("foo = true"); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(Boolean.TRUE, result.getBoolean("foo")); + TomlParseResult result2 = Toml.parse("\nfoo=false"); + assertFalse(result2.hasErrors(), () -> joinErrors(result2)); + assertEquals(Boolean.FALSE, result2.getBoolean("foo")); + } + + @ParameterizedTest + @MethodSource("offsetDateSupplier") + void shouldParseOffsetDateTime(String input, OffsetDateTime expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getOffsetDateTime("foo")); + } + + private static Stream offsetDateSupplier() { + // @formatter:off + return Stream.of( + Arguments.of("foo = 1937-07-18T03:25:43-04:00", OffsetDateTime.parse("1937-07-18T03:25:43-04:00")), + Arguments.of("foo = 1937-07-18 11:44:02+18:00", OffsetDateTime.parse("1937-07-18T11:44:02+18:00")), + Arguments.of("foo = 0000-07-18 11:44:02.00+18:00", OffsetDateTime.parse("0000-07-18T11:44:02+18:00")), + Arguments.of("foo = 1979-05-27T07:32:00Z\nbar = 1979-05-27T00:32:00-07:00\n", + OffsetDateTime.parse("1979-05-27T07:32:00Z")), + Arguments.of("bar = 1979-05-27T07:32:00Z\nfoo = 1979-05-27T00:32:00-07:00\n", + OffsetDateTime.parse("1979-05-27T00:32:00-07:00")), + Arguments.of("foo = 1937-07-18 11:44:02.334543+18:00", + OffsetDateTime.parse("1937-07-18T11:44:02.334543+18:00")), + Arguments.of("foo = 1937-07-18 11:44:02Z", OffsetDateTime.parse("1937-07-18T11:44:02+00:00")) + ); + // @formatter:on + } + + @ParameterizedTest + @MethodSource("localDateTimeSupplier") + void shouldParseLocalDateTime(String input, LocalDateTime expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getLocalDateTime("foo")); + } + + private static Stream localDateTimeSupplier() { + return Stream.of( + Arguments.of("foo = 1937-07-18T03:25:43", LocalDateTime.parse("1937-07-18T03:25:43")), + Arguments.of("foo = 1937-07-18 11:44:02", LocalDateTime.parse("1937-07-18T11:44:02")), + Arguments.of("foo = 0000-07-18 11:44:02.00", LocalDateTime.parse("0000-07-18T11:44:02")), + Arguments.of("foo = 1937-07-18 11:44:02.334543", LocalDateTime.parse("1937-07-18T11:44:02.334543")), + Arguments.of("foo = 1937-07-18 11:44:02", LocalDateTime.parse("1937-07-18T11:44:02"))); + } + + @ParameterizedTest + @MethodSource("localDateSupplier") + void shouldParseLocalDate(String input, LocalDate expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getLocalDate("foo")); + } + + private static Stream localDateSupplier() { + return Stream.of( + Arguments.of("foo = 1937-07-18", LocalDate.parse("1937-07-18")), + Arguments.of("foo = 1937-07-18", LocalDate.parse("1937-07-18")), + Arguments.of("foo = 0000-07-18", LocalDate.parse("0000-07-18")), + Arguments.of("foo = 1937-07-18", LocalDate.parse("1937-07-18")), + Arguments.of("foo = 1937-07-18", LocalDate.parse("1937-07-18"))); + } + + @ParameterizedTest + @MethodSource("localTimeSupplier") + void shouldParseLocalTime(String input, LocalTime expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.getLocalTime("foo")); + } + + private static Stream localTimeSupplier() { + return Stream.of( + Arguments.of("foo = 03:25:43", LocalTime.parse("03:25:43")), + Arguments.of("foo = 11:44:02", LocalTime.parse("11:44:02")), + Arguments.of("foo = 11:44:02.00", LocalTime.parse("11:44:02")), + Arguments.of("foo = 11:44:02.334543", LocalTime.parse("11:44:02.334543")), + Arguments.of("foo = 11:44:02", LocalTime.parse("11:44:02"))); + } + + @ParameterizedTest + @MethodSource("arraySupplier") + void shouldParseArray(String input, Object[] expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + TomlArray array = result.getArray("foo"); + assertNotNull(array); + assertEquals(expected.length, array.size()); + assertTomlArrayEquals(expected, array); + } + + private static Stream arraySupplier() { + return Stream.of( + Arguments.of("foo = []", new Object[0]), + Arguments.of("foo = [\n]", new Object[0]), + Arguments.of("foo = [1]", new Object[] {1L}), + Arguments.of("foo = [ \"bar\"\n]", new Object[] {"bar"}), + Arguments.of("foo = [11:44:02,]", new Object[] {LocalTime.parse("11:44:02")}), + Arguments.of("foo = [\n'bar', #baz\n]", new Object[] {"bar"}), + Arguments.of("foo = ['bar', 'baz']", new Object[] {"bar", "baz"}), + Arguments.of("foo = [\n'''bar\nbaz''',\n'baz'\n]", new Object[] {"bar\nbaz", "baz"}), + Arguments.of("foo = [['bar']]", new Object[] {new Object[] {"bar"}})); + } + + @ParameterizedTest + @MethodSource("tableSupplier") + void shouldParseTable(String input, String key, Object expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.get(key)); + } + + private static Stream tableSupplier() { + return Stream.of( + Arguments.of("[foo]\nbar = 'baz'", "foo.bar", "baz"), + Arguments.of("[foo] #foo.bar\nbar = 'baz'", "foo.bar", "baz"), + Arguments.of("[foo]\n[foo.bar]\nbaz = 'buz'", "foo.bar.baz", "buz"), + Arguments.of("[foo.bar]\nbaz=1\n[foo]\nbaz=2", "foo.baz", 2L)); + } + + @ParameterizedTest + @MethodSource("inlineTableSupplier") + void shouldParseInlineTable(String input, String key, Object expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + assertEquals(expected, result.get(key)); + } + + private static Stream inlineTableSupplier() { + return Stream.of( + Arguments.of("foo = {}", "foo.bar", null), + Arguments.of("foo = { bar = 'baz' }", "foo.bar", "baz"), + Arguments.of("foo = { bar = 'baz', baz.buz = 2 }", "foo.baz.buz", 2L), + Arguments.of("foo = { bar = ['baz', 'buz'] , baz . buz = 2 }", "foo.baz.buz", 2L), + Arguments.of("foo = { bar = ['baz',\n'buz'\n], baz.buz = 2 }", "foo.baz.buz", 2L), + Arguments.of("bar = { bar = ['baz',\n'buz'\n], baz.buz = 2 }\nfoo=2\n", "foo", 2L)); + } + + @ParameterizedTest + @MethodSource("arrayTableSupplier") + void shouldParseArrayTable(String input, Object[] path, Object expected) { + TomlParseResult result = Toml.parse(input); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + + Object element = result; + for (Object step : path) { + if (step instanceof String) { + assertTrue(element instanceof TomlTable); + element = ((TomlTable) element).get((String) step); + } else if (step instanceof Integer) { + assertTrue(element instanceof TomlArray); + element = ((TomlArray) element).get((Integer) step); + } else { + fail("path not found"); + } + } + assertEquals(expected, element); + } + + private static Stream arrayTableSupplier() { + return Stream.of( + Arguments.of("[[foo]]\nbar = 'baz'", new Object[] {"foo", 0, "bar"}, "baz"), + Arguments.of("[[foo]] #foo.bar\nbar = 'baz'", new Object[] {"foo", 0, "bar"}, "baz"), + Arguments.of("[[foo]] \n bar = 'buz'\nbuz=1\n", new Object[] {"foo", 0, "buz"}, 1L), + Arguments.of("[[foo]] \n bar = 'buz'\n[[foo]]\nbar=1\n", new Object[] {"foo", 0, "bar"}, "buz"), + Arguments.of("[[foo]] \n bar = 'buz'\n[[foo]]\nbar=1\n", new Object[] {"foo", 1, "bar"}, 1L), + Arguments.of("[[foo]]\nbar=1\n[[foo]]\nbar=2\n", new Object[] {"foo", 0, "bar"}, 1L), + Arguments.of("[[foo]]\nbar=1\n[[foo]]\nbar=2\n", new Object[] {"foo", 1, "bar"}, 2L), + Arguments.of("[[foo]]\n\n[foo.bar]\n\nbaz=2\n\n", new Object[] {"foo", 0, "bar", "baz"}, 2L), + Arguments.of( + "[[foo]]\n[[foo.bar]]\n[[foo.baz]]\n[foo.bar.baz]\nbuz=2\n[foo.baz.buz]\nbiz=3\n", + new Object[] {"foo", 0, "bar", 0, "baz", "buz"}, + 2L), + Arguments.of( + "[[foo]]\n[[foo.bar]]\n[[foo.baz]]\n[foo.bar.baz]\nbuz=2\n[foo.baz.buz]\nbiz=3\n", + new Object[] {"foo", 0, "baz", 0, "buz", "biz"}, + 3L)); + } + + @ParameterizedTest + @MethodSource("errorCaseSupplier") + void shouldHandleParseErrors(String input, int line, int column, String expected) { + TomlParseResult result = Toml.parse(input); + List errors = result.errors(); + assertFalse(errors.isEmpty()); + assertEquals(expected, errors.get(0).getMessage(), () -> joinErrors(result)); + assertEquals(line, errors.get(0).position().line()); + assertEquals(column, errors.get(0).position().column()); + } + + private static Stream errorCaseSupplier() { + // @formatter:off + return Stream.of( + Arguments.of("\"foo\"", 1, 6, "Unexpected end of input, expected . or ="), + Arguments.of("foo", 1, 4, "Unexpected end of input, expected . or ="), + Arguments.of("foo \n", 1, 6, "Unexpected end of line, expected . or ="), + Arguments.of("foo =", 1, 6, "Unexpected end of input, expected ', \", ''', \"\"\", a number, a boolean, a date/time, an array, or a table"), + Arguments.of("foo = 0b", 1, 8, "Unexpected 'b', expected a newline or end-of-input"), + Arguments.of("foo = +", 1, 7, "Unexpected '+', expected ', \", ''', \"\"\", a number, a boolean, a date/time, an array, or a table"), + Arguments.of("=", 1, 1, "Unexpected '=', expected a-z, A-Z, 0-9, ', \", a table key, a newline, or end-of-input"), + Arguments.of("\"foo\tbar\" = 1", 1, 5, "Unexpected '\\t', expected \" or a character"), + Arguments.of("\"foo \nbar\" = 1", 1, 6, "Unexpected end of line, expected \" or a character"), + Arguments.of("foo = \"bar \\y baz\"", 1, 12, "Invalid escape sequence '\\y'"), + Arguments.of("\u0011abc = 'foo'", 1, 1, "Unexpected '\\u0011', expected a-z, A-Z, 0-9, ', \", a table key, a newline, or end-of-input"), + Arguments.of(" \uDBFF\uDFFFAAabc='foo'", 1, 2, "Unexpected '\\U0010ffff', expected a-z, A-Z, 0-9, ', \", a table key, a newline, or end-of-input"), + + Arguments.of("foo = 1234567891234567891233456789", 1, 7, "Integer is too large"), + + Arguments.of("\n\nfoo = \t +1E1000", 3, 18, "Float is too large"), + Arguments.of("foo = +1E-1000", 1, 7, "Float is too small"), + Arguments.of("foo = 0.000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000001", 1, 7, "Float is too small"), + + Arguments.of("\nfoo = 1937-47-18-00:00:00-04:00", 2, 17, "Unexpected '-', expected a newline or end-of-input"), + Arguments.of("\nfoo = 1937-47-18 00:00:00-04:00", 2, 19, "Unexpected '00', expected a newline or end-of-input"), + Arguments.of("\nfoo = 2334567891233457889-07-18T00:00:00-04:00", 2, 7, "Invalid year (valid range 0000..9999)"), + Arguments.of("\nfoo = 2-07-18T00:00:00-04:00", 2, 7, "Invalid year (valid range 0000..9999)"), + Arguments.of("\nfoo = -07-18T00:00:00-04:00", 2, 9, "Unexpected '7-18T00', expected a newline or end-of-input"), + Arguments.of("\nfoo = 1937-47-18T00:00:00-04:00", 2, 12, "Invalid month (valid range 01..12)"), + Arguments.of("\nfoo = 1937-7-18T00:00:00-04:00", 2, 12, "Invalid month (valid range 01..12)"), + Arguments.of("\nfoo = 1937-00-18T00:00:00-04:00", 2, 12, "Invalid month (valid range 01..12)"), + Arguments.of("\nfoo = 1937--18T00:00:00-04:00", 2, 12, "Unexpected '-', expected a date/time"), + Arguments.of("\nfoo = 1937-07-48T00:00:00-04:00", 2, 15, "Invalid day (valid range 01..28/31)"), + Arguments.of("\nfoo = 1937-07-8T00:00:00-04:00", 2, 15, "Invalid day (valid range 01..28/31)"), + Arguments.of("\nfoo = 1937-07-00T00:00:00-04:00", 2, 15, "Invalid day (valid range 01..28/31)"), + Arguments.of("\nfoo = 1937-02-30T00:00:00-04:00", 2, 15, "Invalid date 'FEBRUARY 30'"), + Arguments.of("\nfoo = 1937-07-18T30:00:00-04:00", 2, 18, "Invalid hour (valid range 00..23)"), + Arguments.of("\nfoo = 1937-07-18T3:00:00-04:00", 2, 18, "Invalid hour (valid range 00..23)"), + Arguments.of("\nfoo = 1937-07-18T13:70:00-04:00", 2, 21, "Invalid minutes (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:7:00-04:00", 2, 21, "Invalid minutes (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:92-04:00", 2, 24, "Invalid seconds (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:2-04:00", 2, 24, "Invalid seconds (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:02.0000000009-04:00", 2, 27, "Invalid nanoseconds (valid range 0..999999999)"), + Arguments.of("\nfoo = 1937-07-18T13:55:02.-04:00", 2, 27, "Unexpected '-', expected a date/time"), + Arguments.of("\nfoo = 1937-07-18T13:55:26-25:00", 2, 26, "Invalid zone offset hours (valid range -18..+18)"), + Arguments.of("\nfoo = 1937-07-18T13:55:26-:00", 2, 27, "Unexpected ':', expected a date/time"), + Arguments.of("\nfoo = 1937-07-18T13:55:26-04:60", 2, 30, "Invalid zone offset minutes (valid range 0..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:26-18:30", 2, 26, "Invalid zone offset (valid range -18:00..+18:00)"), + Arguments.of("\nfoo = 1937-07-18T13:55:26-18:", 2, 30, "Unexpected end of input, expected a date/time"), + + Arguments.of("\nfoo = 2334567891233457889-07-18T00:00:00", 2, 7, "Invalid year (valid range 0000..9999)"), + Arguments.of("\nfoo = 1937-47-18T00:00:00", 2, 12, "Invalid month (valid range 01..12)"), + Arguments.of("\nfoo = 1937-07-48T00:00:00", 2, 15, "Invalid day (valid range 01..28/31)"), + Arguments.of("\nfoo = 1937-07-18T30:00:00", 2, 18, "Invalid hour (valid range 00..23)"), + Arguments.of("\nfoo = 1937-07-18T13:70:00", 2, 21, "Invalid minutes (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:92", 2, 24, "Invalid seconds (valid range 00..59)"), + Arguments.of("\nfoo = 1937-07-18T13:55:02.0000000009", 2, 27, "Invalid nanoseconds (valid range 0..999999999)"), + + Arguments.of("\nfoo = 2334567891233457889-07-18", 2, 7, "Invalid year (valid range 0000..9999)"), + Arguments.of("\nfoo = 1937-47-18", 2, 12, "Invalid month (valid range 01..12)"), + Arguments.of("\nfoo = 1937-07-48", 2, 15, "Invalid day (valid range 01..28/31)"), + + Arguments.of("\nfoo = 30:00:00", 2, 7, "Invalid hour (valid range 00..23)"), + Arguments.of("\nfoo = 13:70:00", 2, 10, "Invalid minutes (valid range 00..59)"), + Arguments.of("\nfoo = 13:55:92", 2, 13, "Invalid seconds (valid range 00..59)"), + Arguments.of("\nfoo = 13:55:02.0000000009", 2, 16, "Invalid nanoseconds (valid range 0..999999999)"), + + Arguments.of("foo = [", 1, 8, "Unexpected end of input, expected ], ', \", ''', \"\"\", a number, a boolean, a date/time, an array, a table, or a newline"), + Arguments.of("foo = [ 1\n", 2, 1, "Unexpected end of input, expected ] or a newline"), + Arguments.of("foo = [ 1, 'bar' ]", 1, 12, "Cannot add a string to an array containing integers"), + Arguments.of("foo = [ 1, 'bar ]\n", 1, 18, "Unexpected end of line, expected '"), + + Arguments.of("[]", 1, 1, "Empty table key"), + Arguments.of("[foo] bar='baz'", 1, 7, "Unexpected 'bar', expected a newline or end-of-input"), + Arguments.of("foo='bar'\n[foo]\nbar='baz'", 2, 1, "foo previously defined at line 1, column 1"), + Arguments.of("[foo]\nbar='baz'\n[foo]\nbaz=1", 3, 1, "foo previously defined at line 1, column 1"), + Arguments.of("[foo]\nbar='baz'\n[foo.bar]\nbaz=1", 3, 1, "foo.bar previously defined at line 2, column 1"), + + Arguments.of("foo = {", 1, 8, "Unexpected end of input, expected a-z, A-Z, 0-9, }, ', or \""), + Arguments.of("foo = { bar = 1,\nbaz = 2 }", 1, 17, "Unexpected end of line, expected a-z, A-Z, 0-9, ', or \""), + Arguments.of("foo = { bar = 1\nbaz = 2 }", 1, 16, "Unexpected end of line, expected }"), + Arguments.of("foo = { bar = 1 baz = 2 }", 1, 17, "Unexpected 'baz', expected } or a comma"), + + Arguments.of("[foo]\nbar=1\n[[foo]]\nbar=2\n", 3, 1, "foo is not an array (previously defined at line 1, column 1)"), + Arguments.of("foo = [1]\n[[foo]]\nbar=2\n", 2, 1, "foo previously defined as a literal array at line 1, column 1"), + Arguments.of("foo = []\n[[foo]]\nbar=2\n", 2, 1, "foo previously defined as a literal array at line 1, column 1"), + Arguments.of("[[foo.bar]]\n[foo]\nbaz=2\nbar=3\n", 4, 1, "bar previously defined at line 1, column 1"), + Arguments.of("[[foo]]\nbaz=1\n[[foo.bar]]\nbaz=2\n[foo.bar]\nbaz=3\n", 5, 1, "foo.bar previously defined at line 3, column 1") + ); + // @formatter:on + } + + @Test + void testTomlV0_4_0Example() throws Exception { + InputStream is = this.getClass().getResourceAsStream("/net/consensys/cava/toml/example-v0.4.0.toml"); + assertNotNull(is); + TomlParseResult result = Toml.parse(is, TomlVersion.V0_4_0); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + + assertEquals("value", result.getString("table.key")); + assertEquals("Preston-Werner", result.getString("table.inline.name.last")); + assertEquals("<\\i\\c*\\s*>", result.getString("string.literal.regex")); + assertEquals(2L, result.getArray("array.key5").getLong(1)); + assertEquals( + "granny smith", + result.getArray("fruit").getTable(0).getArray("variety").getTable(1).getString("name")); + } + + @Test + void testHardExample() throws Exception { + InputStream is = this.getClass().getResourceAsStream("/net/consensys/cava/toml/hard_example.toml"); + assertNotNull(is); + TomlParseResult result = Toml.parse(is, TomlVersion.V0_4_0); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + + assertEquals("You'll hate me after this - #", result.getString("the.test_string")); + assertEquals(" And when \"'s are in the string, along with # \"", result.getString("the.hard.harder_test_string")); + assertEquals("]", result.getArray("the.hard.'bit#'.multi_line_array").getString(0)); + } + + @Test + void testHardExampleUnicode() throws Exception { + InputStream is = this.getClass().getResourceAsStream("/net/consensys/cava/toml/hard_example_unicode.toml"); + assertNotNull(is); + TomlParseResult result = Toml.parse(is, TomlVersion.V0_4_0); + assertFalse(result.hasErrors(), () -> joinErrors(result)); + + assertEquals("Ýôú'ℓℓ λáƭè ₥è áƒƭèř ƭλïƨ - #", result.getString("the.test_string")); + assertEquals(" Âñδ ωλèñ \"'ƨ ářè ïñ ƭλè ƨƭřïñϱ, áℓôñϱ ωïƭλ # \"", result.getString("the.hard.harder_test_string")); + assertEquals("]", result.getArray("the.hard.'βïƭ#'.multi_line_array").getString(0)); + } + + private String joinErrors(TomlParseResult result) { + return result.errors().stream().map(TomlParseError::toString).collect(Collectors.joining("\n")); + } + + private static void assertTomlArrayEquals(Object[] expected, TomlArray array) { + for (int i = 0; i < expected.length; ++i) { + Object obj = array.get(i); + if (expected[i] instanceof Object[]) { + assertTrue(obj instanceof TomlArray); + assertTomlArrayEquals((Object[]) expected[i], (TomlArray) obj); + } else { + assertEquals(expected[i], obj); + } + } + } +} diff --git a/toml/src/test/resources/net/consensys/cava/toml/example-v0.4.0.toml b/toml/src/test/resources/net/consensys/cava/toml/example-v0.4.0.toml new file mode 100644 index 00000000..6c207b76 --- /dev/null +++ b/toml/src/test/resources/net/consensys/cava/toml/example-v0.4.0.toml @@ -0,0 +1,244 @@ +################################################################################ +## Comment + +# Speak your mind with the hash symbol. They go from the symbol to the end of +# the line. + + +################################################################################ +## Table + +# Tables (also known as hash tables or dictionaries) are collections of +# key/value pairs. They appear in square brackets on a line by themselves. + +[table] + +key = "value" # Yeah, you can do this. + +# Nested tables are denoted by table names with dots in them. Name your tables +# whatever crap you please, just don't use #, ., [ or ]. + +[table.subtable] + +key = "another value" + +# You don't need to specify all the super-tables if you don't want to. TOML +# knows how to do it for you. + +# [x] you +# [x.y] don't +# [x.y.z] need these +[x.y.z.w] # for this to work + + +################################################################################ +## Inline Table + +# Inline tables provide a more compact syntax for expressing tables. They are +# especially useful for grouped data that can otherwise quickly become verbose. +# Inline tables are enclosed in curly braces `{` and `}`. No newlines are +# allowed between the curly braces unless they are valid within a value. + +[table.inline] + +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } + + +################################################################################ +## String + +# There are four ways to express strings: basic, multi-line basic, literal, and +# multi-line literal. All strings must contain only valid UTF-8 characters. + +[string.basic] + +basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." + +[string.multiline] + +# The following strings are byte-for-byte equivalent: +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +[string.multiline.continued] + +# The following strings are byte-for-byte equivalent: +key1 = "The quick brown fox jumps over the lazy dog." + +key2 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +key3 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ + +[string.literal] + +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + + +[string.literal.multiline] + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + + +################################################################################ +## Integer + +# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. +# Negative numbers are prefixed with a minus sign. + +[integer] + +key1 = +99 +key2 = 42 +key3 = 0 +key4 = -17 + +[integer.underscores] + +# For large numbers, you may use underscores to enhance readability. Each +# underscore must be surrounded by at least one digit. +key1 = 1_000 +key2 = 5_349_221 +key3 = 1_2_3_4_5 # valid but inadvisable + + +################################################################################ +## Float + +# A float consists of an integer part (which may be prefixed with a plus or +# minus sign) followed by a fractional part and/or an exponent part. + +[float.fractional] + +key1 = +1.0 +key2 = 3.1415 +key3 = -0.01 + +[float.exponent] + +key1 = 5e+22 +key2 = 1e6 +key3 = -2E-2 + +[float.both] + +key = 6.626e-34 + +[float.underscores] + +key1 = 9_224_617.445_991_228_313 +key2 = 1e1_00 # modified from original example of 1e1_000, which overflows a Java Double + + +################################################################################ +## Boolean + +# Booleans are just the tokens you're used to. Always lowercase. + +[boolean] + +True = true +False = false + + +################################################################################ +## Datetime + +# Datetimes are RFC 3339 dates. + +[datetime] + +key1 = 1979-05-27T07:32:00Z +key2 = 1979-05-27T00:32:00-07:00 +key3 = 1979-05-27T00:32:00.999999-07:00 + + +################################################################################ +## Array + +# Arrays are square brackets with other primitives inside. Whitespace is +# ignored. Elements are separated by commas. Data types may not be mixed. + +[array] + +key1 = [ 1, 2, 3 ] +key2 = [ "red", "yellow", "green" ] +key3 = [ [ 1, 2 ], [3, 4, 5] ] +key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok + +# Arrays can also be multiline. So in addition to ignoring whitespace, arrays +# also ignore newlines between the brackets. Terminating commas are ok before +# the closing bracket. + +key5 = [ + 1, 2, 3 +] +key6 = [ + 1, + 2, # this is ok +] + + +################################################################################ +## Array of Tables + +# These can be expressed by using a table name in double brackets. Each table +# with the same double bracketed name will be an element in the array. The +# tables are inserted in the order encountered. + +[[products]] + +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] + +name = "Nail" +sku = 284758393 +color = "gray" + + +# You can create nested arrays of tables as well. + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff --git a/toml/src/test/resources/net/consensys/cava/toml/hard_example.toml b/toml/src/test/resources/net/consensys/cava/toml/hard_example.toml new file mode 100644 index 00000000..6abe76de --- /dev/null +++ b/toml/src/test/resources/net/consensys/cava/toml/hard_example.toml @@ -0,0 +1,33 @@ +# Test file for TOML +# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate +# This part you'll really hate + +[the] +test_string = "You'll hate me after this - #" # " Annoying, isn't it? + + [the.hard] + test_array = [ "] ", " # "] # ] There you go, parse this! + test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ] + # You didn't think it'd as easy as chucking out the last #, did you? + another_test_string = " Same thing, but with a string #" + harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too" + # Things will get harder + + [the.hard."bit#"] + "what?" = "You don't think some user won't do that?" + multi_line_array = [ + "]", + # ] Oh yes I did + ] + +# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test + +#[error] if you didn't catch this, your parser is broken +#string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment" like this +#array = [ +# "This might most likely happen in multiline arrays", +# Like here, +# "or here, +# and here" +# ] End of array comment, forgot the # +#number = 3.14 pi <--again forgot the # diff --git a/toml/src/test/resources/net/consensys/cava/toml/hard_example_unicode.toml b/toml/src/test/resources/net/consensys/cava/toml/hard_example_unicode.toml new file mode 100644 index 00000000..6173ce3a --- /dev/null +++ b/toml/src/test/resources/net/consensys/cava/toml/hard_example_unicode.toml @@ -0,0 +1,36 @@ +# Tèƨƭ ƒïℓè ƒôř TÓM£ + +# Óñℓ¥ ƭλïƨ ôñè ƭřïèƨ ƭô è₥úℓáƭè á TÓM£ ƒïℓè ωřïƭƭèñ β¥ á úƨèř ôƒ ƭλè ƙïñδ ôƒ ƥářƨèř ωřïƭèřƨ ƥřôβáβℓ¥ λáƭè +# Tλïƨ ƥářƭ ¥ôú'ℓℓ řèáℓℓ¥ λáƭè + +[the] +test_string = "Ýôú'ℓℓ λáƭè ₥è áƒƭèř ƭλïƨ - #" # " Âññô¥ïñϱ, ïƨñ'ƭ ïƭ? + + + [the.hard] + test_array = [ "] ", " # "] # ] Tλèřè ¥ôú ϱô, ƥářƨè ƭλïƨ! + test_array2 = [ "Tèƨƭ #11 ]ƥřôƲèδ ƭλáƭ", "Éжƥèřï₥èñƭ #9 ωáƨ á ƨúççèƨƨ" ] + # Ýôú δïδñ'ƭ ƭλïñƙ ïƭ'δ áƨ èáƨ¥ áƨ çλúçƙïñϱ ôúƭ ƭλè ℓáƨƭ #, δïδ ¥ôú? + another_test_string = "§á₥è ƭλïñϱ, βúƭ ωïƭλ á ƨƭřïñϱ #" + harder_test_string = " Âñδ ωλèñ \"'ƨ ářè ïñ ƭλè ƨƭřïñϱ, áℓôñϱ ωïƭλ # \"" # "áñδ çô₥₥èñƭƨ ářè ƭλèřè ƭôô" + # Tλïñϱƨ ωïℓℓ ϱèƭ λářδèř + + [the.hard."βïƭ#"] + "ωλáƭ?" = "Ýôú δôñ'ƭ ƭλïñƙ ƨô₥è úƨèř ωôñ'ƭ δô ƭλáƭ?" + multi_line_array = [ + "]", + # ] Óλ ¥èƨ Ì δïδ + ] + +# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test + +#[error] ïƒ ¥ôú δïδñ'ƭ çáƭçλ ƭλïƨ, ¥ôúř ƥářƨèř ïƨ βřôƙèñ +#string = "Âñ¥ƭλïñϱ ôƭλèř ƭλáñ ƭáβƨ, ƨƥáçèƨ áñδ ñèωℓïñè áƒƭèř á ƙè¥ϱřôúƥ ôř ƙè¥ Ʋáℓúè ƥáïř λáƨ èñδèδ ƨλôúℓδ ƥřôδúçè áñ èřřôř úñℓèƨƨ ïƭ ïƨ á çô₥₥èñƭ" ℓïƙè ƭλïƨ + +#array = [ +# "Tλïƨ ₥ïϱλƭ ₥ôƨƭ ℓïƙèℓ¥ λáƥƥèñ ïñ ₥úℓƭïℓïñè ářřá¥ƨ", +# £ïƙè λèřè, +# "ôř λèřè, +# áñδ λèřè" +# ] Éñδ ôƒ ářřᥠçô₥₥èñƭ, ƒôřϱôƭ ƭλè # +#number = 3.14 ƥï <--áϱáïñ ƒôřϱôƭ ƭλè # \ No newline at end of file diff --git a/units/build.gradle b/units/build.gradle new file mode 100644 index 00000000..baff2b89 --- /dev/null +++ b/units/build.gradle @@ -0,0 +1,11 @@ +description = 'Classes and utilities for working with 256 bit integers.' + +dependencies { + compile project(':bytes') + compile 'com.google.guava:guava' + + testCompile 'org.junit.jupiter:junit-jupiter-api' + testCompile 'org.junit.jupiter:junit-jupiter-params' + + testRuntime 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/BaseUInt256Value.java b/units/src/main/java/net/consensys/cava/units/bigints/BaseUInt256Value.java new file mode 100644 index 00000000..bd1a7a1e --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/BaseUInt256Value.java @@ -0,0 +1,338 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +import static java.util.Objects.requireNonNull; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; + +import java.math.BigInteger; +import java.util.function.Function; + +/** + * Base class for {@link UInt256Value}. + * + *

+ * This class is abstract as it is not meant to be used directly, but it has no abstract methods. As mentioned in + * {@link UInt256Value}, this is used to create strongly-typed type aliases of {@link UInt256}. In other words, this + * allow to "tag" numbers with the unit of what they represent for the type-system, which can help clarity, but also + * forbid mixing numbers that are mean to be of different units (the strongly-typed part). + * + *

+ * This class implements {@link UInt256Value}, but also adds a few operations that take a {@link UInt256} directly, for + * instance {@link #multiply(UInt256)}. The rational is that multiplying a given quantity of something by a "raw" number + * is always meaningful, and return a new quantity of the same thing. + * + * @param The concrete type of the value. + */ +public abstract class BaseUInt256Value> implements UInt256Value { + + private final UInt256 value; + private final Function ctor; + + /** + * @param value The value to instantiate this {@code UInt256Value} with. + * @param ctor A constructor for the concrete type. + */ + protected BaseUInt256Value(UInt256 value, Function ctor) { + requireNonNull(value); + requireNonNull(ctor); + this.value = value; + this.ctor = ctor; + } + + /** + * @param value An unsigned value to instantiate this {@code UInt256Value} with. + * @param ctor A constructor for the concrete type. + */ + protected BaseUInt256Value(long value, Function ctor) { + requireNonNull(ctor); + this.value = UInt256.valueOf(value); + this.ctor = ctor; + } + + /** + * @param value An unsigned value to instantiate this {@code UInt256Value} with. + * @param ctor A constructor for the concrete type. + */ + protected BaseUInt256Value(BigInteger value, Function ctor) { + requireNonNull(value); + requireNonNull(ctor); + this.value = UInt256.valueOf(value); + this.ctor = ctor; + } + + /** + * Return a copy of this value, or itself if immutable. + * + *

+ * The default implementation of this method returns a copy using the constructor for the concrete type and the bytes + * returned from {@link #toBytes()}. Most implementations will want to override this method to instead return + * {@code this}. + * + * @return A copy of this value, or itself if immutable. + */ + protected T copy() { + return ctor.apply(value); + } + + /** + * Return the zero value for this type. + * + *

+ * The default implementation of this method returns a value obtained from calling the concrete type constructor with + * an argument of {@link Bytes32#ZERO}. Most implementations will want to override this method to instead return a + * static constant. + * + * @return The zero value for this type. + */ + protected T zero() { + return ctor.apply(UInt256.ZERO); + } + + @Override + public T add(T value) { + return add(value.uint256Value()); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value The amount to be added to this value. + * @return {@code this + value} + */ + public T add(UInt256 value) { + if (value.isZero()) { + return copy(); + } + return ctor.apply(this.value.add(value)); + } + + @Override + public T add(long value) { + if (value == 0) { + return copy(); + } + return ctor.apply(this.value.add(value)); + } + + @Override + public T addMod(T value, UInt256 modulus) { + return addMod(value.uint256Value(), modulus); + } + + /** + * Returns a value equivalent to {@code ((this + value) mod modulus)}. + * + * @param value The amount to be added to this value. + * @param modulus The modulus. + * @return {@code (this + value) mod modulus} + * @throws ArithmeticException {@code modulus} == 0. + */ + public T addMod(UInt256 value, UInt256 modulus) { + return ctor.apply(this.value.addMod(value, modulus)); + } + + @Override + public T addMod(long value, UInt256 modulus) { + return ctor.apply(this.value.addMod(value, modulus)); + } + + @Override + public T addMod(long value, long modulus) { + return ctor.apply(this.value.addMod(value, modulus)); + } + + @Override + public T subtract(T value) { + return subtract(value.uint256Value()); + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value The amount to be subtracted from this value. + * @return {@code this - value} + */ + public T subtract(UInt256 value) { + if (value.isZero()) { + return copy(); + } + return ctor.apply(this.value.subtract(value)); + } + + @Override + public T subtract(long value) { + if (value == 0) { + return copy(); + } + return ctor.apply(this.value.subtract(value)); + } + + @Override + public T multiply(T value) { + return multiply(value.uint256Value()); + } + + /** + * Returns a value that is {@code (this * value)}. + * + * @param value The amount to multiply this value by. + * @return {@code this * value} + */ + public T multiply(UInt256 value) { + if (isZero() || value.isZero()) { + return zero(); + } + if (value.equals(UInt256.ONE)) { + return copy(); + } + return ctor.apply(this.value.multiply(value)); + } + + @Override + public T multiply(long value) { + if (value == 0 || isZero()) { + return zero(); + } + if (value == 1) { + return copy(); + } + return ctor.apply(this.value.multiply(value)); + } + + @Override + public T multiplyMod(T value, UInt256 modulus) { + return multiplyMod(value.uint256Value(), modulus); + } + + /** + * Returns a value that is {@code ((this * value) mod modulus)}. + * + * @param value The amount to multiply this value by. + * @param modulus The modulus. + * @return {@code (this * value) mod modulus} + * @throws ArithmeticException {@code value} < 0 or {@code modulus} == 0. + */ + public T multiplyMod(UInt256 value, UInt256 modulus) { + return ctor.apply(this.value.multiplyMod(value, modulus)); + } + + @Override + public T multiplyMod(long value, UInt256 modulus) { + return ctor.apply(this.value.multiplyMod(value, modulus)); + } + + @Override + public T multiplyMod(long value, long modulus) { + return ctor.apply(this.value.multiplyMod(value, modulus)); + } + + @Override + public T divide(T value) { + return divide(value.uint256Value()); + } + + /** + * Returns a value that is {@code (this / value)}. + * + * @param value The amount to divide this value by. + * @return {@code this / value} + * @throws ArithmeticException {@code value} == 0. + */ + public T divide(UInt256 value) { + return ctor.apply(this.value.divide(value)); + } + + @Override + public T divide(long value) { + return ctor.apply(this.value.divide(value)); + } + + @Override + public T pow(UInt256 exponent) { + return ctor.apply(this.value.pow(exponent)); + } + + @Override + public T pow(long exponent) { + return ctor.apply(this.value.pow(exponent)); + } + + @Override + public T mod(UInt256 modulus) { + return ctor.apply(this.value.mod(modulus)); + } + + @Override + public T mod(long modulus) { + return ctor.apply(this.value.mod(modulus)); + } + + @Override + public int compareTo(T other) { + return compareTo(other.uint256Value()); + } + + /** + * Compare two {@link UInt256} values. + * + * @param other The value to compare to. + * @return A negative integer, zero, or a positive integer as this value is less than, equal to, or greater than the + * specified value. + */ + public int compareTo(UInt256 other) { + return this.value.compareTo(other); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + // Note that we do want strictly class equality in this case: we don't want 2 quantity of + // mismatching unit to be considered equal, even if they do represent the same number. + if (this.getClass() != obj.getClass()) { + return false; + } + + UInt256Value other = (UInt256Value) obj; + return this.value.equals(other.uint256Value()); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public UInt256 uint256Value() { + return value; + } + + @Override + public Bytes32 toBytes() { + return value.toBytes(); + } + + @Override + public Bytes toMinimalBytes() { + return value.toMinimalBytes(); + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/UInt256.java b/units/src/main/java/net/consensys/cava/units/bigints/UInt256.java new file mode 100644 index 00000000..3f38afe7 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/UInt256.java @@ -0,0 +1,795 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; +import net.consensys.cava.bytes.MutableBytes; +import net.consensys.cava.bytes.MutableBytes32; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * An unsigned 256-bit precision number. + * + * This is a raw {@link UInt256Value} - a 256-bit precision unsigned number of no particular unit. + */ +public final class UInt256 implements UInt256Value { + private final static int MAX_CONSTANT = 64; + private final static BigInteger BI_MAX_CONSTANT = BigInteger.valueOf(MAX_CONSTANT); + private static UInt256 CONSTANTS[] = new UInt256[MAX_CONSTANT + 1]; + static { + CONSTANTS[0] = new UInt256(Bytes32.ZERO); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new UInt256(i); + } + } + + /** The minimum value of a UInt256 */ + public final static UInt256 MIN_VALUE = valueOf(0); + /** The maximum value of a UInt256 */ + public final static UInt256 MAX_VALUE = new UInt256(Bytes32.ZERO.not()); + /** The value 0 */ + public final static UInt256 ZERO = valueOf(0); + /** The value 1 */ + public final static UInt256 ONE = valueOf(1); + /** The value 10 */ + public final static UInt256 TEN = valueOf(10); + + private static final int INTS_SIZE = 32 / 4; + // The mask is used to obtain the value of an int as if it were unsigned. + private static final long LONG_MASK = 0xFFFFFFFFL; + private static final BigInteger P_2_256 = BigInteger.valueOf(2).pow(256); + + // The unsigned int components of the value + private final int ints[]; + + /** + * Return a {@code UInt256} containing the specified value. + * + * @param value The value to create a {@code UInt256} for. + * @return A {@code UInt256} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt256 valueOf(long value) { + checkArgument(value >= 0, "Argument must be positive"); + if (value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new UInt256(value); + } + + private UInt256(long value) { + this.ints = new int[INTS_SIZE]; + this.ints[6] = (int) ((value >>> 32) & LONG_MASK); + this.ints[7] = (int) (value & LONG_MASK); + } + + /** + * Return a {@link UInt256} containing the specified value. + * + * @param value The value to create a {@link UInt256} for. + * @return A {@link UInt256} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt256 valueOf(BigInteger value) { + checkArgument(value.signum() >= 0, "Argument must be positive"); + if (value.compareTo(BI_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + int[] ints = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + ints[i] = value.intValue(); + value = value.shiftRight(32); + } + return new UInt256(ints); + } + + /** + * Return a {@link UInt256} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt256}. + * @return A {@link UInt256} containing the specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 32}. + */ + public static UInt256 fromBytes(Bytes bytes) { + return new UInt256(Bytes32.leftPad(bytes)); + } + + /** + * Parse an hexadecimal string into a {@link UInt256}. + * + * @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. + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to valid hexadecimal representation or contains + * more than 32 bytes. + */ + public static UInt256 fromHexString(String str) { + return new UInt256(Bytes32.fromHexStringLenient(str)); + } + + private UInt256(Bytes32 bytes) { + this.ints = new int[INTS_SIZE]; + for (int i = 0, j = 0; i < INTS_SIZE; ++i, j += 4) { + ints[i] = bytes.getInt(j); + } + } + + private UInt256(int ints[]) { + this.ints = ints; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isZero() { + if (this == ZERO) { + return true; + } + for (int i = INTS_SIZE - 1; i >= 0; --i) { + if (this.ints[i] != 0) { + return false; + } + } + return true; + } + + @Override + public UInt256 add(UInt256 value) { + if (value.isZero()) { + return this; + } + if (isZero()) { + return value; + } + int result[] = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value.ints[INTS_SIZE - 1] & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + (value.ints[i] & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + @Override + public UInt256 add(long value) { + if (value == 0) { + return this; + } + if (value > 0 && isZero()) { + return UInt256.valueOf(value); + } + int result[] = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + sum = (this.ints[INTS_SIZE - 2] & LONG_MASK) + (value >>> 32) + (sum >>> 32); + result[INTS_SIZE - 2] = (int) (sum & LONG_MASK); + constant &= result[INTS_SIZE - 2] == 0; + long signExtent = (value >> 63) & LONG_MASK; + for (int i = INTS_SIZE - 3; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + signExtent + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + @Override + public UInt256 addMod(UInt256 value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt256.valueOf(bigIntegerValue().add(value.bigIntegerValue()).mod(modulus.bigIntegerValue())); + } + + @Override + public UInt256 addMod(long value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt256.valueOf(bigIntegerValue().add(BigInteger.valueOf(value)).mod(modulus.bigIntegerValue())); + } + + @Override + public UInt256 addMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("addMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("addMod unsigned with negative modulus"); + } + return UInt256.valueOf(bigIntegerValue().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus))); + } + + @Override + public UInt256 subtract(UInt256 value) { + if (value.isZero()) { + return this; + } + + int result[] = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + ((~value.ints[INTS_SIZE - 1]) & LONG_MASK) + 1; + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + ((~value.ints[i]) & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + @Override + public UInt256 subtract(long value) { + return add(-value); + } + + @Override + public UInt256 multiply(UInt256 value) { + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt256.ONE)) { + return this; + } + return multiply(this.ints, value.ints); + } + + private static UInt256 multiply(int[] x, int[] y) { + int result[] = new int[INTS_SIZE + INTS_SIZE]; + + long carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + INTS_SIZE - 1; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[INTS_SIZE - 1] & LONG_MASK) + carry; + result[k] = (int) product; + carry = product >>> 32; + } + result[INTS_SIZE - 1] = (int) carry; + + for (int i = INTS_SIZE - 2; i >= 0; i--) { + carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + i; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[i] & LONG_MASK) + (result[k] & LONG_MASK) + carry; + + result[k] = (int) product; + carry = product >>> 32; + } + result[i] = (int) carry; + } + + boolean constant = true; + for (int i = INTS_SIZE; i < (INTS_SIZE + INTS_SIZE) - 2; ++i) { + constant &= (result[i] == 0); + } + if (constant && result[INTS_SIZE + INTS_SIZE - 1] >= 0 && result[INTS_SIZE + INTS_SIZE - 1] <= MAX_CONSTANT) { + return CONSTANTS[result[INTS_SIZE + INTS_SIZE - 1]]; + } + return new UInt256(Arrays.copyOfRange(result, INTS_SIZE, INTS_SIZE + INTS_SIZE)); + } + + @Override + public UInt256 multiply(long value) { + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return this; + } + if (value < 0) { + throw new ArithmeticException("multiply unsigned by negative"); + } + UInt256 other = new UInt256(value); + return multiply(this.ints, other.ints); + } + + @Override + public UInt256 multiplyMod(UInt256 value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt256.ONE)) { + return mod(modulus); + } + return UInt256.valueOf(bigIntegerValue().multiply(value.bigIntegerValue()).mod(modulus.bigIntegerValue())); + } + + @Override + public UInt256 multiplyMod(long value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt256.valueOf(bigIntegerValue().multiply(BigInteger.valueOf(value)).mod(modulus.bigIntegerValue())); + } + + @Override + public UInt256 multiplyMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("multiplyMod unsigned with negative modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt256.valueOf(bigIntegerValue().multiply(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus))); + } + + @Override + public UInt256 divide(UInt256 value) { + if (value.isZero()) { + throw new ArithmeticException("divide by zero"); + } + if (value.equals(UInt256.ONE)) { + return this; + } + return UInt256.valueOf(bigIntegerValue().divide(value.bigIntegerValue())); + } + + @Override + public UInt256 divide(long value) { + if (value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value < 0) { + throw new ArithmeticException("divide unsigned by negative"); + } + if (value == 1) { + return this; + } + if (isPowerOf2(value)) { + return shiftRight(log2(value)); + } + return UInt256.valueOf(bigIntegerValue().divide(BigInteger.valueOf(value))); + } + + @Override + public UInt256 pow(UInt256 exponent) { + return UInt256.valueOf(bigIntegerValue().modPow(exponent.bigIntegerValue(), P_2_256)); + } + + @Override + public UInt256 pow(long exponent) { + return UInt256.valueOf(bigIntegerValue().modPow(BigInteger.valueOf(exponent), P_2_256)); + } + + @Override + public UInt256 mod(UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("mod by zero"); + } + return UInt256.valueOf(bigIntegerValue().mod(modulus.bigIntegerValue())); + } + + @Override + public UInt256 mod(long modulus) { + if (modulus == 0) { + throw new ArithmeticException("mod by zero"); + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + if (isPowerOf2(modulus)) { + int log2 = log2(modulus); + int d = log2 / 32; + int s = log2 % 32; + assert (d == 0 || d == 1); + + int[] result = new int[INTS_SIZE]; + // Mask the byte at d to only include the s right-most bits + result[INTS_SIZE - 1 - d] = this.ints[INTS_SIZE - 1 - d] & ~(0xFFFFFFFF << s); + if (d != 0) { + result[INTS_SIZE - 1] = this.ints[INTS_SIZE - 1]; + } + return new UInt256(result); + } + return UInt256.valueOf(bigIntegerValue().mod(BigInteger.valueOf(modulus))); + } + + /** + * Return a bit-wise AND of this value and the supplied value. + * + * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left. + * + * @param value The value to perform the operation with. + * @return The result of a bit-wise AND. + */ + public UInt256 and(UInt256 value) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] & value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise AND of this value 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 bytes The bytes to perform the operation with. + * @return The result of a bit-wise AND. + */ + public UInt256 and(Bytes32 bytes) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(i + 2) & 0xFF) << 8; + other |= ((int) bytes.get(i + 3) & 0xFF); + result[i] = this.ints[i] & other; + } + return new UInt256(result); + } + + /** + * Return a bit-wise OR of this value and the supplied value. + * + * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left. + * + * @param value The value to perform the operation with. + * @return The result of a bit-wise OR. + */ + public UInt256 or(UInt256 value) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] | value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise OR of this value 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 bytes The bytes to perform the operation with. + * @return The result of a bit-wise OR. + */ + public UInt256 or(Bytes32 bytes) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + result[i] = this.ints[i] | (((int) bytes.get(j) & 0xFF) << 24); + result[i] |= ((int) bytes.get(j + 1) & 0xFF) << 16; + result[i] |= ((int) bytes.get(j + 2) & 0xFF) << 8; + result[i] |= ((int) bytes.get(j + 3) & 0xFF); + } + return new UInt256(result); + } + + /** + * Return a bit-wise XOR of this value and the supplied value. + * + * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left. + * + * @param value The value to perform the operation with. + * @return The result of a bit-wise XOR. + */ + public UInt256 xor(UInt256 value) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] ^ value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise XOR of this value 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 bytes The bytes to perform the operation with. + * @return The result of a bit-wise XOR. + */ + public UInt256 xor(Bytes32 bytes) { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + result[i] = this.ints[i] ^ (((int) bytes.get(j) & 0xFF) << 24); + result[i] ^= ((int) bytes.get(j + 1) & 0xFF) << 16; + result[i] ^= ((int) bytes.get(j + 2) & 0xFF) << 8; + result[i] ^= ((int) bytes.get(j + 3) & 0xFF); + } + return new UInt256(result); + } + + /** + * Return a bit-wise NOT of this value. + * + * @return The result of a bit-wise NOT. + */ + public UInt256 not() { + int result[] = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = ~(this.ints[i]); + } + return new UInt256(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. + */ + public UInt256 shiftRight(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 256) { + return ZERO; + } + int result[] = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = INTS_SIZE; + if (s == 0) { + for (int i = INTS_SIZE - d; i > 0;) { + result[--resIdx] = this.ints[--i]; + } + } else { + for (int i = INTS_SIZE - 1 - d; i >= 0; i--) { + int leftSide = this.ints[i] >>> s; + int rightSide = (i == 0) ? 0 : this.ints[i - 1] << (32 - s); + result[--resIdx] = (leftSide | rightSide); + } + } + return new UInt256(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. + */ + public UInt256 shiftLeft(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 256) { + return ZERO; + } + int result[] = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = 0; + if (s == 0) { + for (int i = d; i < INTS_SIZE;) { + result[resIdx++] = this.ints[i++]; + } + } else { + for (int i = d; i < INTS_SIZE; ++i) { + int leftSide = this.ints[i] << s; + int rightSide = (i == INTS_SIZE - 1) ? 0 : (this.ints[i + 1] >>> (32 - s)); + result[resIdx++] = (leftSide | rightSide); + } + } + return new UInt256(result); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof UInt256)) { + return false; + } + UInt256 other = (UInt256) object; + for (int i = 0; i < INTS_SIZE; ++i) { + if (this.ints[i] != other.ints[i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = 1; + for (int i = 0; i < INTS_SIZE; ++i) { + result = 31 * result + this.ints[i]; + } + return result; + } + + @Override + public int compareTo(UInt256 other) { + for (int i = 0; i < INTS_SIZE; ++i) { + int cmp = Long.compare(((long) this.ints[i]) & LONG_MASK, ((long) other.ints[i]) & LONG_MASK); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + @Override + public boolean fitsInt() { + for (int i = 0; i < INTS_SIZE - 1; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 1] >= 0; + } + + @Override + public int intValue() { + if (!fitsInt()) { + throw new ArithmeticException("Value does not fit a 4 byte int"); + } + return this.ints[INTS_SIZE - 1]; + } + + @Override + public boolean fitsLong() { + for (int i = 0; i < INTS_SIZE - 2; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 2] >= 0; + } + + @Override + public long longValue() { + if (!fitsLong()) { + throw new ArithmeticException("Value does not fit a 8 byte long"); + } + return (((long) this.ints[INTS_SIZE - 2]) << 32) | (((long) (this.ints[INTS_SIZE - 1])) & LONG_MASK); + } + + @Override + public String toString() { + return bigIntegerValue().toString(); + } + + @Override + public BigInteger bigIntegerValue() { + byte mag[] = new byte[32]; + for (int i = 0, j = 0; i < INTS_SIZE; ++i) { + mag[j++] = (byte) (this.ints[i] >>> 24); + mag[j++] = (byte) ((this.ints[i] >>> 16) & 0xFF); + mag[j++] = (byte) ((this.ints[i] >>> 8) & 0xFF); + mag[j++] = (byte) (this.ints[i] & 0xFF); + } + return new BigInteger(1, mag); + } + + @Override + public UInt256 uint256Value() { + return this; + } + + @Override + public Bytes32 toBytes() { + MutableBytes32 bytes = MutableBytes32.create(); + for (int i = 0, j = 0; i < INTS_SIZE; ++i, j += 4) { + bytes.setInt(j, this.ints[i]); + } + return bytes; + } + + @Override + public Bytes toMinimalBytes() { + int i = 0; + while (i < INTS_SIZE && this.ints[i] == 0) { + ++i; + } + if (i == INTS_SIZE) { + return Bytes.EMPTY; + } + int firstIntBytes = 4 - (Integer.numberOfLeadingZeros(this.ints[i]) / 8); + int totalBytes = firstIntBytes + ((INTS_SIZE - (i + 1)) * 4); + MutableBytes bytes = MutableBytes.create(totalBytes); + int j = 0; + switch (firstIntBytes) { + case 4: + bytes.set(j++, (byte) (this.ints[i] >>> 24)); + // fall through + case 3: + bytes.set(j++, (byte) ((this.ints[i] >>> 16) & 0xFF)); + // fall through + case 2: + bytes.set(j++, (byte) ((this.ints[i] >>> 8) & 0xFF)); + // fall through + case 1: + bytes.set(j++, (byte) (this.ints[i] & 0xFF)); + } + ++i; + for (; i < INTS_SIZE; ++i, j += 4) { + bytes.setInt(j, this.ints[i]); + } + return bytes; + } + + @Override + public int numberOfLeadingZeros() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (i * 32) + Integer.numberOfLeadingZeros(this.ints[i]); + } + return 256; + } + + @Override + public int bitLength() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (INTS_SIZE * 32) - (i * 32) - Integer.numberOfLeadingZeros(this.ints[i]); + } + return 0; + } + + private static boolean isPowerOf2(long n) { + assert n > 0; + return (n & (n - 1)) == 0; + } + + private static int log2(long v) { + assert v > 0; + return 63 - Long.numberOfLeadingZeros(v); + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/UInt256Domain.java b/units/src/main/java/net/consensys/cava/units/bigints/UInt256Domain.java new file mode 100644 index 00000000..f7e95783 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/UInt256Domain.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +import com.google.common.collect.DiscreteDomain; + +/** + * A {@link DiscreteDomain} over {@link UInt256}. + */ +public final class UInt256Domain extends DiscreteDomain { + + @Override + public UInt256 next(UInt256 value) { + return value.add(1); + } + + @Override + public UInt256 previous(UInt256 value) { + return value.subtract(1); + } + + @Override + public long distance(UInt256 start, UInt256 end) { + boolean negativeDistance = start.compareTo(end) < 0; + UInt256 distance = negativeDistance ? end.subtract(start) : start.subtract(end); + if (!distance.fitsLong()) { + return negativeDistance ? Long.MIN_VALUE : Long.MAX_VALUE; + } + long distanceLong = distance.longValue(); + return negativeDistance ? -distanceLong : distanceLong; + } + + @Override + public UInt256 minValue() { + return UInt256.MIN_VALUE; + } + + @Override + public UInt256 maxValue() { + return UInt256.MAX_VALUE; + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/UInt256Value.java b/units/src/main/java/net/consensys/cava/units/bigints/UInt256Value.java new file mode 100644 index 00000000..8ddf1a87 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/UInt256Value.java @@ -0,0 +1,330 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.Bytes32; + +import java.math.BigInteger; + +/** + * Represents a 256-bit (32 bytes) unsigned integer value. + * + *

+ * A {@link UInt256Value} is an unsigned integer value stored with 32 bytes, so whose value can range between 0 and + * 2^256-1. + * + *

+ * This interface defines operations for value types with a 256-bit precision range. The methods provided by this + * interface take parameters of the same type (and also {@code long}. This provides type safety by ensuring calculations + * cannot mix different {@code UInt256Value} types. + * + *

+ * Where only a pure numerical 256-bit value is required, {@link UInt256} should be used. + * + *

+ * It is strongly advised to extend {@link BaseUInt256Value} rather than implementing this interface directly. Doing so + * provides type safety in that quantities of different units cannot be mixed accidentally. + * + * @param The concrete type of the value. + */ +public interface UInt256Value> extends Comparable { + + /** + * @return True if this is the value 0. + */ + default boolean isZero() { + return toBytes().isZero(); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value The amount to be added to this value. + * @return {@code this + value} + */ + T add(T value); + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value The amount to be added to this value. + * @return {@code this + value} + */ + T add(long value); + + /** + * Returns a value equivalent to {@code ((this + value) mod modulus)}. + * + * @param value The amount to be added to this value. + * @param modulus The modulus. + * @return {@code (this + value) mod modulus} + * @throws ArithmeticException {@code modulus} == 0. + */ + T addMod(T value, UInt256 modulus); + + /** + * Returns a value equivalent to {@code ((this + value) mod modulus)}. + * + * @param value The amount to be added to this value. + * @param modulus The modulus. + * @return {@code (this + value) mod modulus} + * @throws ArithmeticException {@code modulus} == 0. + */ + T addMod(long value, UInt256 modulus); + + /** + * Returns a value equivalent to {@code ((this + value) mod modulus)}. + * + * @param value The amount to be added to this value. + * @param modulus The modulus. + * @return {@code (this + value) mod modulus} + * @throws ArithmeticException {@code modulus} ≤ 0. + */ + T addMod(long value, long modulus); + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value The amount to be subtracted from this value. + * @return {@code this - value} + */ + T subtract(T value); + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value The amount to be subtracted from this value. + * @return {@code this - value} + */ + T subtract(long value); + + /** + * Returns a value that is {@code (this * value)}. + * + * @param value The amount to multiply this value by. + * @return {@code this * value} + */ + T multiply(T value); + + /** + * Returns a value that is {@code (this * value)}. + * + * @param value The amount to multiply this value by. + * @return {@code this * value} + * @throws ArithmeticException {@code value} < 0. + */ + T multiply(long value); + + /** + * Returns a value that is {@code ((this * value) mod modulus)}. + * + * @param value The amount to multiply this value by. + * @param modulus The modulus. + * @return {@code (this * value) mod modulus} + * @throws ArithmeticException {@code value} < 0 or {@code modulus} == 0. + */ + T multiplyMod(T value, UInt256 modulus); + + /** + * Returns a value that is {@code ((this * value) mod modulus)}. + * + * @param value The amount to multiply this value by. + * @param modulus The modulus. + * @return {@code (this * value) mod modulus} + * @throws ArithmeticException {@code value} < 0 or {@code modulus} == 0. + */ + T multiplyMod(long value, UInt256 modulus); + + /** + * Returns a value that is {@code ((this * value) mod modulus)}. + * + * @param value The amount to multiply this value by. + * @param modulus The modulus. + * @return {@code (this * value) mod modulus} + * @throws ArithmeticException {@code value} < 0 or {@code modulus} ≤ 0. + */ + T multiplyMod(long value, long modulus); + + /** + * Returns a value that is {@code (this / value)}. + * + * @param value The amount to divide this value by. + * @return {@code this / value} + * @throws ArithmeticException {@code value} == 0. + */ + T divide(T value); + + /** + * Returns a value that is {@code (this / value)}. + * + * @param value The amount to divide this value by. + * @return {@code this / value} + * @throws ArithmeticException {@code value} ≤ 0. + */ + T divide(long value); + + /** + * Returns a value that is (thisexponent mod 2256) + * + *

+ * This calculates an exponentiation over the modulus of {@code 2^256}. + * + *

+ * Note that {@code exponent} is an {@link UInt256} rather than of the type {@code T}. + * + * @param exponent The exponent to which this value is to be raised. + * @return thisexponent mod 2256 + */ + T pow(UInt256 exponent); + + /** + * Returns a value that is (thisexponent mod 2256) + * + *

+ * This calculates an exponentiation over the modulus of {@code 2^256}. + * + * @param exponent The exponent to which this value is to be raised. + * @return thisexponent mod 2256 + */ + T pow(long exponent); + + /** + * Returns a value that is {@code (this mod modulus)}. + * + * @param modulus The modulus. + * @return {@code this mod modulus}. + * @throws ArithmeticException {@code modulus} == 0. + */ + T mod(UInt256 modulus); + + /** + * Returns a value that is {@code (this mod modulus)}. + * + * @param modulus The modulus. + * @return {@code this mod modulus}. + * @throws ArithmeticException {@code modulus} ≤ 0. + */ + T mod(long modulus); + + /** + * @return True if this value fits a java {@code int} (i.e. is less or equal to {@code Integer.MAX_VALUE}). + */ + default boolean fitsInt() { + // Ints are 4 bytes, so anything but the 4 last bytes must be zeroes + Bytes32 bytes = toBytes(); + for (int i = 0; i < Bytes32.SIZE - 4; i++) { + if (bytes.get(i) != 0) + return false; + } + // Lastly, the left-most byte of the int must not start with a 1. + return bytes.get(Bytes32.SIZE - 4) >= 0; + } + + /** + * @return This value as a java {@code int} assuming it is small enough to fit an {@code int}. + * @throws ArithmeticException If the value does not fit an {@code int}, that is if {@code + * !fitsInt()}. + */ + default int intValue() { + if (!fitsInt()) { + throw new ArithmeticException("Value does not fit a 4 byte int"); + } + return toBytes().getInt(Bytes32.SIZE - 4); + } + + /** + * @return True if this value fits a java {@code long} (i.e. is less or equal to {@code Long.MAX_VALUE}). + */ + default boolean fitsLong() { + // Longs are 8 bytes, so anything but the 8 last bytes must be zeroes + for (int i = 0; i < Bytes32.SIZE - 8; i++) { + if (toBytes().get(i) != 0) + return false; + } + // Lastly, the left-most byte of the long must not start with a 1. + return toBytes().get(Bytes32.SIZE - 8) >= 0; + } + + /** + * @return This value as a java {@code long} assuming it is small enough to fit a {@code long}. + * @throws ArithmeticException If the value does not fit a {@code long}, that is if {@code + * !fitsLong()}. + */ + default long longValue() { + if (!fitsLong()) { + throw new ArithmeticException("Value does not fit a 8 byte long"); + } + return toBytes().getLong(Bytes32.SIZE - 8); + } + + /** + * @return This value as a {@link BigInteger}. + */ + default BigInteger bigIntegerValue() { + return toBytes().unsignedBigIntegerValue(); + } + + /** + * This value represented as an hexadecimal string. + * + *

+ * Note that this representation includes all the 32 underlying bytes, no matter what the integer actually represents + * (in other words, it can have many leading zeros). For a shorter representation that don't include leading zeros, + * use {@link #toShortHexString}. + * + * @return This value represented as an hexadecimal string. + */ + default String toHexString() { + return toBytes().toHexString(); + } + + /** @return This value represented as a minimal hexadecimal string (without any leading zero). */ + default String toShortHexString() { + return toBytes().toShortHexString(); + } + + /** + * Type-cast this value as a {@link UInt256}. + * + * @return This value as a {@link UInt256}. + */ + UInt256 uint256Value(); + + /** + * @return The value as bytes. + */ + Bytes32 toBytes(); + + /** + * @return The value as bytes without any leading zero bytes. + */ + Bytes toMinimalBytes(); + + /** + * @return the number of zero bits preceding the highest-order ("leftmost") one-bit in the binary representation of + * this value, or 256 if the value is equal to zero. + */ + default int numberOfLeadingZeros() { + return toBytes().numberOfLeadingZeros(); + } + + /** + * @return The number of bits following and including the highest-order ("leftmost") one-bit in the binary + * representation of this value, or zero if all bits are zero. + */ + default int bitLength() { + return toBytes().bitLength(); + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/UInt256ValueDomain.java b/units/src/main/java/net/consensys/cava/units/bigints/UInt256ValueDomain.java new file mode 100644 index 00000000..091a7717 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/UInt256ValueDomain.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +import java.util.function.Function; + +import com.google.common.collect.DiscreteDomain; + +/** + * A {@link DiscreteDomain} over a {@link UInt256Value}. + */ +public final class UInt256ValueDomain> extends DiscreteDomain { + + private final T minValue; + private final T maxValue; + + /** + * @param ctor The constructor for the {@link UInt256Value} type. + */ + public UInt256ValueDomain(Function ctor) { + this.minValue = ctor.apply(UInt256.MIN_VALUE); + this.maxValue = ctor.apply(UInt256.MAX_VALUE); + } + + @Override + public T next(T value) { + return value.add(1); + } + + @Override + public T previous(T value) { + return value.subtract(1); + } + + @Override + public long distance(T start, T end) { + boolean negativeDistance = start.compareTo(end) < 0; + T distance = negativeDistance ? end.subtract(start) : start.subtract(end); + if (!distance.fitsLong()) { + return negativeDistance ? Long.MIN_VALUE : Long.MAX_VALUE; + } + long distanceLong = distance.longValue(); + return negativeDistance ? -distanceLong : distanceLong; + } + + @Override + public T minValue() { + return minValue; + } + + @Override + public T maxValue() { + return maxValue; + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/UInt256s.java b/units/src/main/java/net/consensys/cava/units/bigints/UInt256s.java new file mode 100644 index 00000000..6d00908e --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/UInt256s.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +/** Static utility methods on UInt256 values. */ +public final class UInt256s { + private UInt256s() {} + + /** + * Returns the maximum of two UInt256 values. + * + * @param v1 The first value. + * @param v2 The second value. + * @return The maximum of {@code v1} and {@code v2}. + * @param The concrete type of the two values. + */ + public static > T max(T v1, T v2) { + return (v1.compareTo(v2)) >= 0 ? v1 : v2; + } + + /** + * Returns the minimum of two UInt256 values. + * + * @param v1 The first value. + * @param v2 The second value. + * @return The minimum of {@code v1} and {@code v2}. + * @param The concrete type of the two values. + */ + public static > T min(T v1, T v2) { + return (v1.compareTo(v2)) < 0 ? v1 : v2; + } +} diff --git a/units/src/main/java/net/consensys/cava/units/bigints/package-info.java b/units/src/main/java/net/consensys/cava/units/bigints/package-info.java new file mode 100644 index 00000000..53503315 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/bigints/package-info.java @@ -0,0 +1,7 @@ +/** + * Classes and utilities for working with 256 bit integers. + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.units.bigints; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/units/src/main/java/net/consensys/cava/units/ethereum/Gas.java b/units/src/main/java/net/consensys/cava/units/ethereum/Gas.java new file mode 100644 index 00000000..0bccabcb --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/ethereum/Gas.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.ethereum; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.bytes.MutableBytes; +import net.consensys.cava.units.bigints.UInt256; + +import java.math.BigInteger; + +import com.google.common.base.Objects; + +/** + * A unit measure of Gas as used by the Ethereum VM. + */ +public final class Gas { + + private final static int MAX_CONSTANT = 64; + private final static BigInteger BI_MAX_CONSTANT = BigInteger.valueOf(MAX_CONSTANT); + private final static UInt256 UINT256_MAX_CONSTANT = UInt256.valueOf(MAX_CONSTANT); + private static Gas CONSTANTS[] = new Gas[MAX_CONSTANT + 1]; + static { + CONSTANTS[0] = new Gas(0L); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new Gas(i); + } + } + + private final long value; + + private Gas(long value) { + this.value = value; + } + + /** + * Return a {@link Gas} containing the specified value. + * + * @param value The value to create a {@link Gas} for. + * @return A {@link Gas} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Gas valueOf(UInt256 value) { + if (value.compareTo(UINT256_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + if (!value.fitsLong()) { + throw new IllegalArgumentException("Gas value cannot be larger than 2^63 -1"); + } + return new Gas(value.longValue()); + } + + /** + * Return a {@link Gas} containing the specified value. + * + * @param value The value to create a {@link Gas} for. + * @return A {@link Gas} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Gas valueOf(long value) { + checkArgument(value >= 0, "Argument must be positive"); + if (value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new Gas(value); + } + + /** + * Return a {@link Gas} containing the specified value. + * + * @param value The value to create a {@link Gas} for. + * @return A {@link Gas} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Gas valueOf(BigInteger value) { + checkArgument(value.signum() >= 0, "Argument must be positive"); + if (value.compareTo(BI_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + try { + return new Gas(value.longValueExact()); + } catch (ArithmeticException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + /** + * The price of this amount of gas given the provided price per unit of gas. + * + * @param gasPrice The price per unit of gas. + * @return The price of this amount of gas for a per unit of gas price of {@code gasPrice}. + */ + public Wei priceFor(Wei gasPrice) { + return Wei.valueOf(gasPrice.uint256Value().multiply(value).uint256Value()); + } + + public Gas add(Gas other) { + return Gas.valueOf(Math.addExact(value, other.value)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Gas)) { + return false; + } + Gas gas = (Gas) o; + return value == gas.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + public Bytes toBytes() { + MutableBytes bytes = MutableBytes.create(8); + bytes.setLong(0, value); + return bytes; + } + + @Override + public String toString() { + return "Gas{" + "value=" + value + '}'; + } + + public Bytes toMinimalBytes() { + return Bytes.minimalBytes(value); + } + + public int compareTo(long other) { + return Long.compare(value, other); + } +} diff --git a/units/src/main/java/net/consensys/cava/units/ethereum/Wei.java b/units/src/main/java/net/consensys/cava/units/ethereum/Wei.java new file mode 100644 index 00000000..b34872f3 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/ethereum/Wei.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.ethereum; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.cava.units.bigints.BaseUInt256Value; +import net.consensys.cava.units.bigints.UInt256; + +import java.math.BigInteger; + +/** + * A unit measure of Wei as used by the Ethereum VM. + */ +public final class Wei extends BaseUInt256Value { + + private final static int MAX_CONSTANT = 64; + private final static BigInteger BI_MAX_CONSTANT = BigInteger.valueOf(MAX_CONSTANT); + private final static UInt256 UINT256_MAX_CONSTANT = UInt256.valueOf(MAX_CONSTANT); + private static Wei CONSTANTS[] = new Wei[MAX_CONSTANT + 1]; + static { + CONSTANTS[0] = new Wei(UInt256.ZERO); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new Wei(i); + } + } + + private Wei(UInt256 bytes) { + super(bytes, Wei::new); + } + + /** + * Return a {@link Wei} containing the specified value. + * + * @param value The value to create a {@link Wei} for. + * @return A {@link Wei} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Wei valueOf(UInt256 value) { + if (value.compareTo(UINT256_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + return new Wei(value); + } + + private Wei(long value) { + super(value, Wei::new); + } + + /** + * Return a {@link Wei} containing the specified value. + * + * @param value The value to create a {@link Wei} for. + * @return A {@link Wei} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Wei valueOf(long value) { + checkArgument(value >= 0, "Argument must be positive"); + if (value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new Wei(value); + } + + private Wei(BigInteger value) { + super(value, Wei::new); + } + + /** + * Return a {@link Wei} containing the specified value. + * + * @param value The value to create a {@link Wei} for. + * @return A {@link Wei} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static Wei valueOf(BigInteger value) { + checkArgument(value.signum() >= 0, "Argument must be positive"); + if (value.compareTo(BI_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + return new Wei(value); + } +} diff --git a/units/src/main/java/net/consensys/cava/units/ethereum/package-info.java b/units/src/main/java/net/consensys/cava/units/ethereum/package-info.java new file mode 100644 index 00000000..0b2e0441 --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/ethereum/package-info.java @@ -0,0 +1,7 @@ +/** + * Classes and utilities for working with Ethereum units. + */ +@ParametersAreNonnullByDefault +package net.consensys.cava.units.ethereum; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/units/src/main/java/net/consensys/cava/units/package-info.java b/units/src/main/java/net/consensys/cava/units/package-info.java new file mode 100644 index 00000000..f50d662b --- /dev/null +++ b/units/src/main/java/net/consensys/cava/units/package-info.java @@ -0,0 +1,8 @@ +/** + * Classes and utilities for working with 256 bit integers and Ethereum units. + * + *

+ * These classes are included in the standard Cava distribution, or separately when using the gradle dependency + * 'net.consensys.cava:cava-units' (cava-units.jar). + */ +package net.consensys.cava.units; diff --git a/units/src/test/java/net/consensys/cava/units/bigints/Uint256Test.java b/units/src/test/java/net/consensys/cava/units/bigints/Uint256Test.java new file mode 100644 index 00000000..4dd05b1c --- /dev/null +++ b/units/src/test/java/net/consensys/cava/units/bigints/Uint256Test.java @@ -0,0 +1,872 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.bytes.Bytes; + +import java.math.BigInteger; +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 UInt256Test { + + private static UInt256 v(long v) { + return UInt256.valueOf(v); + } + + private static UInt256 biv(String s) { + return UInt256.valueOf(new BigInteger(s)); + } + + private static UInt256 hv(String s) { + return UInt256.fromHexString(s); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(biv("9223372036854775807"), v(1), biv("9223372036854775808")), + Arguments.of(biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908352")), + Arguments.of(biv("13492324908428420834234908342"), v(23422141424214L), biv("13492324908428444256376332556")), + Arguments.of(UInt256.MAX_VALUE, v(1), v(0)), + Arguments.of(UInt256.MAX_VALUE, v(2), v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + v(1), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments + .of(hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), v(1), UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(0), 1L, v(1)), + Arguments.of(v(0), 100L, v(100)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(100), 90L, v(190)), + Arguments.of(biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908352")), + Arguments.of(biv("13492324908428420834234908342"), 23422141424214L, biv("13492324908428444256376332556")), + Arguments.of(UInt256.MAX_VALUE, 1L, v(0)), + Arguments.of(UInt256.MAX_VALUE, 2L, v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + 1L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), 1L, UInt256.MAX_VALUE), + Arguments.of(v(10), -5L, v(5)), + Arguments.of(v(0), -1L, UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt256.valueOf(2), v(0)), + Arguments.of(UInt256.MAX_VALUE.subtract(2), v(1), UInt256.MAX_VALUE, UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), v(1), UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt256UInt256Provider") + void addModUInt256UInt256(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModUInt256UInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), UInt256.ONE, UInt256.valueOf(2), v(0)), + Arguments.of(UInt256.MAX_VALUE.subtract(2), UInt256.ONE, UInt256.MAX_VALUE, UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), UInt256.ONE, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(3), UInt256.valueOf(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), UInt256.valueOf(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt256OfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt256Provider") + void addModLongUInt256(UInt256 v1, long v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), 1L, UInt256.valueOf(2), v(0)), + Arguments.of(UInt256.MAX_VALUE.subtract(2), 1L, UInt256.MAX_VALUE, UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), 1L, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(2), -1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), -7L, UInt256.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt256UInt256OfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt256.ONE, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(UInt256 v1, long v2, long m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModLongLongProvider() { + return Stream + .of(Arguments.of(v(0), 1L, 2L, v(1)), Arguments.of(v(1), 1L, 2L, v(0)), Arguments.of(v(2), 1L, 2L, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of(biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908332")), + Arguments.of(biv("13492324908428420834234908342"), v(23422141424214L), biv("13492324908428397412093484128")), + Arguments.of(v(0), v(1), UInt256.MAX_VALUE), + Arguments.of(v(1), v(2), UInt256.MAX_VALUE), + Arguments + .of(UInt256.MAX_VALUE, v(1), hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(2), 1L, v(1)), + Arguments.of(v(100), 100L, v(0)), + Arguments.of(biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908332")), + Arguments.of(biv("13492324908428420834234908342"), 23422141424214L, biv("13492324908428397412093484128")), + Arguments.of(v(0), 1L, UInt256.MAX_VALUE), + Arguments.of(v(1), 2L, UInt256.MAX_VALUE), + Arguments.of(UInt256.MAX_VALUE, 1L, hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of(v(0), -1L, v(1)), + Arguments.of(v(0), -100L, v(100)), + Arguments.of(v(2), -2L, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of(biv("13492324908428420834234908341"), v(2), biv("26984649816856841668469816682")), + Arguments.of(biv("13492324908428420834234908342"), v(2), biv("26984649816856841668469816684")), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of(biv("13492324908428420834234908342"), v(8), biv("107938599267427366673879266736")), + Arguments.of(biv("13492324908428420834234908342"), v(2048), biv("27632281412461405868513092284416")), + Arguments.of(biv("13492324908428420834234908342"), v(131072), biv("1768466010397529975584837906202624")), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(2)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(3), 2L, v(6)), + Arguments.of(v(4), 2L, v(8)), + Arguments.of(v(10), 18L, v(180)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("26984649816856841668469816682")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("26984649816856841668469816684")), + Arguments.of(v(2), 8L, v(16)), + Arguments.of(v(7), 8L, v(56)), + Arguments.of(v(8), 8L, v(64)), + Arguments.of(v(17), 8L, v(136)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("107938599267427366673879266736")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("27632281412461405868513092284416")), + Arguments.of(biv("13492324908428420834234908342"), 131072L, biv("1768466010397529975584837906202624")), + Arguments.of(v(22), 0L, v(0)), + Arguments.of( + hv("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt256.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt256.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(2), + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt256Provider") + void multiplyModLongUInt256(UInt256 v1, long v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 5L, UInt256.valueOf(2), v(0)), + Arguments.of(v(2), 3L, UInt256.valueOf(7), v(6)), + Arguments.of(v(2), 3L, UInt256.valueOf(6), v(0)), + Arguments.of(v(2), 0L, UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1L, UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt256.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(UInt256 v1, long v2, long m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5L, 2L, v(0)), + Arguments.of(v(2), 3L, 7L, v(6)), + Arguments.of(v(2), 3L, 6L, v(0)), + Arguments.of(v(2), 0L, 6L, v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + Long.MAX_VALUE, + hv("0x000000000000000000000000000000000000000000000000000000000000001C"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of(biv("13492324908428420834234908341"), v(2), biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), v(2), biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), v(2), biv("6746162454214210417117454171")), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128)), + Arguments.of(biv("13492324908428420834234908342"), v(8), biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), v(2048), biv("6588049271693564860466263")), + Arguments.of(biv("13492324908428420834234908342"), v(131072), biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(0)), + Arguments.of(v(2), 2L, v(1)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(4), 2L, v(2)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), 2L, biv("6746162454214210417117454171")), + Arguments.of(v(2), 8L, v(0)), + Arguments.of(v(7), 8L, v(0)), + Arguments.of(v(8), 8L, v(1)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(17), 8L, v(2)), + Arguments.of(v(1024), 8L, v(128)), + Arguments.of(v(1026), 8L, v(128)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("6588049271693564860466263")), + Arguments.of(biv("13492324908428420834234908342"), 131072L, biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt256Provider") + void powUInt256(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + private static Stream powUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), UInt256.valueOf(2), v(4)), + Arguments.of(v(2), UInt256.valueOf(8), v(256)), + Arguments.of(v(3), UInt256.valueOf(3), v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + UInt256.valueOf(3), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(2), 8L, v(256)), + Arguments.of(v(3), 3L, v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + 3L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000")), + Arguments.of(v(3), -3L, hv("0x2F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA13"))); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(1)), + Arguments.of(v(2), 2L, v(0)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(biv("13492324908428420834234908342"), 2L, v(0)), + Arguments.of(biv("13492324908428420834234908343"), 2L, v(1)), + Arguments.of(v(0), 8L, v(0)), + Arguments.of(v(1), 8L, v(1)), + Arguments.of(v(2), 8L, v(2)), + Arguments.of(v(3), 8L, v(3)), + Arguments.of(v(7), 8L, v(7)), + Arguments.of(v(8), 8L, v(0)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(1024), 8L, v(0)), + Arguments.of(v(1026), 8L, v(2)), + Arguments.of(biv("13492324908428420834234908342"), 8L, v(6)), + Arguments.of(biv("13492324908428420834234908343"), 8L, v(7)), + Arguments.of(biv("13492324908428420834234908344"), 8L, v(0))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("andProvider") + void and(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.and(v2)); + } + + private static Stream andProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x000000000000000000000000000000FF00000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("orProvider") + void or(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.or(v2)); + } + + private static Stream orProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x0000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("xorProvider") + void xor(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.xor(v2)); + } + + private static Stream xorProvider() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("notProvider") + void not(UInt256 value, UInt256 expected) { + assertValueEquals(expected, value.not()); + } + + private static Stream notProvider() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftLeftProvider") + void shiftLeft(UInt256 value, int distance, UInt256 expected) { + assertValueEquals(expected, value.shiftLeft(distance)); + } + + private static Stream shiftLeftProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x02")), + Arguments.of(hv("0x01"), 2, hv("0x04")), + Arguments.of(hv("0x01"), 8, hv("0x0100")), + Arguments.of(hv("0x01"), 9, hv("0x0200")), + Arguments.of(hv("0x01"), 16, hv("0x10000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")), + Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")), + Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000001"), + 16, + hv("0x0000000000000000000000000000000000000000000000000000000000010000")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000001"), + 15, + hv("0x0000000000000000000000000000000000000000000000000000000000008000")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 202, + hv("0xFFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftRightProvider") + void shiftRight(UInt256 value, int distance, UInt256 expected) { + assertValueEquals(expected, value.shiftRight(distance)); + } + + private static Stream shiftRightProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x00")), + Arguments.of(hv("0x10"), 1, hv("0x08")), + Arguments.of(hv("0x10"), 2, hv("0x04")), + Arguments.of(hv("0x10"), 8, hv("0x00")), + Arguments.of(hv("0x1000"), 4, hv("0x0100")), + Arguments.of(hv("0x1000"), 5, hv("0x0080")), + Arguments.of(hv("0x1000"), 8, hv("0x0010")), + Arguments.of(hv("0x1000"), 9, hv("0x0008")), + Arguments.of(hv("0x1000"), 16, hv("0x0000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")), + Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")), + Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 16, + hv("0x0000100000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 15, + hv("0x0000200000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv("0x00000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000"), + 202, + hv("0x000000000000000000000000000000000000000000000000003FFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("intValueProvider") + void intValue(UInt256 value, int expected) { + assertEquals(expected, value.intValue()); + } + + private static Stream intValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0), + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x00000000"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x0001"), 1), + Arguments.of(hv("0x000001"), 1), + Arguments.of(hv("0x00000001"), 1), + Arguments.of(hv("0x0100"), 256), + Arguments.of(hv("0x000100"), 256), + Arguments.of(hv("0x00000100"), 256)); + } + + @Test + void shouldThrowForIntValueOfOversizeValue() { + Throwable exception = assertThrows(ArithmeticException.class, () -> hv("0x0100000000").intValue()); + assertEquals("Value does not fit a 4 byte int", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("longValueProvider") + void longValue(UInt256 value, long expected) { + assertEquals(expected, value.longValue()); + } + + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0L), + Arguments.of(hv("0x00"), 0L), + Arguments.of(hv("0x00000000"), 0L), + Arguments.of(hv("0x01"), 1L), + Arguments.of(hv("0x0001"), 1L), + Arguments.of(hv("0x000001"), 1L), + Arguments.of(hv("0x00000001"), 1L), + Arguments.of(hv("0x0000000001"), 1L), + Arguments.of(hv("0x000000000001"), 1L), + Arguments.of(hv("0x0100"), 256L), + Arguments.of(hv("0x000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x000000000100"), 256L), + Arguments.of(hv("0x00000000000100"), 256L), + Arguments.of(hv("0x0000000000000100"), 256L), + Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1)); + } + + @Test + void shouldThrowForLongValueOfOversizeValue() { + Throwable exception = assertThrows(ArithmeticException.class, () -> hv("0x010000000000000000").longValue()); + assertEquals("Value does not fit a 8 byte long", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(UInt256 v1, UInt256 v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1), + Arguments.of( + hv("0x000000000000000000000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 1), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(UInt256 value, Bytes expected) { + assertEquals(expected, value.toBytes()); + } + + private static Stream toBytesProvider() { + return Stream.of( + Arguments + .of(hv("0x00"), Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x01000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000001000000")), + Arguments.of( + hv("0x0100000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000100000000")), + Arguments.of( + hv("0xf100000000ab"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000f100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(UInt256 value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0xf100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(UInt256 value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 256), + Arguments.of(hv("0x01"), 255), + Arguments.of(hv("0x02"), 254), + Arguments.of(hv("0x03"), 254), + Arguments.of(hv("0x0F"), 252), + Arguments.of(hv("0x8F"), 248), + Arguments.of(hv("0x100000000"), 223)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(UInt256 value, int expected) { + assertEquals(expected, value.bitLength()); + } + + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x100000000"), 33)); + } + + private void assertValueEquals(UInt256 expected, UInt256 actual) { + String msg = String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } +} diff --git a/units/src/test/java/net/consensys/cava/units/bigints/test/BaseUInt256ValueTest.java b/units/src/test/java/net/consensys/cava/units/bigints/test/BaseUInt256ValueTest.java new file mode 100644 index 00000000..9d500669 --- /dev/null +++ b/units/src/test/java/net/consensys/cava/units/bigints/test/BaseUInt256ValueTest.java @@ -0,0 +1,865 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.bytes.Bytes; +import net.consensys.cava.units.bigints.BaseUInt256Value; +import net.consensys.cava.units.bigints.UInt256; + +import java.math.BigInteger; +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; + +// This test is in a `test` sub-package to ensure that it does not have access to package-private +// methods within the bigints package, as it should be testing the usage of the public API. +class BaseUInt256ValueTest { + + private static class Value extends BaseUInt256Value { + static final Value MAX_VALUE = new Value(UInt256.MAX_VALUE); + + Value(UInt256 v) { + super(v, Value::new); + } + + Value(long v) { + super(v, Value::new); + } + + Value(BigInteger s) { + super(s, Value::new); + } + } + + private static Value v(long v) { + return new Value(v); + } + + private static Value biv(String s) { + return new Value(new BigInteger(s)); + } + + private static Value hv(String s) { + return new Value(UInt256.fromHexString(s)); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(Value v1, Value v2, Value expected) { + assertValueEquals(expected, v1.add(v2)); + } + + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908352")), + Arguments.of(biv("13492324908428420834234908342"), v(23422141424214L), biv("13492324908428444256376332556")), + Arguments.of(Value.MAX_VALUE, v(1), v(0)), + Arguments.of(Value.MAX_VALUE, v(2), v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + v(1), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), v(1), Value.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addUInt256Provider") + void addUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.add(v2)); + } + + private static Stream addUInt256Provider() { + return Stream.of( + Arguments.of(v(1), UInt256.ZERO, v(1)), + Arguments.of(v(5), UInt256.ZERO, v(5)), + Arguments.of(v(0), UInt256.ONE, v(1)), + Arguments.of(v(0), UInt256.valueOf(100), v(100)), + Arguments.of(v(2), UInt256.valueOf(2), v(4)), + Arguments.of(v(100), UInt256.valueOf(90), v(190)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(10), biv("13492324908428420834234908352")), + Arguments.of( + biv("13492324908428420834234908342"), + UInt256.valueOf(23422141424214L), + biv("13492324908428444256376332556")), + Arguments.of(Value.MAX_VALUE, UInt256.valueOf(1), v(0)), + Arguments.of(Value.MAX_VALUE, UInt256.valueOf(2), v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + UInt256.valueOf(1), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + UInt256.valueOf(1), + Value.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.add(v2)); + } + + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(0), 1L, v(1)), + Arguments.of(v(0), 100L, v(100)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(100), 90L, v(190)), + Arguments.of(biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908352")), + Arguments.of(biv("13492324908428420834234908342"), 23422141424214L, biv("13492324908428444256376332556")), + Arguments.of(Value.MAX_VALUE, 1L, v(0)), + Arguments.of(Value.MAX_VALUE, 2L, v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + 1L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), 1L, Value.MAX_VALUE), + Arguments.of(v(10), -5L, v(5)), + Arguments.of(v(0), -1L, Value.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(Value v1, Value v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt256.valueOf(2), v(0)), + Arguments.of(Value.MAX_VALUE.subtract(2), v(1), UInt256.MAX_VALUE, Value.MAX_VALUE.subtract(1)), + Arguments.of(Value.MAX_VALUE.subtract(1), v(1), UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt256UInt256Provider") + void addModUInt256UInt256(Value v1, UInt256 v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModUInt256UInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), UInt256.ONE, UInt256.valueOf(2), v(0)), + Arguments.of(Value.MAX_VALUE.subtract(2), UInt256.ONE, UInt256.MAX_VALUE, Value.MAX_VALUE.subtract(1)), + Arguments.of(Value.MAX_VALUE.subtract(1), UInt256.ONE, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(3), UInt256.valueOf(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), UInt256.valueOf(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt256OfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt256Provider") + void addModLongUInt256(Value v1, long v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), 1L, UInt256.valueOf(2), v(0)), + Arguments.of(Value.MAX_VALUE.subtract(2), 1L, UInt256.MAX_VALUE, Value.MAX_VALUE.subtract(1)), + Arguments.of(Value.MAX_VALUE.subtract(1), 1L, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(2), -1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), -7L, UInt256.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt256UInt256OfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt256.ONE, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(Value v1, long v2, long m, Value expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + private static Stream addModLongLongProvider() { + return Stream + .of(Arguments.of(v(0), 1L, 2L, v(1)), Arguments.of(v(1), 1L, 2L, v(0)), Arguments.of(v(2), 1L, 2L, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(Value v1, Value v2, Value expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of(biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908332")), + Arguments.of(biv("13492324908428420834234908342"), v(23422141424214L), biv("13492324908428397412093484128")), + Arguments.of(v(0), v(1), Value.MAX_VALUE), + Arguments.of(v(1), v(2), Value.MAX_VALUE), + Arguments.of(Value.MAX_VALUE, v(1), hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractUInt256Provider") + void subtractUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + private static Stream subtractUInt256Provider() { + return Stream.of( + Arguments.of(v(1), UInt256.ZERO, v(1)), + Arguments.of(v(5), UInt256.ZERO, v(5)), + Arguments.of(v(2), UInt256.ONE, v(1)), + Arguments.of(v(100), UInt256.valueOf(100), v(0)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(10), biv("13492324908428420834234908332")), + Arguments.of( + biv("13492324908428420834234908342"), + UInt256.valueOf(23422141424214L), + biv("13492324908428397412093484128")), + Arguments.of(v(0), UInt256.ONE, Value.MAX_VALUE), + Arguments.of(v(1), UInt256.valueOf(2), Value.MAX_VALUE), + Arguments.of( + Value.MAX_VALUE, + UInt256.ONE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(2), 1L, v(1)), + Arguments.of(v(100), 100L, v(0)), + Arguments.of(biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908332")), + Arguments.of(biv("13492324908428420834234908342"), 23422141424214L, biv("13492324908428397412093484128")), + Arguments.of(v(0), 1L, Value.MAX_VALUE), + Arguments.of(v(1), 2L, Value.MAX_VALUE), + Arguments.of(Value.MAX_VALUE, 1L, hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of(v(0), -1L, v(1)), + Arguments.of(v(0), -100L, v(100)), + Arguments.of(v(2), -2L, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(Value v1, Value v2, Value expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of(biv("13492324908428420834234908341"), v(2), biv("26984649816856841668469816682")), + Arguments.of(biv("13492324908428420834234908342"), v(2), biv("26984649816856841668469816684")), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of(biv("13492324908428420834234908342"), v(8), biv("107938599267427366673879266736")), + Arguments.of(biv("13492324908428420834234908342"), v(2048), biv("27632281412461405868513092284416")), + Arguments.of(biv("13492324908428420834234908342"), v(131072), biv("1768466010397529975584837906202624")), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyUInt256Provider") + void multiplyUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + private static Stream multiplyUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(1), UInt256.valueOf(2), v(2)), + Arguments.of(v(2), UInt256.valueOf(2), v(4)), + Arguments.of(v(3), UInt256.valueOf(2), v(6)), + Arguments.of(v(4), UInt256.valueOf(2), v(8)), + Arguments.of(v(10), UInt256.valueOf(18), v(180)), + Arguments.of(biv("13492324908428420834234908341"), UInt256.valueOf(2), biv("26984649816856841668469816682")), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(2), biv("26984649816856841668469816684")), + Arguments.of(v(2), UInt256.valueOf(8), v(16)), + Arguments.of(v(7), UInt256.valueOf(8), v(56)), + Arguments.of(v(8), UInt256.valueOf(8), v(64)), + Arguments.of(v(17), UInt256.valueOf(8), v(136)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(8), biv("107938599267427366673879266736")), + Arguments + .of(biv("13492324908428420834234908342"), UInt256.valueOf(2048), biv("27632281412461405868513092284416")), + Arguments.of( + biv("13492324908428420834234908342"), + UInt256.valueOf(131072), + biv("1768466010397529975584837906202624")), + Arguments.of(v(22), UInt256.ZERO, v(0)), + Arguments.of( + hv("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + UInt256.valueOf(2), + hv("0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + UInt256.valueOf(2), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(2)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(3), 2L, v(6)), + Arguments.of(v(4), 2L, v(8)), + Arguments.of(v(10), 18L, v(180)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("26984649816856841668469816682")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("26984649816856841668469816684")), + Arguments.of(v(2), 8L, v(16)), + Arguments.of(v(7), 8L, v(56)), + Arguments.of(v(8), 8L, v(64)), + Arguments.of(v(17), 8L, v(136)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("107938599267427366673879266736")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("27632281412461405868513092284416")), + Arguments.of(biv("13492324908428420834234908342"), 131072L, biv("1768466010397529975584837906202624")), + Arguments.of(v(22), 0L, v(0)), + Arguments.of( + hv("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(Value v1, Value v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt256.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt256.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(2), + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModUInt256UInt256Provider") + void multiplyModUInt256UInt256(Value v1, UInt256 v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModUInt256UInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(5), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), UInt256.valueOf(3), UInt256.valueOf(7), v(6)), + Arguments.of(v(2), UInt256.valueOf(3), UInt256.valueOf(6), v(0)), + Arguments.of(v(2), UInt256.ZERO, UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + UInt256.valueOf(2), + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModUInt256UInt256OfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(UInt256.valueOf(5), UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt256Provider") + void multiplyModLongUInt256(Value v1, long v2, UInt256 m, Value expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 5L, UInt256.valueOf(2), v(0)), + Arguments.of(v(2), 3L, UInt256.valueOf(7), v(6)), + Arguments.of(v(2), 3L, UInt256.valueOf(6), v(0)), + Arguments.of(v(2), 0L, UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(1L, UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(-1, UInt256.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(Value v1, long v2, long m, Value expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5L, 2L, v(0)), + Arguments.of(v(2), 3L, 7L, v(6)), + Arguments.of(v(2), 3L, 6L, v(0)), + Arguments.of(v(2), 0L, 6L, v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + Long.MAX_VALUE, + hv("0x000000000000000000000000000000000000000000000000000000000000001C"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(Value v1, Value v2, Value expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of(biv("13492324908428420834234908341"), v(2), biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), v(2), biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), v(2), biv("6746162454214210417117454171")), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128)), + Arguments.of(biv("13492324908428420834234908342"), v(8), biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), v(2048), biv("6588049271693564860466263")), + Arguments.of(biv("13492324908428420834234908342"), v(131072), biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideUInt256Provider") + void divideUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + private static Stream divideUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(1), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), UInt256.valueOf(2), v(1)), + Arguments.of(v(3), UInt256.valueOf(2), v(1)), + Arguments.of(v(4), UInt256.valueOf(2), v(2)), + Arguments.of(biv("13492324908428420834234908341"), UInt256.valueOf(2), biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(2), biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), UInt256.valueOf(2), biv("6746162454214210417117454171")), + Arguments.of(v(2), UInt256.valueOf(8), v(0)), + Arguments.of(v(7), UInt256.valueOf(8), v(0)), + Arguments.of(v(8), UInt256.valueOf(8), v(1)), + Arguments.of(v(9), UInt256.valueOf(8), v(1)), + Arguments.of(v(17), UInt256.valueOf(8), v(2)), + Arguments.of(v(1024), UInt256.valueOf(8), v(128)), + Arguments.of(v(1026), UInt256.valueOf(8), v(128)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(8), biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(2048), biv("6588049271693564860466263")), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(131072), biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideUInt256ByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(UInt256.ZERO)); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(0)), + Arguments.of(v(2), 2L, v(1)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(4), 2L, v(2)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), 2L, biv("6746162454214210417117454171")), + Arguments.of(v(2), 8L, v(0)), + Arguments.of(v(7), 8L, v(0)), + Arguments.of(v(8), 8L, v(1)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(17), 8L, v(2)), + Arguments.of(v(1024), 8L, v(128)), + Arguments.of(v(1026), 8L, v(128)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("6588049271693564860466263")), + Arguments.of(biv("13492324908428420834234908342"), 131072L, biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt256Provider") + void powUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + private static Stream powUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), UInt256.valueOf(2), v(4)), + Arguments.of(v(2), UInt256.valueOf(8), v(256)), + Arguments.of(v(3), UInt256.valueOf(3), v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + UInt256.valueOf(3), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(2), 8L, v(256)), + Arguments.of(v(3), 3L, v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + 3L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000")), + Arguments.of(v(3), -3L, hv("0x2F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA13"))); + } + + @ParameterizedTest + @MethodSource("modUInt256Provider") + void modUInt256(Value v1, UInt256 v2, Value expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + private static Stream modUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(2), UInt256.valueOf(2), v(0)), + Arguments.of(v(3), UInt256.valueOf(2), v(1)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(2), v(0)), + Arguments.of(biv("13492324908428420834234908343"), UInt256.valueOf(2), v(1)), + Arguments.of(v(0), UInt256.valueOf(8), v(0)), + Arguments.of(v(1), UInt256.valueOf(8), v(1)), + Arguments.of(v(2), UInt256.valueOf(8), v(2)), + Arguments.of(v(3), UInt256.valueOf(8), v(3)), + Arguments.of(v(7), UInt256.valueOf(8), v(7)), + Arguments.of(v(8), UInt256.valueOf(8), v(0)), + Arguments.of(v(9), UInt256.valueOf(8), v(1)), + Arguments.of(v(1024), UInt256.valueOf(8), v(0)), + Arguments.of(v(1026), UInt256.valueOf(8), v(2)), + Arguments.of(biv("13492324908428420834234908342"), UInt256.valueOf(8), v(6)), + Arguments.of(biv("13492324908428420834234908343"), UInt256.valueOf(8), v(7)), + Arguments.of(biv("13492324908428420834234908344"), UInt256.valueOf(8), v(0))); + } + + @Test + void shouldThrowForModUInt256ByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(UInt256.ZERO)); + assertEquals("mod by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(Value v1, long v2, Value expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(1)), + Arguments.of(v(2), 2L, v(0)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(biv("13492324908428420834234908342"), 2L, v(0)), + Arguments.of(biv("13492324908428420834234908343"), 2L, v(1)), + Arguments.of(v(0), 8L, v(0)), + Arguments.of(v(1), 8L, v(1)), + Arguments.of(v(2), 8L, v(2)), + Arguments.of(v(3), 8L, v(3)), + Arguments.of(v(7), 8L, v(7)), + Arguments.of(v(8), 8L, v(0)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(1024), 8L, v(0)), + Arguments.of(v(1026), 8L, v(2)), + Arguments.of(biv("13492324908428420834234908342"), 8L, v(6)), + Arguments.of(biv("13492324908428420834234908343"), 8L, v(7)), + Arguments.of(biv("13492324908428420834234908344"), 8L, v(0))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(Value v1, Value v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1), + Arguments.of( + hv("0x000000000000000000000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 1), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(Value value, Bytes expected) { + assertEquals(expected, value.toBytes()); + } + + private static Stream toBytesProvider() { + return Stream.of( + Arguments + .of(hv("0x00"), Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x01000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000001000000")), + Arguments.of( + hv("0x0100000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000100000000")), + Arguments.of( + hv("0xf100000000ab"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000f100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(Value value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0xf100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(Value value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 256), + Arguments.of(hv("0x01"), 255), + Arguments.of(hv("0x02"), 254), + Arguments.of(hv("0x03"), 254), + Arguments.of(hv("0x0F"), 252), + Arguments.of(hv("0x8F"), 248), + Arguments.of(hv("0x100000000"), 223)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(Value value, int expected) { + assertEquals(expected, value.bitLength()); + } + + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x100000000"), 33)); + } + + private void assertValueEquals(Value expected, Value actual) { + String msg = String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } +} diff --git a/units/src/test/java/net/consensys/cava/units/bigints/test/GasTest.java b/units/src/test/java/net/consensys/cava/units/bigints/test/GasTest.java new file mode 100644 index 00000000..9c140264 --- /dev/null +++ b/units/src/test/java/net/consensys/cava/units/bigints/test/GasTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.units.ethereum.Gas; +import net.consensys.cava.units.ethereum.Wei; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class GasTest { + + @Test + void testOverflowThroughAddition() { + Gas max = Gas.valueOf(Long.MAX_VALUE); + assertThrows(ArithmeticException.class, () -> { + max.add(Gas.valueOf(1L)); + }); + } + + @Test + void testOverflow() { + assertThrows(IllegalArgumentException.class, () -> { + Gas.valueOf(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)); + }); + } + + @Test + void testGetWeiPrice() { + Gas gas = Gas.valueOf(5L); + Wei result = gas.priceFor(Wei.valueOf(3L)); + assertEquals(15, result.intValue()); + } + + @Test + void testReuseConstants() { + List oneTime = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + oneTime.add(Gas.valueOf((long) i)); + } + List secondTime = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + secondTime.add(Gas.valueOf((long) i)); + } + for (int i = 0; i < 128; i++) { + Gas first = oneTime.get(i); + Gas second = secondTime.get(i); + if (i <= 64) { + assertSame(first, second); + } else { + assertNotSame(first, second); + assertEquals(first, second); + } + } + } + + @Test + void testNegativeLong() { + assertThrows(IllegalArgumentException.class, () -> { + Gas.valueOf(-1L); + }); + } + + @Test + void testNegativeBigInteger() { + assertThrows(IllegalArgumentException.class, () -> { + Gas.valueOf(BigInteger.valueOf(-123L)); + }); + } + +} diff --git a/units/src/test/java/net/consensys/cava/units/bigints/test/WeiTest.java b/units/src/test/java/net/consensys/cava/units/bigints/test/WeiTest.java new file mode 100644 index 00000000..6e7b6434 --- /dev/null +++ b/units/src/test/java/net/consensys/cava/units/bigints/test/WeiTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018, ConsenSys Inc. + * + * 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.units.bigints.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.consensys.cava.units.ethereum.Wei; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class WeiTest { + + @Test + void testReuseConstants() { + List oneTime = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + oneTime.add(Wei.valueOf((long) i)); + } + List secondTime = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + secondTime.add(Wei.valueOf((long) i)); + } + for (int i = 0; i < 128; i++) { + Wei first = oneTime.get(i); + Wei second = secondTime.get(i); + if (i <= 64) { + assertSame(first, second); + } else { + assertNotSame(first, second); + assertEquals(first, second); + } + } + } + + @Test + void testNegativeLong() { + assertThrows(IllegalArgumentException.class, () -> { + Wei.valueOf(-1L); + }); + } + + @Test + void testNegativeBigInteger() { + assertThrows(IllegalArgumentException.class, () -> { + Wei.valueOf(BigInteger.valueOf(-123L)); + }); + } +}