mirror of
https://github.com/vacp2p/status-linea-besu.git
synced 2026-01-09 15:28:09 -05:00
Initial commit
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
This commit is contained in:
4
.dockerignore
Executable file
4
.dockerignore
Executable file
@@ -0,0 +1,4 @@
|
||||
.gradle
|
||||
.idea
|
||||
.vertx
|
||||
build
|
||||
5
.gitattributes
vendored
Executable file
5
.gitattributes
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
* text eol=lf
|
||||
*.jar -text
|
||||
*.bat -text
|
||||
*.pcap binary
|
||||
*.blocks binary
|
||||
27
.gitignore
vendored
Executable file
27
.gitignore
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
*.bak
|
||||
*.swp
|
||||
*.tmp
|
||||
*~.nib
|
||||
*.iml
|
||||
*.launch
|
||||
*.swp
|
||||
*.log
|
||||
.classpath
|
||||
.DS_Store
|
||||
.externalToolBuilders/
|
||||
.gradle/
|
||||
.idea/
|
||||
.loadpath
|
||||
.metadata
|
||||
.prefs
|
||||
.project
|
||||
.recommenders/
|
||||
.settings
|
||||
.springBeans
|
||||
.vertx
|
||||
bin/
|
||||
local.properties
|
||||
target/
|
||||
tmp/
|
||||
build/
|
||||
out/
|
||||
4
.gitmodules
vendored
Executable file
4
.gitmodules
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
[submodule "eth-ref-tests"]
|
||||
path = ethereum/referencetests/src/test/resources
|
||||
url = https://github.com/ethereum/tests.git
|
||||
ignore = all
|
||||
60
CONTRIBUTING.md
Executable file
60
CONTRIBUTING.md
Executable file
@@ -0,0 +1,60 @@
|
||||
# Contributing to Pantheon
|
||||
|
||||
Welcome to the Pantheon repository! This document describes the procedure and guidelines for contributing to the Pantheon 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. That being said, before the pull request is merged, it should be squashed.
|
||||
|
||||
#### Stylistic
|
||||
|
||||
The fundamental resource Pantheon 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 [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html), as the the [Google Java Style reformatter](https://github.com/google/google-java-format) is a component piece of our continuous integration architecture, code that does not comply stylistically will fail to pass the requisite candidate tests.
|
||||
|
||||
#### 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.
|
||||
13
Dockerfile
Executable file
13
Dockerfile
Executable file
@@ -0,0 +1,13 @@
|
||||
# Base Alpine Linux based image with OpenJDK JRE only
|
||||
#FROM openjdk:8-jre-alpine
|
||||
FROM openjdk:8-jdk
|
||||
|
||||
# copy application (with libraries inside)
|
||||
ADD build/install/pantheon /opt/pantheon/
|
||||
ADD integration-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/genesis.json /opt/pantheon/genesis.json
|
||||
|
||||
# List Exposed Ports
|
||||
EXPOSE 8084 8545 30303 30303/udp
|
||||
|
||||
# specify default command
|
||||
ENTRYPOINT ["/opt/pantheon/bin/pantheon"]
|
||||
69
Jenkinsfile
vendored
Executable file
69
Jenkinsfile
vendored
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env groovy
|
||||
|
||||
if (env.BRANCH_NAME == "master") {
|
||||
properties([
|
||||
buildDiscarder(
|
||||
logRotator(
|
||||
daysToKeepStr: '90'
|
||||
)
|
||||
)
|
||||
])
|
||||
} else {
|
||||
properties([
|
||||
buildDiscarder(
|
||||
logRotator(
|
||||
numToKeepStr: '10'
|
||||
)
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
node {
|
||||
checkout scm
|
||||
docker.image('docker:18.06.0-ce-dind').withRun('--privileged') { d ->
|
||||
docker.image('openjdk:8u181-jdk').inside("--link ${d.id}:docker") {
|
||||
try {
|
||||
stage('Compile') {
|
||||
sh './gradlew --no-daemon --parallel clean compileJava'
|
||||
}
|
||||
stage('compile tests') {
|
||||
sh './gradlew --no-daemon --parallel compileTestJava'
|
||||
}
|
||||
stage('assemble') {
|
||||
sh './gradlew --no-daemon --parallel assemble'
|
||||
}
|
||||
stage('Build') {
|
||||
sh './gradlew --no-daemon --parallel build'
|
||||
}
|
||||
stage('Reference tests') {
|
||||
sh './gradlew --no-daemon --parallel referenceTest'
|
||||
}
|
||||
stage('Integration Tests') {
|
||||
sh './gradlew --no-daemon --parallel integrationTest'
|
||||
}
|
||||
stage('Acceptance Tests') {
|
||||
sh './gradlew --no-daemon --parallel acceptanceTest --tests Web3Sha3AcceptanceTest --tests PantheonClusterAcceptanceTest --tests MiningAcceptanceTest'
|
||||
}
|
||||
stage('Check Licenses') {
|
||||
sh './gradlew --no-daemon --parallel checkLicenses'
|
||||
}
|
||||
stage('Check javadoc') {
|
||||
sh './gradlew --no-daemon --parallel javadoc'
|
||||
}
|
||||
// stage('Smoke test') {
|
||||
// sh 'DOCKER_HOST=$DOCKER_PORT DOCKER_HOSTNAME=docker ./gradlew --no-daemon smokeTest'
|
||||
// }
|
||||
stage('Jacoco root report') {
|
||||
sh './gradlew --no-daemon jacocoRootReport'
|
||||
}
|
||||
} finally {
|
||||
archiveArtifacts '**/build/reports/**'
|
||||
archiveArtifacts '**/build/test-results/**'
|
||||
archiveArtifacts 'build/reports/**'
|
||||
archiveArtifacts 'build/distributions/**'
|
||||
|
||||
junit '**/build/test-results/**/*.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
LICENSE
Executable file
201
LICENSE
Executable file
@@ -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.
|
||||
126
README.md
Executable file
126
README.md
Executable file
@@ -0,0 +1,126 @@
|
||||
# Pantheon Ethereum Client · [](https://circleci.com/gh/ConsenSys/pantheon) [](https://github.com/ConsenSys/pantheon/blob/master/LICENSE)
|
||||
|
||||
## Pantheon Users
|
||||
|
||||
The process for building and running Pantheon as a user is different to when developing. All user documentation is on our Wiki and some processes are different to those described in this Readme.
|
||||
|
||||
### Build, Install, and Run Pantheon
|
||||
|
||||
Building, installing, and running Pantheon is described in the Wiki:
|
||||
* [Build and Install](https://github.com/ConsenSys/pantheon/wiki/Installation)
|
||||
* [Quickstart](https://github.com/ConsenSys/pantheon/wiki/Quickstart)
|
||||
|
||||
### Documentation
|
||||
|
||||
User and reference documentation available on the Wiki includes:
|
||||
* [Command Line Options](https://github.com/ConsenSys/pantheon/wiki/Pantheon-CLI-Syntax)
|
||||
* [https://github.com/ConsenSys/pantheon/wiki/JSON-RPC-API](https://github.com/ConsenSys/pantheon/wiki/JSON-RPC-API)
|
||||
* [Docker Quickstart Tutorial](https://github.com/ConsenSys/pantheon/wiki/Docker-Quickstart)
|
||||
|
||||
## Pantheon Developers
|
||||
|
||||
## Build Instructions
|
||||
|
||||
To build, clone this repo and run with `./gradlew` like so:
|
||||
|
||||
```
|
||||
git clone --recursive https://github.com/ConsenSys/pantheon
|
||||
cd pantheon
|
||||
./gradlew
|
||||
```
|
||||
|
||||
After a successful build, distribution packages will be available in `build/distribution`.
|
||||
|
||||
## Code Style
|
||||
|
||||
We use Google's Java coding conventions for the project. To reformat code, run:
|
||||
|
||||
```
|
||||
./gradlew spotlessApply
|
||||
```
|
||||
|
||||
Code style will be checked automatically during a build.
|
||||
|
||||
## Testing
|
||||
|
||||
All the unit tests are run as part of the build, but can be explicitly triggered with:
|
||||
```
|
||||
./gradlew test
|
||||
```
|
||||
The integration tests can be triggered with:
|
||||
```
|
||||
./gradlew integrationTest
|
||||
```
|
||||
|
||||
The reference tests (described below) can be triggered with:
|
||||
```
|
||||
./gradlew referenceTest
|
||||
```
|
||||
The system tests can be triggered with:
|
||||
```
|
||||
./gradlew smokeTest
|
||||
```
|
||||
|
||||
## Running Pantheon
|
||||
|
||||
You can build and run Pantheon with default options via:
|
||||
|
||||
```
|
||||
./gradlew run
|
||||
```
|
||||
|
||||
By default this stores all persistent data in `build/pantheon`.
|
||||
|
||||
If you want to set custom CLI arguments for the Pantheon execution, you can use the property `pantheon.run.args` like e.g.:
|
||||
|
||||
```sh
|
||||
./gradlew run -Ppantheon.run.args="--discovery=false --home=/tmp/pantheontmp"
|
||||
```
|
||||
|
||||
which will pass `--discovery=false` and `--home=/tmp/pantheontmp` to the invocation.
|
||||
|
||||
### Ethereum reference tests
|
||||
|
||||
On top of the project proper unit tests, specific unit tests are provided to
|
||||
run the Ethereum reference tests available at https://github.com/ethereum/tests
|
||||
and described at http://ethereum-tests.readthedocs.io/en/latest/. Those are run
|
||||
as part of the unit test suite as described above, but for debugging, it is
|
||||
often convenient to run only a subset of those tests, for which a few convenience
|
||||
as provided. For instance, one can run only "Frontier" general state tests with
|
||||
```
|
||||
./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:referenceTest -Dtest.single=GeneralStateTest -Dtest.ethereum.state.eip=Frontier
|
||||
```
|
||||
or only the tests that match a particular pattern with something like:
|
||||
```
|
||||
gradle :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.include='^CALLCODE.*-Frontier'
|
||||
```
|
||||
Please see the comment on the `test` target in the top level `build.gradle`
|
||||
file for more details.
|
||||
|
||||
### Logging
|
||||
|
||||
This project employs the logging utility [Apache Log4j](https://logging.apache.org/log4j/2.x/),
|
||||
accordingly levels of detail can be specified as follows:
|
||||
|
||||
```
|
||||
ALL: All levels including custom levels.
|
||||
DEBUG: Designates fine-grained informational events that are most useful to debug an application.
|
||||
ERROR: Designates error events that might still allow the application to continue running.
|
||||
FATAL: Designates very severe error events that will presumably lead the application to abort.
|
||||
INFO: Designates informational messages that highlight the progress of the application at coarse-grained level.
|
||||
OFF: The highest possible rank and is intended to turn off logging.
|
||||
TRACE: Designates finer-grained informational events than the DEBUG.
|
||||
WARN: Designates potentially harmful situations.
|
||||
```
|
||||
|
||||
One mechanism of globally effecting the log output of a running client is though modification the file
|
||||
`/pantheon/src/main/resources/log4j2.xml`, where it can be specified under the `<Property name="root.log.level">`.
|
||||
As such, corresponding instances of information logs throughout the codebase, e.g. `log.fatal("Fatal Message!");`,
|
||||
will be rendered to the console while the client is in use.
|
||||
|
||||
## Contribution
|
||||
|
||||
Welcome to the Pantheon Ethereum project repo. If you would like to help contribute
|
||||
code to the project, please fork, commit and send us a pull request.
|
||||
|
||||
Please read the [Contribution guidelines](docs/CONTRIBUTORS.md) for this project.
|
||||
33
acceptance-tests/build.gradle
Executable file
33
acceptance-tests/build.gradle
Executable file
@@ -0,0 +1,33 @@
|
||||
dependencies {
|
||||
|
||||
testRuntime 'org.apache.logging.log4j:log4j-core'
|
||||
testRuntime 'org.apache.logging.log4j:log4j-slf4j-impl'
|
||||
|
||||
testImplementation project(':crypto')
|
||||
testImplementation project(':ethereum:client')
|
||||
testImplementation project(':ethereum:eth')
|
||||
testImplementation project(':ethereum:core')
|
||||
testImplementation project(':ethereum:jsonrpc')
|
||||
testImplementation project(':pantheon')
|
||||
testImplementation project(':util')
|
||||
|
||||
testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts')
|
||||
testImplementation 'junit:junit'
|
||||
testImplementation 'org.awaitility:awaitility'
|
||||
testImplementation 'com.squareup.okhttp3:okhttp'
|
||||
testImplementation 'io.vertx:vertx-core'
|
||||
testImplementation 'org.apache.logging.log4j:log4j-api'
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
testImplementation 'com.google.guava:guava'
|
||||
testImplementation 'org.web3j:core'
|
||||
}
|
||||
|
||||
test.enabled = false
|
||||
|
||||
task acceptanceTest(type: Test) {
|
||||
System.setProperty('acctests.runPantheonAsProcess', 'true')
|
||||
mustRunAfter rootProject.subprojects*.test
|
||||
description = 'Runs Pantheon acceptance tests.'
|
||||
group = 'verification'
|
||||
}
|
||||
acceptanceTest.dependsOn(rootProject.installDist)
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.consensys.pantheon.tests.acceptance;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
import static org.web3j.utils.Convert.Unit.ETHER;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Account;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CreateAccountAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode minerNode;
|
||||
private PantheonNode fullNode;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
minerNode = cluster.create(pantheonMinerNode("node1"));
|
||||
fullNode = cluster.create(pantheonNode("node2"));
|
||||
cluster.start(minerNode, fullNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateAnAccount() {
|
||||
final Account account = accounts.createAccount("account1", "20", ETHER, fullNode);
|
||||
accounts.waitForAccountBalance(account, "20", ETHER, minerNode);
|
||||
accounts.waitForAccountBalance(account, "20", ETHER, fullNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.consensys.pantheon.tests.acceptance;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class PantheonClusterAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode minerNode;
|
||||
private PantheonNode fullNode;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
minerNode = cluster.create(pantheonMinerNode("node1"));
|
||||
fullNode = cluster.create(pantheonNode("node2"));
|
||||
cluster.start(minerNode, fullNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConnectToOtherPeer() {
|
||||
jsonRpc.waitForPeersConnected(minerNode, 1);
|
||||
jsonRpc.waitForPeersConnected(fullNode, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package net.consensys.pantheon.tests.acceptance;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonRpcDisabledNode;
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.patheonNodeWithRpcApis;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.exceptions.ClientConnectionException;
|
||||
|
||||
public class RpcApisTogglesAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode rpcEnabledNode;
|
||||
private PantheonNode rpcDisabledNode;
|
||||
private PantheonNode ethApiDisabledNode;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
rpcEnabledNode = cluster.create(pantheonNode("rpc-enabled"));
|
||||
rpcDisabledNode = cluster.create(pantheonRpcDisabledNode("rpc-disabled"));
|
||||
ethApiDisabledNode = cluster.create(patheonNodeWithRpcApis("eth-api-disabled", RpcApis.NET));
|
||||
cluster.start(rpcEnabledNode, rpcDisabledNode, ethApiDisabledNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSucceedConnectingToNodeWithJsonRpcEnabled() {
|
||||
rpcEnabledNode.verifyJsonRpcEnabled();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailConnectingToNodeWithJsonRpcDisabled() {
|
||||
rpcDisabledNode.verifyJsonRpcDisabled();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSucceedConnectingToNodeWithWsRpcEnabled() {
|
||||
rpcEnabledNode.verifyWsRpcEnabled();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailConnectingToNodeWithWsRpcDisabled() {
|
||||
rpcDisabledNode.verifyWsRpcDisabled();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSucceedCallingMethodFromEnabledApiGroup() throws Exception {
|
||||
final Web3j web3j = ethApiDisabledNode.web3j();
|
||||
|
||||
assertThat(web3j.netVersion().send().getError()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailCallingMethodFromDisabledApiGroup() {
|
||||
final Web3j web3j = ethApiDisabledNode.web3j();
|
||||
|
||||
assertThat(catchThrowable(() -> web3j.ethAccounts().send()))
|
||||
.isInstanceOf(ClientConnectionException.class)
|
||||
.hasMessageContaining("Invalid response received: 400");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Accounts;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.Cluster;
|
||||
|
||||
import org.junit.After;
|
||||
|
||||
public class AcceptanceTestBase {
|
||||
|
||||
protected Cluster cluster = new Cluster();
|
||||
protected Accounts accounts = new Accounts();
|
||||
protected JsonRpc jsonRpc = new JsonRpc(cluster);
|
||||
|
||||
@After
|
||||
public void tearDownAcceptanceTestBase() throws Exception {
|
||||
cluster.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.Cluster;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
public class JsonRpc {
|
||||
|
||||
private final Cluster nodes;
|
||||
|
||||
public JsonRpc(final Cluster nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public void waitForPeersConnected(final PantheonNode node, final int expectedNumberOfPeers) {
|
||||
WaitUtils.waitFor(() -> assertThat(node.getPeerCount()).isEqualTo(expectedNumberOfPeers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.awaitility.core.ThrowingRunnable;
|
||||
|
||||
public class WaitUtils {
|
||||
public static void waitFor(final ThrowingRunnable condition) {
|
||||
Awaitility.await().ignoreExceptions().atMost(30, TimeUnit.SECONDS).untilAsserted(condition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.account;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.PrivateKey;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.util.bytes.Bytes32;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.web3j.crypto.Credentials;
|
||||
|
||||
public class Account {
|
||||
|
||||
private final String name;
|
||||
private final KeyPair keyPair;
|
||||
private long nonce = 0;
|
||||
|
||||
private Account(final String name, final KeyPair keyPair) {
|
||||
this.name = name;
|
||||
this.keyPair = keyPair;
|
||||
}
|
||||
|
||||
public static Account create(final String name) {
|
||||
return new Account(name, KeyPair.generate());
|
||||
}
|
||||
|
||||
public static Account fromPrivateKey(final String name, final String privateKey) {
|
||||
return new Account(name, KeyPair.create(PrivateKey.create(Bytes32.fromHexString(privateKey))));
|
||||
}
|
||||
|
||||
public Credentials web3jCredentials() {
|
||||
return Credentials.create(
|
||||
keyPair.getPrivateKey().toString(), keyPair.getPublicKey().toString());
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())).toString();
|
||||
}
|
||||
|
||||
public BigInteger getNextNonce() {
|
||||
return BigInteger.valueOf(nonce++);
|
||||
}
|
||||
|
||||
public void setNextNonce(final long nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.account;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.WaitUtils.waitFor;
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.web3j.utils.Convert.Unit.ETHER;
|
||||
import static org.web3j.utils.Convert.toWei;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.web3j.utils.Convert.Unit;
|
||||
|
||||
public class Accounts {
|
||||
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
private final Account richBenefactorOne;
|
||||
private final Account richBenefactorTwo;
|
||||
|
||||
public Accounts() {
|
||||
richBenefactorOne =
|
||||
Account.fromPrivateKey(
|
||||
"Rich Benefactor One",
|
||||
"8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63");
|
||||
richBenefactorTwo =
|
||||
Account.fromPrivateKey(
|
||||
"Rich Benefactor Two",
|
||||
"c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3");
|
||||
}
|
||||
|
||||
public Account createAccount(
|
||||
final String accountName,
|
||||
final String initialBalance,
|
||||
final Unit initialBalanceUnit,
|
||||
final PantheonNode createOnNode) {
|
||||
final Account account = Account.create(accountName);
|
||||
createOnNode.transferFunds(richBenefactorOne, account, initialBalance, initialBalanceUnit);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public Account createAccount(final String accountName) {
|
||||
return Account.create(accountName);
|
||||
}
|
||||
|
||||
public void waitForAccountBalance(
|
||||
final Account account,
|
||||
final String expectedBalance,
|
||||
final Unit balanceUnit,
|
||||
final PantheonNode node) {
|
||||
LOG.info(
|
||||
"Waiting for {} to have a balance of {} {} on node {}",
|
||||
account.getName(),
|
||||
expectedBalance,
|
||||
balanceUnit,
|
||||
node.getName());
|
||||
|
||||
waitFor(
|
||||
() ->
|
||||
assertThat(node.getAccountBalance(account))
|
||||
.isEqualTo(toWei(expectedBalance, balanceUnit).toBigIntegerExact()));
|
||||
}
|
||||
|
||||
public void waitForAccountBalance(
|
||||
final Account account, final int etherAmount, final PantheonNode node) {
|
||||
waitForAccountBalance(account, String.valueOf(etherAmount), ETHER, node);
|
||||
}
|
||||
|
||||
public Hash transfer(final Account recipient, final int amount, final PantheonNode node) {
|
||||
return transfer(richBenefactorOne, recipient, amount, node);
|
||||
}
|
||||
|
||||
public Hash transfer(
|
||||
final Account sender, final Account recipient, final int amount, final PantheonNode node) {
|
||||
return node.transferFunds(sender, recipient, String.valueOf(amount), Unit.ETHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer funds in separate transactions (1 eth increments). This is a strategy to increase the
|
||||
* total of transactions.
|
||||
*
|
||||
* @param fromAccount account sending the ether value
|
||||
* @param toAccount account receiving the ether value
|
||||
* @param etherAmount amount of ether to transfer
|
||||
* @return a list with the hashes of each transaction
|
||||
*/
|
||||
public List<Hash> incrementalTransfer(
|
||||
final Account fromAccount,
|
||||
final Account toAccount,
|
||||
final int etherAmount,
|
||||
final PantheonNode node) {
|
||||
final List<Hash> txHashes = new ArrayList<>();
|
||||
for (int i = 1; i <= etherAmount; i++) {
|
||||
final Hash hash = node.transferFunds(fromAccount, toAccount, String.valueOf(1), Unit.ETHER);
|
||||
txHashes.add(hash);
|
||||
}
|
||||
return txHashes;
|
||||
}
|
||||
|
||||
public Account getSecondaryBenefactor() {
|
||||
return richBenefactorTwo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.WaitUtils.waitFor;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.web3j.utils.Convert.toWei;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.WaitUtils;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Account;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.web3j.utils.Convert.Unit;
|
||||
|
||||
public class Cluster implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(Cluster.class);
|
||||
|
||||
private final Map<String, PantheonNode> nodes = new HashMap<>();
|
||||
private final PantheonNodeRunner pantheonNodeRunner = PantheonNodeRunner.instance();
|
||||
|
||||
public void start(final PantheonNode... nodes) {
|
||||
this.nodes.clear();
|
||||
|
||||
final List<String> bootNodes = new ArrayList<>();
|
||||
|
||||
for (final PantheonNode node : nodes) {
|
||||
this.nodes.put(node.getName(), node);
|
||||
bootNodes.add(node.enodeUrl());
|
||||
}
|
||||
|
||||
for (final PantheonNode node : nodes) {
|
||||
node.bootnodes(bootNodes);
|
||||
node.start(pantheonNodeRunner);
|
||||
}
|
||||
|
||||
for (final PantheonNode node : nodes) {
|
||||
awaitPeerDiscovery(node, nodes.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for (final PantheonNode node : nodes.values()) {
|
||||
node.stop();
|
||||
}
|
||||
pantheonNodeRunner.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (final PantheonNode node : nodes.values()) {
|
||||
node.close();
|
||||
}
|
||||
pantheonNodeRunner.shutdown();
|
||||
}
|
||||
|
||||
public PantheonNode create(final PantheonNodeConfig config) throws IOException {
|
||||
config.initSocket();
|
||||
final PantheonNode node =
|
||||
new PantheonNode(
|
||||
config.getName(),
|
||||
config.getSocketPort(),
|
||||
config.getMiningParameters(),
|
||||
config.getJsonRpcConfiguration(),
|
||||
config.getWebSocketConfiguration());
|
||||
config.closeSocket();
|
||||
return node;
|
||||
}
|
||||
|
||||
private void awaitPeerDiscovery(final PantheonNode node, final int nodeCount) {
|
||||
if (node.jsonRpcEnabled()) {
|
||||
WaitUtils.waitFor(() -> assertThat(node.getPeerCount()).isEqualTo(nodeCount - 1));
|
||||
}
|
||||
}
|
||||
|
||||
public void awaitPropagation(final Account account, final int expectedBalance) {
|
||||
awaitPropagation(account, String.valueOf(expectedBalance), Unit.ETHER);
|
||||
}
|
||||
|
||||
public void awaitPropagation(
|
||||
final Account account, final String expectedBalance, final Unit balanceUnit) {
|
||||
|
||||
for (final PantheonNode node : nodes.values()) {
|
||||
LOG.info(
|
||||
"Waiting for {} to have a balance of {} {} on node {}",
|
||||
account.getName(),
|
||||
expectedBalance,
|
||||
balanceUnit,
|
||||
node.getName());
|
||||
|
||||
waitFor(
|
||||
() ->
|
||||
assertThat(node.getAccountBalance(account))
|
||||
.isEqualTo(toWei(expectedBalance, balanceUnit).toBigIntegerExact()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.core.methods.response.EthBlockNumber;
|
||||
|
||||
public class Eth {
|
||||
|
||||
private final Web3j web3j;
|
||||
|
||||
public Eth(final Web3j web3) {
|
||||
this.web3j = web3;
|
||||
}
|
||||
|
||||
public BigInteger blockNumber() throws IOException {
|
||||
final EthBlockNumber result = web3j.ethBlockNumber().send();
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.hasError()).isFalse();
|
||||
return result.getBlockNumber();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
import static org.web3j.protocol.core.DefaultBlockParameterName.LATEST;
|
||||
import static org.web3j.utils.Numeric.toHexString;
|
||||
|
||||
import net.consensys.pantheon.controller.KeyPairUtil;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.blockcreation.MiningParameters;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Account;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.ConnectException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.io.MoreFiles;
|
||||
import com.google.common.io.RecursiveDeleteOption;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.java_websocket.exceptions.WebsocketNotConnectedException;
|
||||
import org.web3j.crypto.RawTransaction;
|
||||
import org.web3j.crypto.TransactionEncoder;
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.Web3jService;
|
||||
import org.web3j.protocol.core.methods.response.EthGetBalance;
|
||||
import org.web3j.protocol.http.HttpService;
|
||||
import org.web3j.protocol.websocket.WebSocketService;
|
||||
import org.web3j.utils.Async;
|
||||
import org.web3j.utils.Convert;
|
||||
import org.web3j.utils.Convert.Unit;
|
||||
|
||||
public class PantheonNode implements AutoCloseable {
|
||||
|
||||
private static final String LOCALHOST = "127.0.0.1";
|
||||
private static final Logger LOG = getLogger();
|
||||
private static final BigInteger MINIMUM_GAS_PRICE = BigInteger.valueOf(1000);
|
||||
private static final BigInteger TRANSFER_GAS_COST = BigInteger.valueOf(21000);
|
||||
|
||||
private final String name;
|
||||
private final Path homeDirectory;
|
||||
private final KeyPair keyPair;
|
||||
private final int p2pPort;
|
||||
private final MiningParameters miningParameters;
|
||||
private final JsonRpcConfiguration jsonRpcConfiguration;
|
||||
private final WebSocketConfiguration webSocketConfiguration;
|
||||
private final boolean jsonRpcEnabled;
|
||||
private final boolean wsRpcEnabled;
|
||||
private final Properties portsProperties = new Properties();
|
||||
|
||||
private List<String> bootnodes = new ArrayList<>();
|
||||
private Eth eth;
|
||||
private Web3 web3;
|
||||
private Web3j web3j;
|
||||
|
||||
public PantheonNode(
|
||||
final String name,
|
||||
final int p2pPort,
|
||||
final MiningParameters miningParameters,
|
||||
final JsonRpcConfiguration jsonRpcConfiguration,
|
||||
final WebSocketConfiguration webSocketConfiguration)
|
||||
throws IOException {
|
||||
this.name = name;
|
||||
this.homeDirectory = Files.createTempDirectory("acctest");
|
||||
this.keyPair = KeyPairUtil.loadKeyPair(homeDirectory);
|
||||
this.p2pPort = p2pPort;
|
||||
this.miningParameters = miningParameters;
|
||||
this.jsonRpcConfiguration = jsonRpcConfiguration;
|
||||
this.webSocketConfiguration = webSocketConfiguration;
|
||||
this.jsonRpcEnabled = jsonRpcConfiguration.isEnabled();
|
||||
this.wsRpcEnabled = webSocketConfiguration.isEnabled();
|
||||
LOG.info("Created PantheonNode {}", this.toString());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
String enodeUrl() {
|
||||
return "enode://" + keyPair.getPublicKey().toString() + "@" + LOCALHOST + ":" + p2pPort;
|
||||
}
|
||||
|
||||
private Optional<String> jsonRpcBaseUrl() {
|
||||
if (jsonRpcEnabled) {
|
||||
return Optional.of(
|
||||
"http://"
|
||||
+ jsonRpcConfiguration.getHost()
|
||||
+ ":"
|
||||
+ portsProperties.getProperty("json-rpc"));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> wsRpcBaseUrl() {
|
||||
if (wsRpcEnabled) {
|
||||
return Optional.of(
|
||||
"ws://" + webSocketConfiguration.getHost() + ":" + portsProperties.getProperty("ws-rpc"));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Integer> jsonRpcWebSocketPort() {
|
||||
if (wsRpcEnabled) {
|
||||
return Optional.of(Integer.valueOf(portsProperties.getProperty("ws-rpc")));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return LOCALHOST;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Web3j web3j() {
|
||||
if (!jsonRpcBaseUrl().isPresent()) {
|
||||
throw new IllegalStateException(
|
||||
"Can't create a web3j instance for a node with RPC disabled.");
|
||||
}
|
||||
|
||||
if (web3j == null) {
|
||||
return web3j(new HttpService(jsonRpcBaseUrl().get()));
|
||||
}
|
||||
|
||||
return web3j;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Web3j web3j(final Web3jService web3jService) {
|
||||
if (web3j == null) {
|
||||
web3j = Web3j.build(web3jService, 2000, Async.defaultExecutorService());
|
||||
}
|
||||
|
||||
return web3j;
|
||||
}
|
||||
|
||||
public Hash transferFunds(
|
||||
final Account from, final Account to, final String amount, final Unit unit) {
|
||||
final RawTransaction transaction =
|
||||
RawTransaction.createEtherTransaction(
|
||||
from.getNextNonce(),
|
||||
MINIMUM_GAS_PRICE,
|
||||
TRANSFER_GAS_COST,
|
||||
to.getAddress(),
|
||||
Convert.toWei(amount, unit).toBigIntegerExact());
|
||||
final String signedTransactionData =
|
||||
toHexString(TransactionEncoder.signMessage(transaction, from.web3jCredentials()));
|
||||
try {
|
||||
return Hash.fromHexString(
|
||||
web3j().ethSendRawTransaction(signedTransactionData).send().getTransactionHash());
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public BigInteger getAccountBalance(final Account account) {
|
||||
try {
|
||||
final EthGetBalance balanceResponse =
|
||||
web3j().ethGetBalance(account.getAddress(), LATEST).send();
|
||||
return balanceResponse.getBalance();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getPeerCount() {
|
||||
try {
|
||||
return web3j().netPeerCount().send().getQuantity().intValueExact();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void start(final PantheonNodeRunner runner) {
|
||||
runner.startNode(this);
|
||||
loadPortsFile();
|
||||
}
|
||||
|
||||
private void loadPortsFile() {
|
||||
try (FileInputStream fis =
|
||||
new FileInputStream(new File(homeDirectory.toFile(), "pantheon.ports"))) {
|
||||
portsProperties.load(fis);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("Error reading Pantheon ports file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyJsonRpcEnabled() {
|
||||
if (!jsonRpcBaseUrl().isPresent()) {
|
||||
throw new RuntimeException("JSON-RPC is not enabled in node configuration");
|
||||
}
|
||||
|
||||
try {
|
||||
assertThat(web3j().netVersion().send().getError()).isNull();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyJsonRpcDisabled() {
|
||||
if (jsonRpcBaseUrl().isPresent()) {
|
||||
throw new RuntimeException("JSON-RPC is enabled in node configuration");
|
||||
}
|
||||
|
||||
final HttpService web3jService = new HttpService("http://" + LOCALHOST + ":8545");
|
||||
assertThat(catchThrowable(() -> web3j(web3jService).netVersion().send()))
|
||||
.isInstanceOf(ConnectException.class)
|
||||
.hasMessage("Failed to connect to /127.0.0.1:8545");
|
||||
}
|
||||
|
||||
public void verifyWsRpcEnabled() {
|
||||
if (!wsRpcBaseUrl().isPresent()) {
|
||||
throw new RuntimeException("WS-RPC is not enabled in node configuration");
|
||||
}
|
||||
|
||||
try {
|
||||
final WebSocketService webSocketService = new WebSocketService(wsRpcBaseUrl().get(), true);
|
||||
assertThat(web3j(webSocketService).netVersion().send().getError()).isNull();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyWsRpcDisabled() {
|
||||
if (wsRpcBaseUrl().isPresent()) {
|
||||
throw new RuntimeException("WS-RPC is enabled in node configuration");
|
||||
}
|
||||
|
||||
final WebSocketService webSocketService =
|
||||
new WebSocketService("ws://" + LOCALHOST + ":8546", true);
|
||||
assertThat(catchThrowable(() -> web3j(webSocketService).netVersion().send()))
|
||||
.isInstanceOf(WebsocketNotConnectedException.class);
|
||||
}
|
||||
|
||||
Path homeDirectory() {
|
||||
return homeDirectory;
|
||||
}
|
||||
|
||||
boolean jsonRpcEnabled() {
|
||||
return jsonRpcEnabled;
|
||||
}
|
||||
|
||||
JsonRpcConfiguration jsonRpcConfiguration() {
|
||||
return jsonRpcConfiguration;
|
||||
}
|
||||
|
||||
Optional<String> jsonRpcListenAddress() {
|
||||
if (jsonRpcEnabled) {
|
||||
return Optional.of(jsonRpcConfiguration.getHost() + ":" + jsonRpcConfiguration.getPort());
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
boolean wsRpcEnabled() {
|
||||
return wsRpcEnabled;
|
||||
}
|
||||
|
||||
WebSocketConfiguration webSocketConfiguration() {
|
||||
return webSocketConfiguration;
|
||||
}
|
||||
|
||||
Optional<String> wsRpcListenAddress() {
|
||||
return Optional.of(webSocketConfiguration.getHost() + ":" + webSocketConfiguration.getPort());
|
||||
}
|
||||
|
||||
int p2pPort() {
|
||||
return p2pPort;
|
||||
}
|
||||
|
||||
String p2pListenAddress() {
|
||||
return LOCALHOST + ":" + p2pPort;
|
||||
}
|
||||
|
||||
List<String> bootnodes() {
|
||||
return bootnodes
|
||||
.stream()
|
||||
.filter(node -> !node.equals(this.enodeUrl()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void bootnodes(final List<String> bootnodes) {
|
||||
this.bootnodes = bootnodes;
|
||||
}
|
||||
|
||||
public MiningParameters getMiningParameters() {
|
||||
return miningParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("p2pPort", p2pPort)
|
||||
.add("homeDirectory", homeDirectory)
|
||||
.add("keyPair", keyPair)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (web3j != null) {
|
||||
web3j.shutdown();
|
||||
web3j = null;
|
||||
}
|
||||
|
||||
eth = null;
|
||||
web3 = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
stop();
|
||||
try {
|
||||
MoreFiles.deleteRecursively(homeDirectory, RecursiveDeleteOption.ALLOW_INSECURE);
|
||||
} catch (final IOException e) {
|
||||
LOG.info("Failed to clean up temporary file: {}", homeDirectory, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Web3 web3() {
|
||||
if (web3 == null) {
|
||||
web3 = new Web3(web3j());
|
||||
}
|
||||
|
||||
return web3;
|
||||
}
|
||||
|
||||
public Eth eth() {
|
||||
if (eth == null) {
|
||||
eth = new Eth(web3j());
|
||||
}
|
||||
|
||||
return eth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import net.consensys.pantheon.ethereum.blockcreation.MiningParameters;
|
||||
import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PantheonNodeConfig {
|
||||
|
||||
private final String name;
|
||||
private final MiningParameters miningParameters;
|
||||
private final JsonRpcConfiguration jsonRpcConfiguration;
|
||||
private final WebSocketConfiguration webSocketConfiguration;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
private PantheonNodeConfig(
|
||||
final String name,
|
||||
final MiningParameters miningParameters,
|
||||
final JsonRpcConfiguration jsonRpcConfiguration,
|
||||
final WebSocketConfiguration webSocketConfiguration) {
|
||||
this.name = name;
|
||||
this.miningParameters = miningParameters;
|
||||
this.jsonRpcConfiguration = jsonRpcConfiguration;
|
||||
this.webSocketConfiguration = webSocketConfiguration;
|
||||
}
|
||||
|
||||
private PantheonNodeConfig(final String name, final MiningParameters miningParameters) {
|
||||
this.name = name;
|
||||
this.miningParameters = miningParameters;
|
||||
this.jsonRpcConfiguration = createJsonRpcConfig();
|
||||
this.webSocketConfiguration = createWebSocketConfig();
|
||||
}
|
||||
|
||||
private static MiningParameters createMiningParameters(final boolean miner) {
|
||||
return new MiningParametersTestBuilder().enabled(miner).build();
|
||||
}
|
||||
|
||||
public static PantheonNodeConfig pantheonMinerNode(final String name) {
|
||||
return new PantheonNodeConfig(name, createMiningParameters(true));
|
||||
}
|
||||
|
||||
public static PantheonNodeConfig pantheonNode(final String name) {
|
||||
return new PantheonNodeConfig(name, createMiningParameters(false));
|
||||
}
|
||||
|
||||
public static PantheonNodeConfig pantheonRpcDisabledNode(final String name) {
|
||||
return new PantheonNodeConfig(
|
||||
name,
|
||||
createMiningParameters(false),
|
||||
JsonRpcConfiguration.createDefault(),
|
||||
WebSocketConfiguration.createDefault());
|
||||
}
|
||||
|
||||
public static PantheonNodeConfig patheonNodeWithRpcApis(
|
||||
final String name, final RpcApis... enabledRpcApis) {
|
||||
final JsonRpcConfiguration jsonRpcConfig = createJsonRpcConfig();
|
||||
jsonRpcConfig.setRpcApis(Arrays.asList(enabledRpcApis));
|
||||
final WebSocketConfiguration webSocketConfig = createWebSocketConfig();
|
||||
webSocketConfig.setRpcApis(Arrays.asList(enabledRpcApis));
|
||||
|
||||
return new PantheonNodeConfig(
|
||||
name, createMiningParameters(false), jsonRpcConfig, webSocketConfig);
|
||||
}
|
||||
|
||||
private static JsonRpcConfiguration createJsonRpcConfig() {
|
||||
final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault();
|
||||
config.setEnabled(true);
|
||||
config.setPort(0);
|
||||
return config;
|
||||
}
|
||||
|
||||
private static WebSocketConfiguration createWebSocketConfig() {
|
||||
final WebSocketConfiguration config = WebSocketConfiguration.createDefault();
|
||||
config.setEnabled(true);
|
||||
config.setPort(0);
|
||||
return config;
|
||||
}
|
||||
|
||||
public void initSocket() throws IOException {
|
||||
serverSocket = new ServerSocket(0);
|
||||
}
|
||||
|
||||
public void closeSocket() throws IOException {
|
||||
serverSocket.close();
|
||||
}
|
||||
|
||||
public int getSocketPort() {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public MiningParameters getMiningParameters() {
|
||||
return miningParameters;
|
||||
}
|
||||
|
||||
public JsonRpcConfiguration getJsonRpcConfiguration() {
|
||||
return jsonRpcConfiguration;
|
||||
}
|
||||
|
||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||
return webSocketConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
|
||||
public interface PantheonNodeRunner {
|
||||
|
||||
static PantheonNodeRunner instance() {
|
||||
if (Boolean.getBoolean("acctests.runPantheonAsProcess")) {
|
||||
return new ProcessPantheonNodeRunner();
|
||||
} else {
|
||||
return new ThreadPantheonNodeRunner();
|
||||
}
|
||||
}
|
||||
|
||||
void startNode(PantheonNode node);
|
||||
|
||||
void stopNode(PantheonNode node);
|
||||
|
||||
void shutdown();
|
||||
|
||||
default void waitForPortsFile(final Path dataDir) {
|
||||
final File file = new File(dataDir.toFile(), "pantheon.ports");
|
||||
Awaitility.waitAtMost(30, TimeUnit.SECONDS)
|
||||
.until(
|
||||
() -> {
|
||||
if (file.exists()) {
|
||||
try (Stream<String> s = Files.lines(file.toPath())) {
|
||||
return s.count() > 0;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.awaitility.Awaitility;
|
||||
|
||||
public class ProcessPantheonNodeRunner implements PantheonNodeRunner {
|
||||
|
||||
private final Logger LOG = LogManager.getLogger();
|
||||
|
||||
private final Map<String, Process> pantheonProcesses = new HashMap<>();
|
||||
|
||||
ProcessPantheonNodeRunner() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startNode(final PantheonNode node) {
|
||||
final Path dataDir = node.homeDirectory();
|
||||
|
||||
final List<String> params = new ArrayList<>();
|
||||
params.add("build/install/pantheon/bin/pantheon");
|
||||
params.add("--datadir");
|
||||
params.add(dataDir.toAbsolutePath().toString());
|
||||
|
||||
params.add("--dev-mode");
|
||||
|
||||
params.add("--p2p-listen");
|
||||
params.add(node.p2pListenAddress());
|
||||
|
||||
if (node.getMiningParameters().isMiningEnabled()) {
|
||||
params.add("--miner-enabled");
|
||||
params.add("--miner-coinbase");
|
||||
params.add(node.getMiningParameters().getCoinbase().get().toString());
|
||||
}
|
||||
|
||||
params.add("--bootnodes");
|
||||
params.add(String.join(",", node.bootnodes()));
|
||||
|
||||
if (node.jsonRpcEnabled()) {
|
||||
params.add("--rpc-enabled");
|
||||
params.add("--rpc-listen");
|
||||
params.add(node.jsonRpcListenAddress().get());
|
||||
params.add("--rpc-api");
|
||||
params.add(apiList(node.jsonRpcConfiguration().getRpcApis()));
|
||||
}
|
||||
|
||||
if (node.wsRpcEnabled()) {
|
||||
params.add("--ws-enabled");
|
||||
params.add("--ws-listen");
|
||||
params.add(node.wsRpcListenAddress().get());
|
||||
params.add("--ws-api");
|
||||
params.add(apiList(node.webSocketConfiguration().getRpcApis()));
|
||||
}
|
||||
|
||||
final ProcessBuilder processBuilder =
|
||||
new ProcessBuilder(params)
|
||||
.directory(new File(System.getProperty("user.dir")).getParentFile())
|
||||
.inheritIO();
|
||||
|
||||
try {
|
||||
final Process process = processBuilder.start();
|
||||
pantheonProcesses.put(node.getName(), process);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Error starting PantheonNode process", e);
|
||||
}
|
||||
|
||||
waitForPortsFile(dataDir);
|
||||
}
|
||||
|
||||
private String apiList(final Collection<RpcApis> rpcApis) {
|
||||
return String.join(",", rpcApis.stream().map(RpcApis::getValue).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopNode(final PantheonNode node) {
|
||||
node.stop();
|
||||
if (pantheonProcesses.containsKey(node.getName())) {
|
||||
final Process process = pantheonProcesses.get(node.getName());
|
||||
killPantheonProcess(node.getName(), process);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void shutdown() {
|
||||
final HashMap<String, Process> localMap = new HashMap<>(pantheonProcesses);
|
||||
localMap.forEach(this::killPantheonProcess);
|
||||
}
|
||||
|
||||
private void killPantheonProcess(final String name, final Process process) {
|
||||
LOG.info("Killing " + name + " process");
|
||||
|
||||
Awaitility.waitAtMost(30, TimeUnit.SECONDS)
|
||||
.until(
|
||||
() -> {
|
||||
if (process.isAlive()) {
|
||||
process.destroy();
|
||||
return false;
|
||||
} else {
|
||||
pantheonProcesses.remove(name);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import net.consensys.pantheon.Runner;
|
||||
import net.consensys.pantheon.RunnerBuilder;
|
||||
import net.consensys.pantheon.cli.PantheonControllerBuilder;
|
||||
import net.consensys.pantheon.controller.PantheonController;
|
||||
import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration.Builder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class ThreadPantheonNodeRunner implements PantheonNodeRunner {
|
||||
|
||||
private static final int NETWORK_ID = 10;
|
||||
|
||||
private final Logger LOG = LogManager.getLogger();
|
||||
private final Map<String, Runner> pantheonRunners = new HashMap<>();
|
||||
private ExecutorService nodeExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public void startNode(final PantheonNode node) {
|
||||
if (nodeExecutor == null || nodeExecutor.isShutdown()) {
|
||||
nodeExecutor = Executors.newCachedThreadPool();
|
||||
}
|
||||
|
||||
final PantheonControllerBuilder builder = new PantheonControllerBuilder();
|
||||
PantheonController<?> pantheonController;
|
||||
try {
|
||||
pantheonController =
|
||||
builder.build(
|
||||
new Builder().build(),
|
||||
null,
|
||||
node.homeDirectory(),
|
||||
false,
|
||||
node.getMiningParameters(),
|
||||
true,
|
||||
NETWORK_ID);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("Error building PantheonController", e);
|
||||
}
|
||||
|
||||
final Runner runner =
|
||||
new RunnerBuilder()
|
||||
.build(
|
||||
Vertx.vertx(),
|
||||
pantheonController,
|
||||
true,
|
||||
node.bootnodes(),
|
||||
node.getHost(),
|
||||
node.p2pPort(),
|
||||
25,
|
||||
node.jsonRpcConfiguration(),
|
||||
node.webSocketConfiguration(),
|
||||
node.homeDirectory());
|
||||
|
||||
nodeExecutor.submit(runner::execute);
|
||||
|
||||
waitForPortsFile(node.homeDirectory().toAbsolutePath());
|
||||
|
||||
pantheonRunners.put(node.getName(), runner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopNode(final PantheonNode node) {
|
||||
node.stop();
|
||||
killRunner(node.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
pantheonRunners.keySet().forEach(this::killRunner);
|
||||
try {
|
||||
nodeExecutor.shutdownNow();
|
||||
if (!nodeExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Failed to shut down node executor");
|
||||
}
|
||||
} catch (final InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void killRunner(final String name) {
|
||||
LOG.info("Killing " + name + " runner");
|
||||
|
||||
if (pantheonRunners.containsKey(name)) {
|
||||
try {
|
||||
pantheonRunners.get(name).close();
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Error shutting down node " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.node;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.core.methods.response.Web3Sha3;
|
||||
|
||||
public class Web3 {
|
||||
|
||||
private final Web3j web3j;
|
||||
|
||||
public Web3(final Web3j web3) {
|
||||
this.web3j = web3;
|
||||
}
|
||||
|
||||
public String web3Sha3(final String input) throws IOException {
|
||||
final Web3Sha3 result = web3j.web3Sha3(input).send();
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.hasError()).isFalse();
|
||||
return result.getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class JsonRpcSuccessEvent {
|
||||
|
||||
private final String version;
|
||||
private final long id;
|
||||
private final String result;
|
||||
|
||||
@JsonCreator
|
||||
public JsonRpcSuccessEvent(
|
||||
@JsonProperty("jsonrpc") final String version,
|
||||
@JsonProperty("id") final long id,
|
||||
@JsonProperty("result") final String result) {
|
||||
this.id = id;
|
||||
this.result = result;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Subscription {
|
||||
|
||||
private static final String SIXTY_FOUR_HEX_PATTERN = "0x[0-9a-f]{64}";
|
||||
private static final String HEX_PATTERN = "0x[0-9a-f]+";
|
||||
|
||||
private final WebSocketConnection connection;
|
||||
private final String value;
|
||||
|
||||
public Subscription(final WebSocketConnection connection, final String value) {
|
||||
assertThat(value).matches(HEX_PATTERN);
|
||||
assertThat(connection).isNotNull();
|
||||
this.value = value;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void verifyEventReceived(final Hash expectedTransaction) {
|
||||
verifyEventReceived(expectedTransaction, 1);
|
||||
}
|
||||
|
||||
public void verifyEventReceived(final Hash expectedTransaction, final int expectedOccurrences) {
|
||||
final List<SubscriptionEvent> events = connection.getSubscriptionEvents();
|
||||
assertThat(events).isNotNull();
|
||||
int occurrences = 0;
|
||||
|
||||
for (final SubscriptionEvent event : events) {
|
||||
if (matches(expectedTransaction, event)) {
|
||||
occurrences++;
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(occurrences)
|
||||
.as("Expecting: %s occurrences, but found: %s", expectedOccurrences, occurrences)
|
||||
.isEqualTo(expectedOccurrences);
|
||||
}
|
||||
|
||||
private boolean matches(final Hash expectedTransaction, final SubscriptionEvent event) {
|
||||
return isEthSubscription(event)
|
||||
&& isExpectedSubscription(event)
|
||||
&& isExpectedTransaction(expectedTransaction, event);
|
||||
}
|
||||
|
||||
private boolean isEthSubscription(final SubscriptionEvent event) {
|
||||
return "2.0".equals(event.getVersion())
|
||||
&& "eth_subscription".equals(event.getMethod())
|
||||
&& event.getParams() != null;
|
||||
}
|
||||
|
||||
private boolean isExpectedSubscription(final SubscriptionEvent event) {
|
||||
final Map<String, String> params = event.getParams();
|
||||
return params.size() == 2
|
||||
&& params.containsKey("subscription")
|
||||
&& value.equals(params.get("subscription"));
|
||||
}
|
||||
|
||||
private boolean isExpectedTransaction(
|
||||
final Hash expectedTransaction, final SubscriptionEvent event) {
|
||||
final Map<String, String> params = event.getParams();
|
||||
final String result = params.get("result");
|
||||
return params.containsKey("result")
|
||||
&& expectedTransaction.toString().equals(result)
|
||||
&& result.matches(SIXTY_FOUR_HEX_PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class SubscriptionEvent {
|
||||
|
||||
private final String version;
|
||||
private final String method;
|
||||
private final Map<String, String> params;
|
||||
|
||||
@JsonCreator
|
||||
public SubscriptionEvent(
|
||||
@JsonProperty("jsonrpc") final String version,
|
||||
@JsonProperty("method") final String method,
|
||||
@JsonProperty("params") final Map<String, String> params) {
|
||||
this.version = version;
|
||||
this.method = method;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Map<String, String> getParams() {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
|
||||
public class WebSocket {
|
||||
|
||||
private static final String HEX_PATTERN = "0x[0-9a-f]+";
|
||||
|
||||
private final WebSocketConnection connection;
|
||||
|
||||
public WebSocket(final Vertx vertx, final PantheonNode node) {
|
||||
this.connection = new WebSocketConnection(vertx, node);
|
||||
}
|
||||
|
||||
public Subscription subscribe() {
|
||||
final JsonRpcSuccessEvent subscribe = connection.subscribe("newPendingTransactions");
|
||||
|
||||
assertThat(subscribe).isNotNull();
|
||||
assertThat(subscribe.getVersion()).isEqualTo("2.0");
|
||||
assertThat(subscribe.getId()).isGreaterThan(0);
|
||||
assertThat(subscribe.getResult()).matches(HEX_PATTERN);
|
||||
|
||||
return new Subscription(connection, subscribe.getResult());
|
||||
}
|
||||
|
||||
public void unsubscribe(final Subscription subscription) {
|
||||
final JsonRpcSuccessEvent unsubscribe = connection.unsubscribe(subscription);
|
||||
|
||||
assertThat(unsubscribe).isNotNull();
|
||||
assertThat(unsubscribe.getVersion()).isEqualTo("2.0");
|
||||
assertThat(unsubscribe.getId()).isGreaterThan(0);
|
||||
assertThat(unsubscribe.getResult()).isEqualTo("true");
|
||||
}
|
||||
|
||||
public void verifyTotalEventsReceived(final int expectedTotalEventCount) {
|
||||
final List<SubscriptionEvent> events = connection.getSubscriptionEvents();
|
||||
assertThat(events).isNotNull();
|
||||
assertThat(events.size()).isEqualTo(expectedTotalEventCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.WaitUtils;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.HttpClientOptions;
|
||||
import io.vertx.core.http.RequestOptions;
|
||||
import io.vertx.core.http.WebSocket;
|
||||
import io.vertx.core.json.DecodeException;
|
||||
import io.vertx.core.json.Json;
|
||||
|
||||
public class WebSocketConnection {
|
||||
|
||||
private final RequestOptions options;
|
||||
private final ConcurrentLinkedDeque<SubscriptionEvent> subscriptionEvents;
|
||||
|
||||
private volatile String error;
|
||||
private volatile boolean receivedResponse;
|
||||
private volatile JsonRpcSuccessEvent latestEvent;
|
||||
private volatile WebSocket connection;
|
||||
|
||||
public WebSocketConnection(final Vertx vertx, final PantheonNode node) {
|
||||
if (!node.jsonRpcWebSocketPort().isPresent()) {
|
||||
throw new IllegalStateException(
|
||||
"Can't start websocket connection for node with RPC disabled");
|
||||
}
|
||||
subscriptionEvents = new ConcurrentLinkedDeque<>();
|
||||
options = new RequestOptions();
|
||||
options.setPort(node.jsonRpcWebSocketPort().get());
|
||||
options.setHost(node.getHost());
|
||||
|
||||
connect(vertx);
|
||||
}
|
||||
|
||||
public JsonRpcSuccessEvent subscribe(final String params) {
|
||||
resetLatestResult();
|
||||
return send(
|
||||
String.format("{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"%s\"]}", params));
|
||||
}
|
||||
|
||||
public JsonRpcSuccessEvent unsubscribe(final Subscription subscription) {
|
||||
resetLatestResult();
|
||||
return send(
|
||||
String.format(
|
||||
"{\"id\": 2, \"method\": \"eth_unsubscribe\", \"params\": [\"%s\"]}", subscription));
|
||||
}
|
||||
|
||||
private JsonRpcSuccessEvent send(final String json) {
|
||||
|
||||
connection.writeBinaryMessage(Buffer.buffer(json));
|
||||
|
||||
WaitUtils.waitFor(() -> assertThat(receivedResponse).isEqualTo(true));
|
||||
|
||||
assertThat(latestEvent)
|
||||
.as(
|
||||
"Expecting a JSON-RPC success response to message: %s, instead received: %s",
|
||||
json, error)
|
||||
.isNotNull();
|
||||
|
||||
return latestEvent;
|
||||
}
|
||||
|
||||
private void connect(final Vertx vertx) {
|
||||
vertx
|
||||
.createHttpClient(new HttpClientOptions())
|
||||
.websocket(
|
||||
options,
|
||||
websocket -> {
|
||||
webSocketConnection(websocket);
|
||||
|
||||
websocket.handler(
|
||||
data -> {
|
||||
try {
|
||||
final WebSocketEvent eventType = Json.decodeValue(data, WebSocketEvent.class);
|
||||
|
||||
if (eventType.isSubscription()) {
|
||||
success(Json.decodeValue(data, SubscriptionEvent.class));
|
||||
} else {
|
||||
success(Json.decodeValue(data, JsonRpcSuccessEvent.class));
|
||||
}
|
||||
|
||||
} catch (final DecodeException e) {
|
||||
error(data.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
WaitUtils.waitFor(() -> assertThat(connection).isNotNull());
|
||||
}
|
||||
|
||||
private void webSocketConnection(final WebSocket connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private void resetLatestResult() {
|
||||
this.receivedResponse = false;
|
||||
this.error = null;
|
||||
this.latestEvent = null;
|
||||
}
|
||||
|
||||
private void error(final String response) {
|
||||
this.receivedResponse = true;
|
||||
this.error = response;
|
||||
}
|
||||
|
||||
private void success(final JsonRpcSuccessEvent result) {
|
||||
this.receivedResponse = true;
|
||||
this.latestEvent = result;
|
||||
}
|
||||
|
||||
private void success(final SubscriptionEvent result) {
|
||||
this.receivedResponse = true;
|
||||
this.subscriptionEvents.add(result);
|
||||
}
|
||||
|
||||
public List<SubscriptionEvent> getSubscriptionEvents() {
|
||||
return new ArrayList<>(subscriptionEvents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.consensys.pantheon.tests.acceptance.dsl.pubsub;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class WebSocketEvent {
|
||||
|
||||
private static final String SUBSCRIPTION_METHOD = "eth_subscription";
|
||||
|
||||
private final boolean subscription;
|
||||
|
||||
@JsonCreator
|
||||
public WebSocketEvent(@JsonProperty("method") final String method) {
|
||||
this.subscription = SUBSCRIPTION_METHOD.equalsIgnoreCase(method);
|
||||
}
|
||||
|
||||
public boolean isSubscription() {
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.consensys.pantheon.tests.acceptance.jsonrpc;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class Web3Sha3AcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode node;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
node = cluster.create(pantheonNode("node1"));
|
||||
cluster.start(node);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCorrectSha3() throws IOException {
|
||||
final String input = "0x68656c6c6f20776f726c64";
|
||||
final String sha3 = "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad";
|
||||
|
||||
final String response = node.web3().web3Sha3(input);
|
||||
|
||||
assertThat(response).isEqualTo(sha3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.consensys.pantheon.tests.acceptance.mining;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static org.web3j.utils.Convert.Unit.ETHER;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Account;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MiningAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode minerNode;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
minerNode = cluster.create(pantheonMinerNode("miner1"));
|
||||
cluster.start(minerNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMineTransactions() {
|
||||
final Account fromAccount = accounts.createAccount("account1", "50", ETHER, minerNode);
|
||||
final Account toAccount = accounts.createAccount("account2", "0", ETHER, minerNode);
|
||||
accounts.waitForAccountBalance(fromAccount, 50, minerNode);
|
||||
|
||||
accounts.incrementalTransfer(fromAccount, toAccount, 1, minerNode);
|
||||
accounts.waitForAccountBalance(toAccount, 1, minerNode);
|
||||
|
||||
accounts.incrementalTransfer(fromAccount, toAccount, 2, minerNode);
|
||||
accounts.waitForAccountBalance(toAccount, 3, minerNode);
|
||||
|
||||
accounts.incrementalTransfer(fromAccount, toAccount, 3, minerNode);
|
||||
accounts.waitForAccountBalance(toAccount, 6, minerNode);
|
||||
|
||||
accounts.incrementalTransfer(fromAccount, toAccount, 4, minerNode);
|
||||
accounts.waitForAccountBalance(toAccount, 10, minerNode);
|
||||
|
||||
accounts.incrementalTransfer(fromAccount, toAccount, 5, minerNode);
|
||||
accounts.waitForAccountBalance(toAccount, 15, minerNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package net.consensys.pantheon.tests.acceptance.pubsub;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.account.Account;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.pubsub.Subscription;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.pubsub.WebSocket;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class NewPendingTransactionAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private Vertx vertx;
|
||||
private Account accountOne;
|
||||
private WebSocket minerWebSocket;
|
||||
private WebSocket archiveWebSocket;
|
||||
private PantheonNode minerNode;
|
||||
private PantheonNode archiveNode;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
vertx = Vertx.vertx();
|
||||
minerNode = cluster.create(pantheonMinerNode("miner-node1"));
|
||||
archiveNode = cluster.create(pantheonNode("full-node1"));
|
||||
cluster.start(minerNode, archiveNode);
|
||||
accountOne = accounts.createAccount("account-one");
|
||||
minerWebSocket = new WebSocket(vertx, minerNode);
|
||||
archiveWebSocket = new WebSocket(vertx, archiveNode);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
vertx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionRemovedByChainReorganisationMustPublishEvent() throws Exception {
|
||||
|
||||
// Create the light fork
|
||||
final Subscription lightForkSubscription = minerWebSocket.subscribe();
|
||||
|
||||
final Hash lightForkEvent = accounts.transfer(accountOne, 5, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 5);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(1);
|
||||
lightForkSubscription.verifyEventReceived(lightForkEvent);
|
||||
|
||||
final BigInteger lighterForkBlockNumber = minerNode.eth().blockNumber();
|
||||
|
||||
cluster.stop();
|
||||
|
||||
// Create the heavy fork
|
||||
final PantheonNode minerNodeTwo = cluster.create(pantheonMinerNode("miner-node2"));
|
||||
cluster.start(minerNodeTwo);
|
||||
|
||||
final WebSocket heavyForkWebSocket = new WebSocket(vertx, minerNodeTwo);
|
||||
final Subscription heavyForkSubscription = heavyForkWebSocket.subscribe();
|
||||
|
||||
final Account accountTwo = accounts.createAccount("account-two");
|
||||
|
||||
// Keep both forks transactions valid by using a different benefactor
|
||||
final Account heavyForkBenefactor = accounts.getSecondaryBenefactor();
|
||||
|
||||
final Hash heavyForkEventOne =
|
||||
accounts.transfer(heavyForkBenefactor, accountTwo, 1, minerNodeTwo);
|
||||
cluster.awaitPropagation(accountTwo, 1);
|
||||
final Hash heavyForkEventTwo =
|
||||
accounts.transfer(heavyForkBenefactor, accountTwo, 2, minerNodeTwo);
|
||||
cluster.awaitPropagation(accountTwo, 1 + 2);
|
||||
final Hash heavyForkEventThree =
|
||||
accounts.transfer(heavyForkBenefactor, accountTwo, 3, minerNodeTwo);
|
||||
cluster.awaitPropagation(accountTwo, 1 + 2 + 3);
|
||||
|
||||
heavyForkWebSocket.verifyTotalEventsReceived(3);
|
||||
heavyForkSubscription.verifyEventReceived(heavyForkEventOne);
|
||||
heavyForkSubscription.verifyEventReceived(heavyForkEventTwo);
|
||||
heavyForkSubscription.verifyEventReceived(heavyForkEventThree);
|
||||
|
||||
final BigInteger heavierForkBlockNumber = minerNodeTwo.eth().blockNumber();
|
||||
|
||||
cluster.stop();
|
||||
|
||||
// Restart the two nodes on the light fork with the additional node from the heavy fork
|
||||
cluster.start(minerNode, archiveNode, minerNodeTwo);
|
||||
|
||||
final WebSocket minerMergedForksWebSocket = new WebSocket(vertx, minerNode);
|
||||
final WebSocket minerTwoMergedForksWebSocket = new WebSocket(vertx, minerNodeTwo);
|
||||
final WebSocket archiveMergedForksWebSocket = new WebSocket(vertx, archiveNode);
|
||||
final Subscription minerMergedForksSubscription = minerMergedForksWebSocket.subscribe();
|
||||
final Subscription minerTwoMergedForksSubscription = minerTwoMergedForksWebSocket.subscribe();
|
||||
final Subscription archiveMergedForksSubscription = archiveMergedForksWebSocket.subscribe();
|
||||
|
||||
// Check that all node have loaded their respective forks, i.e. not begin new chains
|
||||
assertThat(minerNode.eth().blockNumber()).isGreaterThanOrEqualTo(lighterForkBlockNumber);
|
||||
assertThat(archiveNode.eth().blockNumber()).isGreaterThanOrEqualTo(lighterForkBlockNumber);
|
||||
assertThat(minerNodeTwo.eth().blockNumber()).isGreaterThanOrEqualTo(heavierForkBlockNumber);
|
||||
|
||||
// This publish give time needed for heavy fork to be chosen
|
||||
final Hash mergedForksEventOne =
|
||||
accounts.transfer(accounts.getSecondaryBenefactor(), accountTwo, 3, minerNodeTwo);
|
||||
cluster.awaitPropagation(accountTwo, 9);
|
||||
|
||||
minerMergedForksWebSocket.verifyTotalEventsReceived(1);
|
||||
minerMergedForksSubscription.verifyEventReceived(lightForkEvent);
|
||||
archiveMergedForksWebSocket.verifyTotalEventsReceived(1);
|
||||
archiveMergedForksSubscription.verifyEventReceived(lightForkEvent);
|
||||
minerTwoMergedForksWebSocket.verifyTotalEventsReceived(2);
|
||||
minerTwoMergedForksSubscription.verifyEventReceived(lightForkEvent);
|
||||
minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventOne);
|
||||
|
||||
// Check that account two (funded in heavier chain) can be mined on miner one (from lighter
|
||||
// chain)
|
||||
final Hash mergedForksEventTwo = accounts.transfer(accountTwo, 3, minerNode);
|
||||
cluster.awaitPropagation(accountTwo, 9 + 3);
|
||||
|
||||
// Check that account one (funded in lighter chain) can be mined on miner two (from heavier
|
||||
// chain)
|
||||
final Hash mergedForksEventThree = accounts.transfer(accountOne, 2, minerNodeTwo);
|
||||
cluster.awaitPropagation(accountOne, 5 + 2);
|
||||
|
||||
minerMergedForksWebSocket.verifyTotalEventsReceived(1 + 1 + 1);
|
||||
minerMergedForksSubscription.verifyEventReceived(mergedForksEventTwo);
|
||||
minerMergedForksSubscription.verifyEventReceived(mergedForksEventThree);
|
||||
archiveMergedForksWebSocket.verifyTotalEventsReceived(1 + 1 + 1);
|
||||
archiveMergedForksSubscription.verifyEventReceived(mergedForksEventTwo);
|
||||
archiveMergedForksSubscription.verifyEventReceived(mergedForksEventThree);
|
||||
minerTwoMergedForksWebSocket.verifyTotalEventsReceived(2 + 1 + 1);
|
||||
minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventTwo);
|
||||
minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventThree);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscriptionToMinerNodeMustReceivePublishEvent() {
|
||||
final Subscription minerSubscription = minerWebSocket.subscribe();
|
||||
|
||||
final Hash event = accounts.transfer(accountOne, 4, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 4);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(1);
|
||||
minerSubscription.verifyEventReceived(event);
|
||||
|
||||
minerWebSocket.unsubscribe(minerSubscription);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscriptionToArchiveNodeMustReceivePublishEvent() {
|
||||
final Subscription archiveSubscription = archiveWebSocket.subscribe();
|
||||
|
||||
final Hash event = accounts.transfer(accountOne, 23, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 23);
|
||||
|
||||
archiveWebSocket.verifyTotalEventsReceived(1);
|
||||
archiveSubscription.verifyEventReceived(event);
|
||||
|
||||
archiveWebSocket.unsubscribe(archiveSubscription);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everySubscriptionMustReceivePublishEvent() {
|
||||
final Subscription minerSubscriptionOne = minerWebSocket.subscribe();
|
||||
final Subscription minerSubscriptionTwo = minerWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionOne = archiveWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionTwo = archiveWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionThree = archiveWebSocket.subscribe();
|
||||
|
||||
final Hash event = accounts.transfer(accountOne, 30, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 30);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(2);
|
||||
minerSubscriptionOne.verifyEventReceived(event);
|
||||
minerSubscriptionTwo.verifyEventReceived(event);
|
||||
|
||||
archiveWebSocket.verifyTotalEventsReceived(3);
|
||||
archiveSubscriptionOne.verifyEventReceived(event);
|
||||
archiveSubscriptionTwo.verifyEventReceived(event);
|
||||
archiveSubscriptionThree.verifyEventReceived(event);
|
||||
|
||||
minerWebSocket.unsubscribe(minerSubscriptionOne);
|
||||
minerWebSocket.unsubscribe(minerSubscriptionTwo);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionOne);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionTwo);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionThree);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscriptionToMinerNodeMustReceiveEveryPublishEvent() {
|
||||
final Subscription minerSubscription = minerWebSocket.subscribe();
|
||||
|
||||
final Hash eventOne = accounts.transfer(accountOne, 1, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 1);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(1);
|
||||
minerSubscription.verifyEventReceived(eventOne);
|
||||
|
||||
final Hash eventTwo = accounts.transfer(accountOne, 4, minerNode);
|
||||
final Hash eventThree = accounts.transfer(accountOne, 5, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 1 + 4 + 5);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(3);
|
||||
minerSubscription.verifyEventReceived(eventTwo);
|
||||
minerSubscription.verifyEventReceived(eventThree);
|
||||
|
||||
minerWebSocket.unsubscribe(minerSubscription);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subscriptionToArchiveNodeMustReceiveEveryPublishEvent() {
|
||||
final Subscription archiveSubscription = archiveWebSocket.subscribe();
|
||||
|
||||
final Hash eventOne = accounts.transfer(accountOne, 2, minerNode);
|
||||
final Hash eventTwo = accounts.transfer(accountOne, 5, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 2 + 5);
|
||||
|
||||
archiveWebSocket.verifyTotalEventsReceived(2);
|
||||
archiveSubscription.verifyEventReceived(eventOne);
|
||||
archiveSubscription.verifyEventReceived(eventTwo);
|
||||
|
||||
final Hash eventThree = accounts.transfer(accountOne, 8, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 2 + 5 + 8);
|
||||
|
||||
archiveWebSocket.verifyTotalEventsReceived(3);
|
||||
archiveSubscription.verifyEventReceived(eventThree);
|
||||
|
||||
archiveWebSocket.unsubscribe(archiveSubscription);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everySubscriptionMustReceiveEveryPublishEvent() {
|
||||
final Subscription minerSubscriptionOne = minerWebSocket.subscribe();
|
||||
final Subscription minerSubscriptionTwo = minerWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionOne = archiveWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionTwo = archiveWebSocket.subscribe();
|
||||
final Subscription archiveSubscriptionThree = archiveWebSocket.subscribe();
|
||||
|
||||
final Hash eventOne = accounts.transfer(accountOne, 10, minerNode);
|
||||
final Hash eventTwo = accounts.transfer(accountOne, 5, minerNode);
|
||||
cluster.awaitPropagation(accountOne, 10 + 5);
|
||||
|
||||
minerWebSocket.verifyTotalEventsReceived(4);
|
||||
minerSubscriptionOne.verifyEventReceived(eventOne);
|
||||
minerSubscriptionOne.verifyEventReceived(eventTwo);
|
||||
minerSubscriptionTwo.verifyEventReceived(eventOne);
|
||||
minerSubscriptionTwo.verifyEventReceived(eventTwo);
|
||||
|
||||
archiveWebSocket.verifyTotalEventsReceived(6);
|
||||
archiveSubscriptionOne.verifyEventReceived(eventOne);
|
||||
archiveSubscriptionOne.verifyEventReceived(eventTwo);
|
||||
archiveSubscriptionTwo.verifyEventReceived(eventOne);
|
||||
archiveSubscriptionTwo.verifyEventReceived(eventTwo);
|
||||
archiveSubscriptionThree.verifyEventReceived(eventOne);
|
||||
archiveSubscriptionThree.verifyEventReceived(eventTwo);
|
||||
|
||||
minerWebSocket.unsubscribe(minerSubscriptionOne);
|
||||
minerWebSocket.unsubscribe(minerSubscriptionTwo);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionOne);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionTwo);
|
||||
archiveWebSocket.unsubscribe(archiveSubscriptionThree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
pragma solidity ^0.4.0;
|
||||
// compile with:
|
||||
// solc EventEmitter.sol --bin --abi --optimize --overwrite -o .
|
||||
// then create web3j wrappers with:
|
||||
// web3j solidity generate ./generated/EventEmitter.bin ./generated/EventEmitter.abi -o ../../../../../ -p net.consensys.pantheon.tests.web3j.generated
|
||||
contract EventEmitter {
|
||||
address owner;
|
||||
event stored(address _to, uint _amount);
|
||||
address _sender;
|
||||
uint _value;
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
function store(uint _amount) public {
|
||||
emit stored(msg.sender, _amount);
|
||||
_value = _amount;
|
||||
_sender = msg.sender;
|
||||
}
|
||||
|
||||
function value() constant public returns (uint) {
|
||||
return _value;
|
||||
}
|
||||
|
||||
function sender() constant public returns (address) {
|
||||
return _sender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.consensys.pantheon.tests.web3j;
|
||||
|
||||
import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
import net.consensys.pantheon.tests.web3j.generated.EventEmitter;
|
||||
import net.consensys.pantheon.tests.web3j.generated.EventEmitter.StoredEventResponse;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.web3j.crypto.Credentials;
|
||||
import org.web3j.protocol.core.methods.request.EthFilter;
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt;
|
||||
import rx.Observable;
|
||||
|
||||
/*
|
||||
* This class is based around the EventEmitter solidity contract
|
||||
*
|
||||
*/
|
||||
public class EventEmitterAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
public static final BigInteger DEFAULT_GAS_PRICE = BigInteger.valueOf(1000);
|
||||
public static final BigInteger DEFAULT_GAS_LIMIT = BigInteger.valueOf(3000000);
|
||||
Credentials MAIN_CREDENTIALS =
|
||||
Credentials.create("0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63");
|
||||
|
||||
private PantheonNode node;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
node = cluster.create(pantheonMinerNode("node1"));
|
||||
cluster.start(node);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeployContractAndAllowLookupOfValuesAndEmittingEvents() throws Exception {
|
||||
System.out.println("Sending Create Contract Transaction");
|
||||
final EventEmitter eventEmitter =
|
||||
EventEmitter.deploy(node.web3j(), MAIN_CREDENTIALS, DEFAULT_GAS_PRICE, DEFAULT_GAS_LIMIT)
|
||||
.send();
|
||||
final Observable<StoredEventResponse> storedEventResponseObservable =
|
||||
eventEmitter.storedEventObservable(new EthFilter());
|
||||
final AtomicBoolean subscriptionReceived = new AtomicBoolean(false);
|
||||
storedEventResponseObservable.subscribe(
|
||||
storedEventResponse -> {
|
||||
subscriptionReceived.set(true);
|
||||
assertEquals(BigInteger.valueOf(12), storedEventResponse._amount);
|
||||
});
|
||||
|
||||
final TransactionReceipt send = eventEmitter.store(BigInteger.valueOf(12)).send();
|
||||
|
||||
assertEquals(BigInteger.valueOf(12), eventEmitter.value().send());
|
||||
assertTrue(subscriptionReceived.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"constant":true,"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"}],"name":"store","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"sender","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_to","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"stored","type":"event"}]
|
||||
@@ -0,0 +1 @@
|
||||
608060405234801561001057600080fd5b5060008054600160a060020a03191633179055610187806100326000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fa4f245811461005b5780636057361d1461008257806367e404ce1461009c575b600080fd5b34801561006757600080fd5b506100706100da565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061009a6004356100e0565b005b3480156100a857600080fd5b506100b161013f565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b60025490565b604080513381526020810183905281517fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5929181900390910190a16002556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a72305820f958aea7922a9538be4c34980ad3171806aad2d3fedb62682cef2ca4e1f1f3120029
|
||||
@@ -0,0 +1,184 @@
|
||||
package net.consensys.pantheon.tests.web3j.generated;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.web3j.abi.EventEncoder;
|
||||
import org.web3j.abi.TypeReference;
|
||||
import org.web3j.abi.datatypes.Address;
|
||||
import org.web3j.abi.datatypes.Event;
|
||||
import org.web3j.abi.datatypes.Function;
|
||||
import org.web3j.abi.datatypes.Type;
|
||||
import org.web3j.abi.datatypes.generated.Uint256;
|
||||
import org.web3j.crypto.Credentials;
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.core.DefaultBlockParameter;
|
||||
import org.web3j.protocol.core.RemoteCall;
|
||||
import org.web3j.protocol.core.methods.request.EthFilter;
|
||||
import org.web3j.protocol.core.methods.response.Log;
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt;
|
||||
import org.web3j.tx.Contract;
|
||||
import org.web3j.tx.TransactionManager;
|
||||
import rx.Observable;
|
||||
import rx.functions.Func1;
|
||||
|
||||
/**
|
||||
* Auto generated code.
|
||||
*
|
||||
* <p><strong>Do not modify!</strong>
|
||||
*
|
||||
* <p>Please use the <a href="https://docs.web3j.io/command_line.html">web3j command line tools</a>,
|
||||
* or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the <a
|
||||
* href="https://github.com/web3j/web3j/tree/master/codegen">codegen module</a> to update.
|
||||
*
|
||||
* <p>Generated with web3j version 3.5.0.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class EventEmitter extends Contract {
|
||||
private static final String BINARY =
|
||||
"608060405234801561001057600080fd5b5060008054600160a060020a03191633179055610187806100326000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fa4f245811461005b5780636057361d1461008257806367e404ce1461009c575b600080fd5b34801561006757600080fd5b506100706100da565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061009a6004356100e0565b005b3480156100a857600080fd5b506100b161013f565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b60025490565b604080513381526020810183905281517fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5929181900390910190a16002556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a72305820f958aea7922a9538be4c34980ad3171806aad2d3fedb62682cef2ca4e1f1f3120029";
|
||||
|
||||
public static final String FUNC_VALUE = "value";
|
||||
|
||||
public static final String FUNC_STORE = "store";
|
||||
|
||||
public static final String FUNC_SENDER = "sender";
|
||||
|
||||
public static final Event STORED_EVENT =
|
||||
new Event(
|
||||
"stored",
|
||||
Arrays.<TypeReference<?>>asList(
|
||||
new TypeReference<Address>() {}, new TypeReference<Uint256>() {}));
|
||||
|
||||
protected EventEmitter(
|
||||
final String contractAddress,
|
||||
final Web3j web3j,
|
||||
final Credentials credentials,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
|
||||
}
|
||||
|
||||
protected EventEmitter(
|
||||
final String contractAddress,
|
||||
final Web3j web3j,
|
||||
final TransactionManager transactionManager,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
|
||||
}
|
||||
|
||||
public RemoteCall<BigInteger> value() {
|
||||
final Function function =
|
||||
new Function(
|
||||
FUNC_VALUE,
|
||||
Arrays.<Type>asList(),
|
||||
Arrays.<TypeReference<?>>asList(new TypeReference<Uint256>() {}));
|
||||
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
|
||||
}
|
||||
|
||||
public RemoteCall<TransactionReceipt> store(final BigInteger _amount) {
|
||||
final Function function =
|
||||
new Function(
|
||||
FUNC_STORE,
|
||||
Arrays.<Type>asList(new org.web3j.abi.datatypes.generated.Uint256(_amount)),
|
||||
Collections.<TypeReference<?>>emptyList());
|
||||
return executeRemoteCallTransaction(function);
|
||||
}
|
||||
|
||||
public RemoteCall<String> sender() {
|
||||
final Function function =
|
||||
new Function(
|
||||
FUNC_SENDER,
|
||||
Arrays.<Type>asList(),
|
||||
Arrays.<TypeReference<?>>asList(new TypeReference<Address>() {}));
|
||||
return executeRemoteCallSingleValueReturn(function, String.class);
|
||||
}
|
||||
|
||||
public static RemoteCall<EventEmitter> deploy(
|
||||
final Web3j web3j,
|
||||
final Credentials credentials,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
return deployRemoteCall(EventEmitter.class, web3j, credentials, gasPrice, gasLimit, BINARY, "");
|
||||
}
|
||||
|
||||
public static RemoteCall<EventEmitter> deploy(
|
||||
final Web3j web3j,
|
||||
final TransactionManager transactionManager,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
return deployRemoteCall(
|
||||
EventEmitter.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, "");
|
||||
}
|
||||
|
||||
public List<StoredEventResponse> getStoredEvents(final TransactionReceipt transactionReceipt) {
|
||||
final List<Contract.EventValuesWithLog> valueList =
|
||||
extractEventParametersWithLog(STORED_EVENT, transactionReceipt);
|
||||
final ArrayList<StoredEventResponse> responses =
|
||||
new ArrayList<StoredEventResponse>(valueList.size());
|
||||
for (final Contract.EventValuesWithLog eventValues : valueList) {
|
||||
final StoredEventResponse typedResponse = new StoredEventResponse();
|
||||
typedResponse.log = eventValues.getLog();
|
||||
typedResponse._to = (String) eventValues.getNonIndexedValues().get(0).getValue();
|
||||
typedResponse._amount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue();
|
||||
responses.add(typedResponse);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
public Observable<StoredEventResponse> storedEventObservable(final EthFilter filter) {
|
||||
return web3j
|
||||
.ethLogObservable(filter)
|
||||
.map(
|
||||
new Func1<Log, StoredEventResponse>() {
|
||||
@Override
|
||||
public StoredEventResponse call(final Log log) {
|
||||
final Contract.EventValuesWithLog eventValues =
|
||||
extractEventParametersWithLog(STORED_EVENT, log);
|
||||
final StoredEventResponse typedResponse = new StoredEventResponse();
|
||||
typedResponse.log = log;
|
||||
typedResponse._to = (String) eventValues.getNonIndexedValues().get(0).getValue();
|
||||
typedResponse._amount =
|
||||
(BigInteger) eventValues.getNonIndexedValues().get(1).getValue();
|
||||
return typedResponse;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<StoredEventResponse> storedEventObservable(
|
||||
final DefaultBlockParameter startBlock, final DefaultBlockParameter endBlock) {
|
||||
final EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
|
||||
filter.addSingleTopic(EventEncoder.encode(STORED_EVENT));
|
||||
return storedEventObservable(filter);
|
||||
}
|
||||
|
||||
public static EventEmitter load(
|
||||
final String contractAddress,
|
||||
final Web3j web3j,
|
||||
final Credentials credentials,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
return new EventEmitter(contractAddress, web3j, credentials, gasPrice, gasLimit);
|
||||
}
|
||||
|
||||
public static EventEmitter load(
|
||||
final String contractAddress,
|
||||
final Web3j web3j,
|
||||
final TransactionManager transactionManager,
|
||||
final BigInteger gasPrice,
|
||||
final BigInteger gasLimit) {
|
||||
return new EventEmitter(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
|
||||
}
|
||||
|
||||
public static class StoredEventResponse {
|
||||
public Log log;
|
||||
|
||||
public String _to;
|
||||
|
||||
public BigInteger _amount;
|
||||
}
|
||||
}
|
||||
16
acceptance-tests/src/test/resources/log4j2.xml
Executable file
16
acceptance-tests/src/test/resources/log4j2.xml
Executable file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="INFO" monitorInterval="30">
|
||||
<Properties>
|
||||
<Property name="root.log.level">INFO</Property>
|
||||
</Properties>
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} | %t | %-5level | %c{1} | %msg%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="${sys:root.log.level}">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
78
acceptance-tests/truffle-pet-shop-tutorial/README.md
Executable file
78
acceptance-tests/truffle-pet-shop-tutorial/README.md
Executable file
@@ -0,0 +1,78 @@
|
||||
# Truffle Pet Shop example with Pantheon
|
||||
Here you will find the bare bones of the [Pet Shop example](http://truffleframework.com/tutorials/pet-shop) and instructions to run it with Pantheon and a wallet to manage keys.
|
||||
## Pre-requisites
|
||||
* [Truffle](https://truffleframework.com/) installed
|
||||
```
|
||||
npm install -g truffle
|
||||
```
|
||||
* [Wallet](https://www.npmjs.com/package/truffle-privatekey-provider) installed
|
||||
```
|
||||
npm install truffle-privatekey-provider
|
||||
```
|
||||
## To run the pet shop example:
|
||||
```
|
||||
cd acceptance-tests/truffle-pet-shop-tutorial
|
||||
```
|
||||
* here you will find truffle.js which has network configurations for
|
||||
* development (Ganache) and
|
||||
* devwallet (points to localhost:8545)
|
||||
* Note you don't need Ganache running unless you want to run the tests against it (see below)
|
||||
* However this truffle.js uses address and private key generated by Ganache with default mnemonic "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"
|
||||
|
||||
* To run the Truffle example with Pantheon, you need Pantheon running
|
||||
* [check out and build Pantheon](../../README.md)
|
||||
* run Pantheon (either in IDE or via command line), with mining and rpc enabled. Eg:
|
||||
|
||||
```
|
||||
cd $pantheon-working-dir
|
||||
./gradlew run -Ppantheon.run.args="--miner-enabled --miner-coinbase 0x627306090abaB3A6e1400e9345bC60c78a8BEf57 --sync-mode FULL --no-discovery --dev-mode --rpc-enabled --rpc-api eth,net --rpc-cors-origins 'all'"
|
||||
```
|
||||
* Run Truffle migrate
|
||||
```
|
||||
truffle migrate --network devwallet
|
||||
```
|
||||
* Output should look something like:
|
||||
```
|
||||
Using network 'devwallet'.
|
||||
|
||||
Running migration: 1_initial_migration.js
|
||||
Deploying Migrations...
|
||||
... 0x2c16dd43c0adfe0c697279e388f531581c2b722e7f0e968e3e65e4345bdeb502
|
||||
Migrations: 0xfb88de099e13c3ed21f80a7a1e49f8caecf10df6
|
||||
Saving successful migration to network...
|
||||
... 0x1135ea1dd6947f262d65dde8712d17b4b0ec0a36cc917772ce8acd7fe01ca8e2
|
||||
Saving artifacts...
|
||||
Running migration: 2_deploy_contracts.js
|
||||
Deploying Adoption...
|
||||
... 0xa3d220639719b8e007a7aa8cb18e8caf3587337b77bac833959f4853b1695369
|
||||
Adoption: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a
|
||||
Saving successful migration to network...
|
||||
... 0xd7245d7b1c0a7eb5a5198754f7edd7abdae3b806605b54ecc4716f9b4b05de61
|
||||
Saving artifacts...
|
||||
|
||||
```
|
||||
If migrate works, try running the tests
|
||||
|
||||
```
|
||||
cd acceptance-tests/truffle-pet-shop-tutorial
|
||||
truffle test --network devwallet
|
||||
```
|
||||
* Output should look something like:
|
||||
```
|
||||
Using network 'devwallet'.
|
||||
|
||||
Compiling ./contracts/Adoption.sol...
|
||||
Compiling ./test/TestAdoption.sol...
|
||||
Compiling truffle/Assert.sol...
|
||||
Compiling truffle/DeployedAddresses.sol...
|
||||
|
||||
|
||||
TestAdoption
|
||||
✓ testUserCanAdoptPet (4050ms)
|
||||
✓ testGetAdopterAddressByPetId (5050ms)
|
||||
✓ testGetAdopterAddressByPetIdInArray (4039ms)
|
||||
✓ testUserCanUnadoptPet (5049ms)
|
||||
|
||||
|
||||
4 passing (52s)
|
||||
```
|
||||
37
acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol
Executable file
37
acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol
Executable file
@@ -0,0 +1,37 @@
|
||||
pragma solidity ^0.4.17;
|
||||
|
||||
contract Adoption {
|
||||
address[16] public adopters;
|
||||
// Adopting a pet
|
||||
function adopt(uint petId) public returns (uint) {
|
||||
require(petId >= 0 && petId <= 15);
|
||||
|
||||
adopters[petId] = msg.sender;
|
||||
|
||||
return petId;
|
||||
}
|
||||
// who has adopted this pet?
|
||||
function getOwner(uint petId) public view returns (address) {
|
||||
require(petId >= 0 && petId <= 15);
|
||||
return adopters[petId];
|
||||
}
|
||||
|
||||
// is this pet adopted?
|
||||
function isAdopted(uint petId) public view returns (bool) {
|
||||
require(petId >= 0 && petId <= 15);
|
||||
return adopters[petId] != 0;
|
||||
}
|
||||
// Adopting a pet
|
||||
function unadopt(uint petId) public returns (uint) {
|
||||
require(petId >= 0 && petId <= 15);
|
||||
|
||||
adopters[petId] = 0;
|
||||
|
||||
return petId;
|
||||
}
|
||||
|
||||
// Retrieving the adopters
|
||||
function getAdopters() public view returns (address[16]) {
|
||||
return adopters;
|
||||
}
|
||||
}
|
||||
23
acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol
Executable file
23
acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol
Executable file
@@ -0,0 +1,23 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
contract Migrations {
|
||||
address public owner;
|
||||
uint public last_completed_migration;
|
||||
|
||||
modifier restricted() {
|
||||
if (msg.sender == owner) _;
|
||||
}
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
function setCompleted(uint completed) public restricted {
|
||||
last_completed_migration = completed;
|
||||
}
|
||||
|
||||
function upgrade(address new_address) public restricted {
|
||||
Migrations upgraded = Migrations(new_address);
|
||||
upgraded.setCompleted(last_completed_migration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
var Migrations = artifacts.require("./Migrations.sol");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
var Adoption = artifacts.require("Adoption");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Adoption);
|
||||
};
|
||||
52
acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol
Executable file
52
acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol
Executable file
@@ -0,0 +1,52 @@
|
||||
pragma solidity ^0.4.17;
|
||||
|
||||
import "truffle/Assert.sol";
|
||||
import "truffle/DeployedAddresses.sol";
|
||||
import "../contracts/Adoption.sol";
|
||||
|
||||
contract TestAdoption {
|
||||
Adoption adoption = Adoption(DeployedAddresses.Adoption());
|
||||
// Testing the adopt() function
|
||||
function testUserCanAdoptPet() public {
|
||||
uint returnedId = adoption.adopt(8);
|
||||
|
||||
uint expected = 8;
|
||||
|
||||
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
|
||||
}
|
||||
|
||||
// Testing retrieval of a single pet's owner
|
||||
function testGetAdopterAddressByPetId() public {
|
||||
// Expected owner is this contract
|
||||
address expected = this;
|
||||
|
||||
address adopter = adoption.adopters(8);
|
||||
|
||||
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
|
||||
}
|
||||
|
||||
// Testing retrieval of all pet owners
|
||||
function testGetAdopterAddressByPetIdInArray() public {
|
||||
// Expected owner is this contract
|
||||
address expected = this;
|
||||
|
||||
// Store adopters in memory rather than contract's storage
|
||||
address[16] memory adopters = adoption.getAdopters();
|
||||
|
||||
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
|
||||
}
|
||||
|
||||
|
||||
// Testing the unadopt() function
|
||||
function testUserCanUnadoptPet() public {
|
||||
uint returnedId = adoption.unadopt(8);
|
||||
|
||||
uint expected = 8;
|
||||
|
||||
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
|
||||
|
||||
// Store adopters in memory rather than contract's storage
|
||||
address[16] memory adopters = adoption.getAdopters();
|
||||
Assert.equal(adopters[8], 0, "owner should be reset after unadopt");
|
||||
}
|
||||
}
|
||||
32
acceptance-tests/truffle-pet-shop-tutorial/test/test.js
Executable file
32
acceptance-tests/truffle-pet-shop-tutorial/test/test.js
Executable file
@@ -0,0 +1,32 @@
|
||||
/* global artifacts contract describe assert it */
|
||||
|
||||
const TestAdoption = artifacts.require('Adoption.sol');
|
||||
var proxy;
|
||||
contract('Adoption 1', () => {
|
||||
describe('Function: adopt pet 1', () => {
|
||||
it('Should successfully adopt pet within range', async () => {
|
||||
proxy = await TestAdoption.new();
|
||||
await proxy.adopt(1);
|
||||
|
||||
assert(true, 'expected adoption of pet within range to succeed');
|
||||
});
|
||||
|
||||
it('Should catch an error and then return', async () => {
|
||||
try {
|
||||
await proxy.adopt(22);
|
||||
} catch (err) {
|
||||
assert(true, err.toString().includes('revert'), 'expected revert in message');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert(false, 'did not catch expected error from petID out of range');
|
||||
});
|
||||
|
||||
it('Should successfully adopt pet within range 2', async () => {
|
||||
await proxy.adopt(2);
|
||||
|
||||
assert(true, 'expected adoption of pet within range to succeed');
|
||||
});
|
||||
});
|
||||
});
|
||||
24
acceptance-tests/truffle-pet-shop-tutorial/test/test2.js
Executable file
24
acceptance-tests/truffle-pet-shop-tutorial/test/test2.js
Executable file
@@ -0,0 +1,24 @@
|
||||
/* global artifacts contract describe assert it */
|
||||
|
||||
const TestAdoption = artifacts.require('Adoption.sol');
|
||||
var proxy;
|
||||
contract('Adoption 2', () => {
|
||||
describe('Function: adopt pet 3', () => {
|
||||
it('Should successfully adopt pet within range', async () => {
|
||||
proxy = await TestAdoption.new();
|
||||
|
||||
await proxy.adopt(3);
|
||||
assert(true, 'expected adoption of pet within range to succeed');
|
||||
|
||||
const isAdopted = await proxy.isAdopted(3);
|
||||
assert.equal(isAdopted, true, 'expected pet 3 to be adopted (adopted in this test method)');
|
||||
});
|
||||
|
||||
it('Pet 2 should NOT be adopted (was adopted in a different test file)', async () => {
|
||||
// check status of pet2
|
||||
const isAdopted = await proxy.isAdopted(2);
|
||||
|
||||
assert.equal(isAdopted, false, 'expected pet 2 to NOT be adopted (was adopted in a different test file)');
|
||||
});
|
||||
});
|
||||
});
|
||||
21
acceptance-tests/truffle-pet-shop-tutorial/truffle.js
Executable file
21
acceptance-tests/truffle-pet-shop-tutorial/truffle.js
Executable file
@@ -0,0 +1,21 @@
|
||||
const PrivateKeyProvider = require('truffle-privatekey-provider');
|
||||
|
||||
// address 627306090abaB3A6e1400e9345bC60c78a8BEf57
|
||||
const privateKey = 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3';
|
||||
const localDev = 'http://127.0.0.1:8545';
|
||||
|
||||
const privateKeyProvider = new PrivateKeyProvider(privateKey, localDev);
|
||||
|
||||
module.exports = {
|
||||
networks: {
|
||||
development: {
|
||||
host: '127.0.0.1',
|
||||
port: 7545,
|
||||
network_id: '*',
|
||||
},
|
||||
devwallet: {
|
||||
provider: privateKeyProvider,
|
||||
network_id: '2018',
|
||||
},
|
||||
},
|
||||
};
|
||||
401
build.gradle
Executable file
401
build.gradle
Executable file
@@ -0,0 +1,401 @@
|
||||
import net.ltgt.gradle.errorprone.CheckSeverity
|
||||
|
||||
plugins {
|
||||
id 'com.diffplug.gradle.spotless' version '3.13.0'
|
||||
id 'com.palantir.git-version' version '0.10.0'
|
||||
id 'io.spring.dependency-management' version '1.0.4.RELEASE'
|
||||
id 'com.github.hierynomus.license' version '0.14.0'
|
||||
id 'net.ltgt.errorprone' version '0.6'
|
||||
id 'me.champeau.gradle.jmh' version '0.4.5' apply false
|
||||
id 'com.jfrog.bintray' version '1.7.3'
|
||||
}
|
||||
|
||||
apply from: './versions.gradle'
|
||||
|
||||
defaultTasks 'build', 'checkLicenses', 'javadoc'
|
||||
|
||||
def buildAliases = ['dev': [
|
||||
'spotlessApply',
|
||||
'build',
|
||||
'checkLicenses',
|
||||
'javadoc'
|
||||
]]
|
||||
|
||||
def expandedTaskList = []
|
||||
gradle.startParameter.taskNames.each {
|
||||
expandedTaskList << (buildAliases[it] ? buildAliases[it] : it)
|
||||
}
|
||||
gradle.startParameter.taskNames = expandedTaskList.flatten()
|
||||
|
||||
// Gets a integer command argument, passed with -Pname=x, or the defaut if not provided.
|
||||
def _intCmdArg(name, defaultValue) {
|
||||
return project.hasProperty(name) ? project.property(name) as int : defaultValue
|
||||
}
|
||||
|
||||
def _intCmdArg(name) {
|
||||
return _intCmdArg(name, null)
|
||||
}
|
||||
|
||||
def _strListCmdArg(name, defaultValue) {
|
||||
if (!project.hasProperty(name))
|
||||
return defaultValue
|
||||
|
||||
return ((String)project.property(name)).tokenize(',')
|
||||
}
|
||||
|
||||
def _strListCmdArg(name) {
|
||||
return _strListCmdArg(name, null)
|
||||
}
|
||||
|
||||
def baseVersion = '0.8.0'
|
||||
project.version = baseVersion + '-SNAPSHOT'
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'net.ltgt.errorprone'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply from: "${rootDir}/gradle/versions.gradle"
|
||||
apply from: "${rootDir}/gradle/check-licenses.gradle"
|
||||
|
||||
version = rootProject.version
|
||||
|
||||
jacoco { toolVersion = '0.8.2' }
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url "https://consensys.bintray.com/pegasys-repo" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
errorprone("com.google.errorprone:error_prone_core:$errorproneCore")
|
||||
if (JavaVersion.current().isJava8()) {
|
||||
errorproneJavac("com.google.errorprone:javac:$errorproneJavac")
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.diffplug.gradle.spotless'
|
||||
spotless {
|
||||
java {
|
||||
// This path needs to be relative to each project
|
||||
target fileTree('.') {
|
||||
include '**/*.java'
|
||||
exclude '**/generalstate/GeneralStateReferenceTest*.java'
|
||||
exclude '**/generalstate/GeneralStateRegressionReferenceTest*.java'
|
||||
exclude '**/blockchain/BlockchainReferenceTest*.java'
|
||||
exclude '**/.gradle/**'
|
||||
}
|
||||
removeUnusedImports()
|
||||
googleJavaFormat()
|
||||
importOrder 'net.consensys', 'java', ''
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
groovyGradle {
|
||||
target '*.gradle'
|
||||
greclipse().configFile(rootProject.file('gradle/formatter.properties'))
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs += [
|
||||
'-Xlint:unchecked',
|
||||
'-Xlint:cast',
|
||||
'-Xlint:rawtypes',
|
||||
'-Xlint:overloads',
|
||||
'-Xlint:divzero',
|
||||
'-Xlint:finally',
|
||||
'-Xlint:static',
|
||||
'-Werror',
|
||||
]
|
||||
|
||||
options.errorprone {
|
||||
excludedPaths '.*/(generated|.*ReferenceTest_.*)'
|
||||
check('FutureReturnValueIgnored', CheckSeverity.OFF)
|
||||
check('InsecureCryptoUsage', CheckSeverity.WARN)
|
||||
check('FieldCanBeFinal', CheckSeverity.WARN)
|
||||
check('WildcardImport', CheckSeverity.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pass some system properties provided on the gradle command line to test executions for
|
||||
* convenience.
|
||||
*
|
||||
* The properties passed are:
|
||||
* - 'test.ethereum.include': allows to run a single Ethereum reference tests. For instance,
|
||||
* running a single general state test can be done with:
|
||||
* ./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.include=callcodecallcallcode_101-Frontier
|
||||
* The meaning being that will be run only the tests for which the value passed as "include"
|
||||
* (which can be a java pattern) matches parts of the test name. Knowing that tests names for
|
||||
* reference tests are of the form:
|
||||
* <name>(-<milestone>([<variant>])?)?
|
||||
* where <name> is the test name as defined in the json file (usually the name of the json file
|
||||
* as well), <milestone> is the Ethereum milestone tested (not all test use it) and <variant>
|
||||
* is only use in some general state tests where for the same json file and same milestone,
|
||||
* multiple variant of that test are run. The variant is a simple number.
|
||||
* - 'test.ethereum.state.eip': for general state tests, allows to only run tests for the
|
||||
* milestone specified by this value. So for instance,
|
||||
* ./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.state.eip=Frontier
|
||||
* only run general state tests for Frontier. Note that this behavior could be achieved as well
|
||||
* with the 'include' option above since it is a pattern, but this is a slightly more convenient
|
||||
* option.
|
||||
* - 'root.log.level' and 'evm.log.level': allow to control the log level used during the tests.
|
||||
*/
|
||||
test {
|
||||
jvmArgs = [
|
||||
'-Xmx4g',
|
||||
'-XX:-UseGCOverheadLimit'
|
||||
]
|
||||
Set toImport = [
|
||||
'test.ethereum.include',
|
||||
'test.ethereum.state.eip',
|
||||
'root.log.level',
|
||||
'evm.log.level'
|
||||
]
|
||||
for (String name : toImport) {
|
||||
if (System.getProperty(name) != null) {
|
||||
systemProperty name, System.getProperty(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise Xdoclint behaviour across JDKs (OpenJDK 8 is more lenient than Oracle JDK by default).
|
||||
javadoc {
|
||||
options.addStringOption('Xdoclint:all', '-quiet')
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task deploy() {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
subprojects {
|
||||
|
||||
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
|
||||
}
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
sourceSets {
|
||||
// test-support can be consumed as a library by other projects in their tests
|
||||
testSupport {
|
||||
java {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
srcDir file('src/test-support/java')
|
||||
}
|
||||
resources.srcDir file('src/test-support/resources')
|
||||
}
|
||||
integrationTest {
|
||||
java {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
srcDir file('src/integration-test/java')
|
||||
}
|
||||
resources.srcDir file('src/integration-test/resources')
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
testSupportImplementation.extendsFrom implementation
|
||||
integrationTestImplementation.extendsFrom implementation
|
||||
testSupportArtifacts
|
||||
}
|
||||
|
||||
task testSupportJar (type: Jar) {
|
||||
baseName = "${project.name}-support-test"
|
||||
from sourceSets.testSupport.output
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation sourceSets.testSupport.output
|
||||
integrationTestImplementation sourceSets.testSupport.output
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn:["compileTestJava"]){
|
||||
group = "verification"
|
||||
description = "Runs the Pantheon Integration Test"
|
||||
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
if (file('src/jmh').directory) {
|
||||
apply plugin: 'me.champeau.gradle.jmh'
|
||||
|
||||
jmhCompileGeneratedClasses {
|
||||
options.compilerArgs += [
|
||||
'-XepDisableAllChecks'
|
||||
]
|
||||
}
|
||||
|
||||
jmh {
|
||||
// Allows to control JMH execution directly from the command line. I typical execution may look
|
||||
// like:
|
||||
// gradle jmh -Pf=2 -Pwi=3 -Pi=5 -Pinclude=MyBench
|
||||
// which will run 2 forks with 3 warmup iterations and 5 normal ones for each, and will only
|
||||
// run the benchmark matching 'MyBench' (a regexp).
|
||||
warmupForks = _intCmdArg('wf')
|
||||
warmupIterations = _intCmdArg('wi')
|
||||
fork = _intCmdArg('f')
|
||||
iterations = _intCmdArg('i')
|
||||
benchmarkMode = _strListCmdArg('bm')
|
||||
include = _strListCmdArg('include', [''])
|
||||
humanOutputFile = project.file("${project.buildDir}/reports/jmh/results.txt")
|
||||
resultFormat = 'JSON'
|
||||
}
|
||||
|
||||
dependencies { jmh 'org.apache.logging.log4j:log4j-api' }
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
if (project.jar.enabled) {
|
||||
publishing {
|
||||
publications {
|
||||
MavenDeployment(MavenPublication) {
|
||||
from components.java
|
||||
groupId 'net.consensys.pantheon'
|
||||
artifactId project.jar.baseName
|
||||
version project.version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = System.getenv('BINTRAY_USER')
|
||||
key = System.getenv('BINTRAY_KEY')
|
||||
publications = ['MavenDeployment']
|
||||
pkg {
|
||||
repo = rootProject.name.toLowerCase()
|
||||
name = rootProject.name.capitalize()
|
||||
userOrg = 'consensys'
|
||||
licenses = ['Apache-2.0']
|
||||
version {
|
||||
name = project.version
|
||||
desc = rootProject.name.capitalize() + ' distribution'
|
||||
released = new Date()
|
||||
vcsTag = project.version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deploy.dependsOn bintrayUpload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jar { enabled = false }
|
||||
|
||||
apply plugin: 'application'
|
||||
mainClassName = "net.consensys.pantheon.Pantheon"
|
||||
applicationDefaultJvmArgs = [
|
||||
"-Dvertx.disableFileCPResolving=true",
|
||||
"-Dpantheon.home=PANTHEON_HOME",
|
||||
// We shutdown log4j ourselves, as otherwise his shutdown hook runs before our own and whatever
|
||||
// happens during shutdown is not logged.
|
||||
"-Dlog4j.shutdownHookEnabled=false",
|
||||
"-Djava.security.egd=file:/dev/urandom"
|
||||
]
|
||||
|
||||
run {
|
||||
args project.hasProperty("pantheon.run.args") ? project.property("pantheon.run.args").toString().split("\\s+") : []
|
||||
doFirst {
|
||||
applicationDefaultJvmArgs = applicationDefaultJvmArgs.collect{it.replace('PANTHEON_HOME', "$buildDir/pantheon")}
|
||||
}
|
||||
}
|
||||
|
||||
startScripts {
|
||||
doLast {
|
||||
unixScript.text = unixScript.text.replace('PANTHEON_HOME', '\$APP_HOME')
|
||||
windowsScript.text = windowsScript.text.replace('PANTHEON_HOME', '%~dp0..')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':ethereum:evmtool')
|
||||
compile project(':pantheon')
|
||||
errorprone 'com.google.errorprone:error_prone_core:2.3.1'
|
||||
}
|
||||
|
||||
task createEVMToolApp(type: CreateStartScripts) {
|
||||
outputDir = startScripts.outputDir
|
||||
mainClassName = 'net.consensys.pantheon.EVMTool'
|
||||
applicationName = 'evm'
|
||||
classpath = startScripts.classpath
|
||||
}
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from(createEVMToolApp)
|
||||
fileMode = 0755
|
||||
}
|
||||
|
||||
applicationDistribution.into("") { from("LICENSE") }
|
||||
|
||||
distTar {
|
||||
doFirst {
|
||||
delete fileTree(dir: 'build/distributions', include: '*.tar.gz')
|
||||
}
|
||||
compression = Compression.GZIP
|
||||
extension = 'tar.gz'
|
||||
}
|
||||
|
||||
distZip {
|
||||
doFirst {
|
||||
delete fileTree(dir: 'build/distributions', include: '*.zip')
|
||||
}
|
||||
}
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
|
||||
sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
|
||||
classDirectories = files(subprojects.sourceSets.main.output)
|
||||
executionData = files(subprojects.jacocoTestReport.executionData) //how to exclude some package/classes com.test.**
|
||||
reports {
|
||||
xml.enabled true
|
||||
csv.enabled true
|
||||
html.destination file("build/reports/jacocoHtml")
|
||||
}
|
||||
onlyIf = { true }
|
||||
doFirst {
|
||||
executionData = files(executionData.findAll { it.exists() })
|
||||
}
|
||||
}
|
||||
|
||||
configurations { annotationProcessor }
|
||||
|
||||
// Prevent errorprone-checks being dependent upon errorprone-checks!
|
||||
// However, ensure all subprojects comply with the custom rules.
|
||||
configure(subprojects.findAll {it.name != 'errorprone-checks'}) {
|
||||
dependencies { annotationProcessor project(":errorprone-checks") }
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs += [
|
||||
'-processorpath',
|
||||
configurations.annotationProcessor.asPath
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!file("ethereum/referencetests/src/test/resources/README.md").exists()) {
|
||||
throw new GradleException("ethereum/referencetests/src/test/resources/README.md missing: please clone submodules (git submodule update --init --recursive)")
|
||||
}
|
||||
124
buildSrc/src/main/groovy/ProjectPropertiesFile.groovy
Executable file
124
buildSrc/src/main/groovy/ProjectPropertiesFile.groovy
Executable file
@@ -0,0 +1,124 @@
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.Nested
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
class ProjectPropertiesFile extends DefaultTask {
|
||||
|
||||
private String destPackage = ''
|
||||
private String filename = defaultFilename()
|
||||
private List<Property> properties = new ArrayList<>()
|
||||
|
||||
@OutputFile
|
||||
File getOutputFile() {
|
||||
String outputFile = "${project.projectDir}/src/main/java/${packagePath()}/${filename}.java"
|
||||
return project.file(outputFile)
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void generateFile() {
|
||||
getOutputFile().text = generateFileContent()
|
||||
}
|
||||
|
||||
void setDestPackage(String destPackage) {
|
||||
this.destPackage = destPackage
|
||||
}
|
||||
|
||||
@Input
|
||||
String getDestPackage() {
|
||||
return destPackage
|
||||
}
|
||||
|
||||
void setFilename(String filename) {
|
||||
this.filename = filename
|
||||
}
|
||||
|
||||
@Input
|
||||
String getFilename() {
|
||||
return filename
|
||||
}
|
||||
|
||||
void addString(String name, String value) {
|
||||
properties.add(new Property(name, value, PropertyType.STRING))
|
||||
}
|
||||
|
||||
@Nested
|
||||
List<Property> getProperties() {
|
||||
return properties
|
||||
}
|
||||
|
||||
private String packagePath() {
|
||||
return destPackage.replace(".", "/")
|
||||
}
|
||||
|
||||
private String defaultFilename() {
|
||||
return "${project.name.capitalize()}Info"
|
||||
}
|
||||
|
||||
private String generateFileContent() {
|
||||
String[] varDeclarations = properties.stream().map({p -> p.variableDeclaration()}).toArray()
|
||||
String[] methodDeclarations = properties.stream().map({p -> p.methodDeclaration()}).toArray()
|
||||
return """package ${destPackage};
|
||||
|
||||
// This file is generated via a gradle task and should not be edited directly.
|
||||
public class ${filename} {
|
||||
${String.join("\n ", varDeclarations)}
|
||||
|
||||
private ${filename}() {}
|
||||
${String.join("\n", methodDeclarations)}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
private enum PropertyType {
|
||||
STRING("String")
|
||||
|
||||
private final String strVal
|
||||
PropertyType(String strVal) {
|
||||
this.strVal = strVal
|
||||
}
|
||||
|
||||
String toString() {
|
||||
return strVal
|
||||
}
|
||||
}
|
||||
|
||||
private static class Property {
|
||||
private final String name
|
||||
private final String value
|
||||
private final PropertyType type
|
||||
|
||||
Property(name, value, type) {
|
||||
this.name = name
|
||||
this.value = value
|
||||
this.type = type
|
||||
}
|
||||
|
||||
@Input
|
||||
String getName() {
|
||||
return name
|
||||
}
|
||||
|
||||
@Input
|
||||
String getValue() {
|
||||
return value
|
||||
}
|
||||
|
||||
@Input
|
||||
String getType() {
|
||||
return type.toString()
|
||||
}
|
||||
|
||||
String variableDeclaration() {
|
||||
return " private static final ${type} ${name} = \"${value}\";"
|
||||
}
|
||||
|
||||
String methodDeclaration() {
|
||||
return """
|
||||
public static ${type} ${name}() {
|
||||
return ${name};
|
||||
}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
1
consensus/build.gradle
Executable file
1
consensus/build.gradle
Executable file
@@ -0,0 +1 @@
|
||||
jar { enabled = false }
|
||||
28
consensus/clique/build.gradle
Executable file
28
consensus/clique/build.gradle
Executable file
@@ -0,0 +1,28 @@
|
||||
plugins { id 'java' }
|
||||
|
||||
version '1.0.0-SNAPSHOT'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation project(':crypto')
|
||||
implementation project(':ethereum:core')
|
||||
implementation project(':ethereum:eth')
|
||||
implementation project(':ethereum:jsonrpc')
|
||||
implementation project(':ethereum:rlp')
|
||||
implementation project(':ethereum:p2p')
|
||||
implementation project(':services:kvstore')
|
||||
implementation project(':consensus:common')
|
||||
|
||||
implementation 'com.google.guava:guava'
|
||||
implementation 'io.vertx:vertx-core'
|
||||
|
||||
implementation project(':util')
|
||||
testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts')
|
||||
testImplementation group: 'junit', name: 'junit', version: '4.12'
|
||||
testImplementation "org.assertj:assertj-core:3.10.0"
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.headervalidationrules.CliqueDifficultyValidationRule;
|
||||
import net.consensys.pantheon.consensus.clique.headervalidationrules.CliqueExtraDataValidationRule;
|
||||
import net.consensys.pantheon.consensus.clique.headervalidationrules.CoinbaseHeaderValidationRule;
|
||||
import net.consensys.pantheon.consensus.clique.headervalidationrules.SignerRateLimitValidationRule;
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.headervalidationrules.VoteValidationRule;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator;
|
||||
import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.AncestryValidationRule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ConstantFieldValidationRule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasLimitRangeAndDeltaValidationRule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasUsageValidationRule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.TimestampValidationRule;
|
||||
|
||||
public class BlockHeaderValidationRulesetFactory {
|
||||
|
||||
/**
|
||||
* Creates a set of rules which when executed will determine if a given block header is valid with
|
||||
* respect to its parent (or chain).
|
||||
*
|
||||
* <p>Specifically the set of rules provided by this function are to be used for a Clique chain.
|
||||
*
|
||||
* @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks.
|
||||
* @param epochManager an object which determines if a given block is an epoch block.
|
||||
* @return the header validator.
|
||||
*/
|
||||
public static BlockHeaderValidator<CliqueContext> cliqueBlockHeaderValidator(
|
||||
final long secondsBetweenBlocks, final EpochManager epochManager) {
|
||||
|
||||
return new BlockHeaderValidator.Builder<CliqueContext>()
|
||||
.addRule(new AncestryValidationRule())
|
||||
.addRule(new GasUsageValidationRule())
|
||||
.addRule(new GasLimitRangeAndDeltaValidationRule(5000, 0x7fffffffffffffffL))
|
||||
.addRule(new TimestampValidationRule(10, secondsBetweenBlocks))
|
||||
.addRule(new ConstantFieldValidationRule<>("MixHash", BlockHeader::getMixHash, Hash.ZERO))
|
||||
.addRule(
|
||||
new ConstantFieldValidationRule<>(
|
||||
"OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH))
|
||||
.addRule(new CliqueExtraDataValidationRule(epochManager))
|
||||
.addRule(new VoteValidationRule())
|
||||
.addRule(new CliqueDifficultyValidationRule())
|
||||
.addRule(new SignerRateLimitValidationRule())
|
||||
.addRule(new CoinbaseHeaderValidationRule(epochManager))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class CliqueBlockHashing {
|
||||
|
||||
/**
|
||||
* Constructs a hash of the block header, suitable for use when creating the proposer seal. The
|
||||
* extra data is modified to have a null proposer seal and empty list of committed seals.
|
||||
*
|
||||
* @param header The header for which a proposer seal is to be calculated
|
||||
* @param cliqueExtraData The extra data block which is to be inserted to the header once seal is
|
||||
* calculated
|
||||
* @return the hash of the header suitable for signing as the proposer seal
|
||||
*/
|
||||
public static Hash calculateDataHashForProposerSeal(
|
||||
final BlockHeader header, final CliqueExtraData cliqueExtraData) {
|
||||
final BytesValue headerRlp = serializeHeaderWithoutProposerSeal(header, cliqueExtraData);
|
||||
return Hash.hash(headerRlp); // Proposer hash is the hash of the RLP
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers the proposer's {@link Address} from the proposer seal.
|
||||
*
|
||||
* @param header the block header that was signed by the proposer seal
|
||||
* @param cliqueExtraData the parsed CliqueExtraData from the header
|
||||
* @return the proposer address
|
||||
*/
|
||||
public static Address recoverProposerAddress(
|
||||
final BlockHeader header, final CliqueExtraData cliqueExtraData) {
|
||||
if (!cliqueExtraData.getProposerSeal().isPresent()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Supplied cliqueExtraData does not include a proposer " + "seal");
|
||||
}
|
||||
final Hash proposerHash = calculateDataHashForProposerSeal(header, cliqueExtraData);
|
||||
return Util.signatureToAddress(cliqueExtraData.getProposerSeal().get(), proposerHash);
|
||||
}
|
||||
|
||||
private static BytesValue serializeHeaderWithoutProposerSeal(
|
||||
final BlockHeader header, final CliqueExtraData cliqueExtraData) {
|
||||
return serializeHeader(header, () -> encodeExtraDataWithoutProposerSeal(cliqueExtraData));
|
||||
}
|
||||
|
||||
private static BytesValue encodeExtraDataWithoutProposerSeal(
|
||||
final CliqueExtraData cliqueExtraData) {
|
||||
final BytesValue extraDataBytes = cliqueExtraData.encode();
|
||||
// Always trim off final 65 bytes (which maybe zeros)
|
||||
return extraDataBytes.slice(0, extraDataBytes.size() - Signature.BYTES_REQUIRED);
|
||||
}
|
||||
|
||||
private static BytesValue serializeHeader(
|
||||
final BlockHeader header, final Supplier<BytesValue> extraDataSerializer) {
|
||||
final BytesValueRLPOutput out = new BytesValueRLPOutput();
|
||||
out.startList();
|
||||
|
||||
out.writeBytesValue(header.getParentHash());
|
||||
out.writeBytesValue(header.getOmmersHash());
|
||||
out.writeBytesValue(header.getCoinbase());
|
||||
out.writeBytesValue(header.getStateRoot());
|
||||
out.writeBytesValue(header.getTransactionsRoot());
|
||||
out.writeBytesValue(header.getReceiptsRoot());
|
||||
out.writeBytesValue(header.getLogsBloom().getBytes());
|
||||
out.writeUInt256Scalar(header.getDifficulty());
|
||||
out.writeLongScalar(header.getNumber());
|
||||
out.writeLongScalar(header.getGasLimit());
|
||||
out.writeLongScalar(header.getGasUsed());
|
||||
out.writeLongScalar(header.getTimestamp());
|
||||
out.writeBytesValue(extraDataSerializer.get());
|
||||
out.writeBytesValue(header.getMixHash());
|
||||
out.writeLong(header.getNonce());
|
||||
out.endList();
|
||||
return out.encoded();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
|
||||
/**
|
||||
* Holds the data which lives "in parallel" with the importation of blocks etc. when using the
|
||||
* Clique consensus mechanism.
|
||||
*/
|
||||
public class CliqueContext {
|
||||
|
||||
private final VoteTallyCache voteTallyCache;
|
||||
private final VoteProposer voteProposer;
|
||||
|
||||
public CliqueContext(final VoteTallyCache voteTallyCache, final VoteProposer voteProposer) {
|
||||
this.voteTallyCache = voteTallyCache;
|
||||
this.voteProposer = voteProposer;
|
||||
}
|
||||
|
||||
public VoteTallyCache getVoteTallyCache() {
|
||||
return voteTallyCache;
|
||||
}
|
||||
|
||||
public VoteProposer getVoteProposer() {
|
||||
return voteProposer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.mainnet.DifficultyCalculator;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CliqueDifficultyCalculator implements DifficultyCalculator<CliqueContext> {
|
||||
|
||||
private final Address localAddress;
|
||||
|
||||
private final BigInteger IN_TURN_DIFFICULTY = BigInteger.valueOf(2);
|
||||
private final BigInteger OUT_OF_TURN_DIFFICULTY = BigInteger.ONE;
|
||||
|
||||
public CliqueDifficultyCalculator(final Address localAddress) {
|
||||
this.localAddress = localAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger nextDifficulty(
|
||||
final long time, final BlockHeader parent, final ProtocolContext<CliqueContext> context) {
|
||||
|
||||
final Address nextProposer =
|
||||
CliqueHelpers.getProposerForBlockAfter(
|
||||
parent, context.getConsensusState().getVoteTallyCache());
|
||||
return nextProposer.equals(localAddress) ? IN_TURN_DIFFICULTY : OUT_OF_TURN_DIFFICULTY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
import net.consensys.pantheon.util.bytes.BytesValues;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
* Represents the data structure stored in the extraData field of the BlockHeader used when
|
||||
* operating under an Clique consensus mechanism.
|
||||
*/
|
||||
public class CliqueExtraData {
|
||||
|
||||
public static final int EXTRA_VANITY_LENGTH = 32;
|
||||
|
||||
private final BytesValue vanityData;
|
||||
private final List<Address> validators;
|
||||
private final Optional<Signature> proposerSeal;
|
||||
|
||||
public CliqueExtraData(
|
||||
final BytesValue vanityData, final Signature proposerSeal, final List<Address> validators) {
|
||||
|
||||
checkNotNull(vanityData);
|
||||
checkNotNull(validators);
|
||||
checkArgument(vanityData.size() == EXTRA_VANITY_LENGTH);
|
||||
|
||||
this.vanityData = vanityData;
|
||||
this.proposerSeal = Optional.ofNullable(proposerSeal);
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
public static CliqueExtraData decode(final BytesValue input) {
|
||||
if (input.size() < EXTRA_VANITY_LENGTH + Signature.BYTES_REQUIRED) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid BytesValue supplied - too short to produce a valid Clique Extra Data object.");
|
||||
}
|
||||
|
||||
final int validatorByteCount = input.size() - EXTRA_VANITY_LENGTH - Signature.BYTES_REQUIRED;
|
||||
if ((validatorByteCount % Address.SIZE) != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"BytesValue is of invalid size - i.e. contains unused bytes.");
|
||||
}
|
||||
|
||||
final BytesValue vanityData = input.slice(0, EXTRA_VANITY_LENGTH);
|
||||
final List<Address> validators =
|
||||
extractValidators(input.slice(EXTRA_VANITY_LENGTH, validatorByteCount));
|
||||
|
||||
final int proposerSealStartIndex = input.size() - Signature.BYTES_REQUIRED;
|
||||
final Signature proposerSeal = parseProposerSeal(input.slice(proposerSealStartIndex));
|
||||
|
||||
return new CliqueExtraData(vanityData, proposerSeal, validators);
|
||||
}
|
||||
|
||||
private static Signature parseProposerSeal(final BytesValue proposerSealRaw) {
|
||||
return proposerSealRaw.isZero() ? null : Signature.decode(proposerSealRaw);
|
||||
}
|
||||
|
||||
private static List<Address> extractValidators(final BytesValue validatorsRaw) {
|
||||
final List<Address> result = Lists.newArrayList();
|
||||
final int countValidators = validatorsRaw.size() / Address.SIZE;
|
||||
for (int i = 0; i < countValidators; i++) {
|
||||
final int startIndex = i * Address.SIZE;
|
||||
result.add(Address.wrap(validatorsRaw.slice(startIndex, Address.SIZE)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public BytesValue encode() {
|
||||
final BytesValue validatorData = BytesValues.concatenate(validators.toArray(new Address[0]));
|
||||
return BytesValues.concatenate(
|
||||
vanityData,
|
||||
validatorData,
|
||||
proposerSeal
|
||||
.map(Signature::encodedBytes)
|
||||
.orElse(BytesValue.wrap(new byte[Signature.BYTES_REQUIRED])));
|
||||
}
|
||||
|
||||
public BytesValue getVanityData() {
|
||||
return vanityData;
|
||||
}
|
||||
|
||||
public Optional<Signature> getProposerSeal() {
|
||||
return proposerSeal;
|
||||
}
|
||||
|
||||
public List<Address> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.blockcreation.CliqueProposerSelector;
|
||||
import net.consensys.pantheon.consensus.common.ValidatorProvider;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.chain.Blockchain;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CliqueHelpers {
|
||||
|
||||
public static Address getProposerOfBlock(final BlockHeader header) {
|
||||
final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData());
|
||||
return CliqueBlockHashing.recoverProposerAddress(header, extraData);
|
||||
}
|
||||
|
||||
public static List<Address> getValidatorsOfBlock(final BlockHeader header) {
|
||||
final BytesValue extraData = header.getExtraData();
|
||||
final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(extraData);
|
||||
return cliqueExtraData.getValidators();
|
||||
}
|
||||
|
||||
public static Address getProposerForBlockAfter(
|
||||
final BlockHeader parent, final VoteTallyCache voteTallyCache) {
|
||||
final CliqueProposerSelector proposerSelector = new CliqueProposerSelector(voteTallyCache);
|
||||
return proposerSelector.selectProposerForNextBlock(parent);
|
||||
}
|
||||
|
||||
public static boolean addressIsAllowedToProduceNextBlock(
|
||||
final Address candidate,
|
||||
final ProtocolContext<CliqueContext> protocolContext,
|
||||
final BlockHeader parent) {
|
||||
final VoteTally validatorProvider =
|
||||
protocolContext.getConsensusState().getVoteTallyCache().getVoteTallyAtBlock(parent);
|
||||
|
||||
final int minimumUnsignedPastBlocks = minimumBlocksSincePreviousSigning(validatorProvider);
|
||||
|
||||
final Blockchain blockchain = protocolContext.getBlockchain();
|
||||
int unsignedBlockCount = 0;
|
||||
BlockHeader localParent = parent;
|
||||
|
||||
while (unsignedBlockCount < minimumUnsignedPastBlocks) {
|
||||
|
||||
if (localParent.getNumber() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Address parentSigner = CliqueHelpers.getProposerOfBlock(localParent);
|
||||
if (parentSigner.equals(candidate)) {
|
||||
return false;
|
||||
}
|
||||
unsignedBlockCount++;
|
||||
|
||||
localParent =
|
||||
blockchain
|
||||
.getBlockHeader(localParent.getParentHash())
|
||||
.orElseThrow(() -> new IllegalStateException("The block was on a orphaned chain."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int minimumBlocksSincePreviousSigning(final ValidatorProvider validatorProvider) {
|
||||
final int validatorCount = validatorProvider.getCurrentValidators().size();
|
||||
// The number of contiguous blocks in which a signer may only sign 1 (as taken from clique spec)
|
||||
final int signerLimit = (validatorCount / 2) + 1;
|
||||
return signerLimit - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
/** Defines the protocol behaviours for a blockchain using Clique. */
|
||||
public class CliqueProtocolSchedule extends MutableProtocolSchedule<CliqueContext> {
|
||||
|
||||
private static final long DEFAULT_EPOCH_LENGTH = 30_000;
|
||||
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1;
|
||||
private static final int DEFAULT_CHAIN_ID = 4;
|
||||
|
||||
public static ProtocolSchedule<CliqueContext> create(
|
||||
final JsonObject config, final KeyPair nodeKeys) {
|
||||
|
||||
// Get Config Data
|
||||
final Optional<JsonObject> cliqueConfig = Optional.ofNullable(config.getJsonObject("clique"));
|
||||
final long epochLength =
|
||||
cliqueConfig.map(cc -> cc.getLong("epochLength")).orElse(DEFAULT_EPOCH_LENGTH);
|
||||
final long blockPeriod =
|
||||
cliqueConfig
|
||||
.map(cc -> cc.getInteger("blockPeriodSeconds"))
|
||||
.orElse(DEFAULT_BLOCK_PERIOD_SECONDS);
|
||||
final int chainId = config.getInteger("chainId", DEFAULT_CHAIN_ID);
|
||||
|
||||
final MutableProtocolSchedule<CliqueContext> protocolSchedule = new CliqueProtocolSchedule();
|
||||
|
||||
// TODO(tmm) replace address with passed in node data (coming later)
|
||||
final CliqueProtocolSpecs specs =
|
||||
new CliqueProtocolSpecs(
|
||||
blockPeriod,
|
||||
epochLength,
|
||||
chainId,
|
||||
Util.publicKeyToAddress(nodeKeys.getPublicKey()),
|
||||
protocolSchedule);
|
||||
|
||||
protocolSchedule.putMilestone(0, specs.frontier());
|
||||
|
||||
final Long homesteadBlockNumber = config.getLong("homesteadBlock");
|
||||
if (homesteadBlockNumber != null) {
|
||||
protocolSchedule.putMilestone(homesteadBlockNumber, specs.homestead());
|
||||
}
|
||||
|
||||
final Long tangerineWhistleBlockNumber = config.getLong("eip150Block");
|
||||
if (tangerineWhistleBlockNumber != null) {
|
||||
protocolSchedule.putMilestone(tangerineWhistleBlockNumber, specs.tangerineWhistle());
|
||||
}
|
||||
|
||||
final Long spuriousDragonBlockNumber = config.getLong("eip158Block");
|
||||
if (spuriousDragonBlockNumber != null) {
|
||||
protocolSchedule.putMilestone(spuriousDragonBlockNumber, specs.spuriousDragon());
|
||||
}
|
||||
|
||||
final Long byzantiumBlockNumber = config.getLong("byzantiumBlock");
|
||||
if (byzantiumBlockNumber != null) {
|
||||
protocolSchedule.putMilestone(byzantiumBlockNumber, specs.byzantium());
|
||||
}
|
||||
|
||||
return protocolSchedule;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static net.consensys.pantheon.consensus.clique.BlockHeaderValidationRulesetFactory.cliqueBlockHeaderValidator;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.Wei;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockBodyValidator;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockImporter;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSpecBuilder;
|
||||
|
||||
/** Factory for producing Clique protocol specs for given configurations and known fork points */
|
||||
public class CliqueProtocolSpecs {
|
||||
|
||||
private final long secondsBetweenBlocks;
|
||||
private final long epochLength;
|
||||
private final int chainId;
|
||||
private final Address localNodeAddress;
|
||||
private final ProtocolSchedule<CliqueContext> protocolSchedule;
|
||||
|
||||
public CliqueProtocolSpecs(
|
||||
final long secondsBetweenBlocks,
|
||||
final long epochLength,
|
||||
final int chainId,
|
||||
final Address localNodeAddress,
|
||||
final ProtocolSchedule<CliqueContext> protocolSchedule) {
|
||||
this.secondsBetweenBlocks = secondsBetweenBlocks;
|
||||
this.epochLength = epochLength;
|
||||
this.chainId = chainId;
|
||||
this.localNodeAddress = localNodeAddress;
|
||||
this.protocolSchedule = protocolSchedule;
|
||||
}
|
||||
|
||||
public ProtocolSpec<CliqueContext> frontier() {
|
||||
return applyCliqueSpecificModifications(MainnetProtocolSpecs.frontierDefinition());
|
||||
}
|
||||
|
||||
public ProtocolSpec<CliqueContext> homestead() {
|
||||
return applyCliqueSpecificModifications(MainnetProtocolSpecs.homesteadDefinition());
|
||||
}
|
||||
|
||||
public ProtocolSpec<CliqueContext> tangerineWhistle() {
|
||||
return applyCliqueSpecificModifications(MainnetProtocolSpecs.tangerineWhistleDefinition());
|
||||
}
|
||||
|
||||
public ProtocolSpec<CliqueContext> spuriousDragon() {
|
||||
return applyCliqueSpecificModifications(MainnetProtocolSpecs.spuriousDragonDefinition(chainId));
|
||||
}
|
||||
|
||||
public ProtocolSpec<CliqueContext> byzantium() {
|
||||
return applyCliqueSpecificModifications(MainnetProtocolSpecs.byzantiumDefinition(chainId));
|
||||
}
|
||||
|
||||
private ProtocolSpec<CliqueContext> applyCliqueSpecificModifications(
|
||||
final ProtocolSpecBuilder<Void> specBuilder) {
|
||||
final EpochManager epochManager = new EpochManager(epochLength);
|
||||
return specBuilder
|
||||
.<CliqueContext>changeConsensusContextType(
|
||||
difficultyCalculator -> cliqueBlockHeaderValidator(secondsBetweenBlocks, epochManager),
|
||||
MainnetBlockBodyValidator::new,
|
||||
MainnetBlockImporter::new,
|
||||
new CliqueDifficultyCalculator(localNodeAddress))
|
||||
.blockReward(Wei.ZERO)
|
||||
.miningBeneficiaryCalculator(CliqueHelpers::getProposerOfBlock)
|
||||
.build(protocolSchedule);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.consensus.common.VoteType;
|
||||
import net.consensys.pantheon.ethereum.chain.Blockchain;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class CliqueVoteTallyUpdater {
|
||||
|
||||
private static final Logger LOGGER = getLogger();
|
||||
public static final Address NO_VOTE_SUBJECT = Address.wrap(BytesValue.wrap(new byte[20]));
|
||||
|
||||
private final EpochManager epochManager;
|
||||
|
||||
public CliqueVoteTallyUpdater(final EpochManager epochManager) {
|
||||
this.epochManager = epochManager;
|
||||
}
|
||||
|
||||
public VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) {
|
||||
final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber();
|
||||
final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber);
|
||||
LOGGER.info("Loading validator voting state starting from block {}", epochBlockNumber);
|
||||
final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get();
|
||||
final List<Address> initialValidators =
|
||||
CliqueExtraData.decode(epochBlock.getExtraData()).getValidators();
|
||||
final VoteTally voteTally = new VoteTally(initialValidators);
|
||||
for (long blockNumber = epochBlockNumber + 1;
|
||||
blockNumber <= chainHeadBlockNumber;
|
||||
blockNumber++) {
|
||||
updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally);
|
||||
}
|
||||
return voteTally;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the vote tally to reflect changes caused by appending a new block to the chain.
|
||||
*
|
||||
* @param header the header of the block being added
|
||||
* @param voteTally the vote tally to update
|
||||
*/
|
||||
public void updateForBlock(final BlockHeader header, final VoteTally voteTally) {
|
||||
final Address candidate = header.getCoinbase();
|
||||
if (epochManager.isEpochBlock(header.getNumber())) {
|
||||
// epoch blocks are not allowed to include a vote
|
||||
voteTally.discardOutstandingVotes();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!candidate.equals(NO_VOTE_SUBJECT)) {
|
||||
final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData());
|
||||
final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, extraData);
|
||||
voteTally.addVote(proposer, candidate, VoteType.fromNonce(header.getNonce()).get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.ethereum.chain.Blockchain;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
public class VoteTallyCache {
|
||||
|
||||
private final Blockchain blockchain;
|
||||
private final EpochManager epochManager;
|
||||
private final CliqueVoteTallyUpdater voteTallyUpdater;
|
||||
|
||||
private final Cache<Hash, VoteTally> voteTallyCache =
|
||||
CacheBuilder.newBuilder().maximumSize(100).build();
|
||||
|
||||
public VoteTallyCache(
|
||||
final Blockchain blockchain,
|
||||
final CliqueVoteTallyUpdater voteTallyUpdater,
|
||||
final EpochManager epochManager) {
|
||||
checkNotNull(blockchain);
|
||||
checkNotNull(voteTallyUpdater);
|
||||
checkNotNull(epochManager);
|
||||
this.blockchain = blockchain;
|
||||
this.voteTallyUpdater = voteTallyUpdater;
|
||||
this.epochManager = epochManager;
|
||||
}
|
||||
|
||||
public VoteTally getVoteTallyAtBlock(final BlockHeader header) {
|
||||
try {
|
||||
return voteTallyCache.get(header.getHash(), () -> populateCacheUptoAndIncluding(header));
|
||||
} catch (final ExecutionException ex) {
|
||||
throw new RuntimeException("Unable to determine a VoteTally object for the requested block.");
|
||||
}
|
||||
}
|
||||
|
||||
private VoteTally populateCacheUptoAndIncluding(final BlockHeader start) {
|
||||
BlockHeader header = start;
|
||||
final Deque<BlockHeader> intermediateBlocks = new ArrayDeque<>();
|
||||
VoteTally voteTally = null;
|
||||
|
||||
while (true) { // Will run into an epoch block (and thus a VoteTally) to break loop.
|
||||
voteTally = findMostRecentAvailableVoteTally(header, intermediateBlocks);
|
||||
if (voteTally != null) {
|
||||
break;
|
||||
}
|
||||
|
||||
header =
|
||||
blockchain
|
||||
.getBlockHeader(header.getParentHash())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NoSuchElementException(
|
||||
"Supplied block was on a orphaned chain, unable to generate VoteTally."));
|
||||
}
|
||||
return constructMissingCacheEntries(intermediateBlocks, voteTally);
|
||||
}
|
||||
|
||||
private VoteTally findMostRecentAvailableVoteTally(
|
||||
final BlockHeader header, final Deque<BlockHeader> intermediateBlockHeaders) {
|
||||
intermediateBlockHeaders.push(header);
|
||||
VoteTally voteTally = voteTallyCache.getIfPresent(header.getParentHash());
|
||||
if ((voteTally == null) && (epochManager.isEpochBlock(header.getNumber()))) {
|
||||
final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData());
|
||||
voteTally = new VoteTally(extraData.getValidators());
|
||||
}
|
||||
|
||||
return voteTally;
|
||||
}
|
||||
|
||||
private VoteTally constructMissingCacheEntries(
|
||||
final Deque<BlockHeader> headers, final VoteTally tally) {
|
||||
while (!headers.isEmpty()) {
|
||||
final BlockHeader h = headers.pop();
|
||||
voteTallyUpdater.updateForBlock(h, tally);
|
||||
voteTallyCache.put(h.getHash(), tally.copy());
|
||||
}
|
||||
return tally;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueBlockHashing;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueExtraData;
|
||||
import net.consensys.pantheon.crypto.SECP256K1;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.blockcreation.AbstractBlockCreator;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHashFunction;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.core.PendingTransactions;
|
||||
import net.consensys.pantheon.ethereum.core.SealableBlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.ethereum.core.Wei;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public class CliqueBlockCreator extends AbstractBlockCreator<CliqueContext> {
|
||||
|
||||
private final KeyPair nodeKeys;
|
||||
private final ProtocolSchedule<CliqueContext> protocolSchedule;
|
||||
|
||||
public CliqueBlockCreator(
|
||||
final Address coinbase,
|
||||
final ExtraDataCalculator extraDataCalculator,
|
||||
final PendingTransactions pendingTransactions,
|
||||
final ProtocolContext<CliqueContext> protocolContext,
|
||||
final ProtocolSchedule<CliqueContext> protocolSchedule,
|
||||
final Function<Long, Long> gasLimitCalculator,
|
||||
final KeyPair nodeKeys,
|
||||
final Wei minTransactionGasPrice,
|
||||
final BlockHeader parentHeader) {
|
||||
super(
|
||||
coinbase,
|
||||
extraDataCalculator,
|
||||
pendingTransactions,
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
gasLimitCalculator,
|
||||
minTransactionGasPrice,
|
||||
Util.publicKeyToAddress(nodeKeys.getPublicKey()),
|
||||
parentHeader);
|
||||
this.nodeKeys = nodeKeys;
|
||||
this.protocolSchedule = protocolSchedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for signing (hash of) the block (including MixHash and Nonce), and then injecting
|
||||
* the seal into the extraData. This is called after a suitable set of transactions have been
|
||||
* identified, and all resulting hashes have been inserted into the passed-in SealableBlockHeader.
|
||||
*
|
||||
* @param sealableBlockHeader A block header containing StateRoots, TransactionHashes etc.
|
||||
* @return The blockhead which is to be added to the block being proposed.
|
||||
*/
|
||||
@Override
|
||||
protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) {
|
||||
|
||||
final BlockHashFunction blockHashFunction =
|
||||
ScheduleBasedBlockHashFunction.create(protocolSchedule);
|
||||
|
||||
final BlockHeaderBuilder builder =
|
||||
BlockHeaderBuilder.create()
|
||||
.populateFrom(sealableBlockHeader)
|
||||
.mixHash(Hash.ZERO)
|
||||
.nonce(0)
|
||||
.blockHashFunction(blockHashFunction);
|
||||
|
||||
final CliqueExtraData sealedExtraData = constructSignedExtraData(builder.buildBlockHeader());
|
||||
|
||||
// Replace the extraData in the BlockHeaderBuilder, and return header.
|
||||
return builder.extraData(sealedExtraData.encode()).buildBlockHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a CliqueExtraData object with a populated proposerSeal. The signature in the block is
|
||||
* generated from the Hash of the header (minus proposer and committer seals) and the nodeKeys.
|
||||
*
|
||||
* @param headerToSign An almost fully populated header (proposer and committer seals are empty)
|
||||
* @return Extra data containing the same vanity data and validators as extraData, however
|
||||
* proposerSeal will also be populated.
|
||||
*/
|
||||
private CliqueExtraData constructSignedExtraData(final BlockHeader headerToSign) {
|
||||
final CliqueExtraData extraData = CliqueExtraData.decode(headerToSign.getExtraData());
|
||||
final Hash hashToSign =
|
||||
CliqueBlockHashing.calculateDataHashForProposerSeal(headerToSign, extraData);
|
||||
return new CliqueExtraData(
|
||||
extraData.getVanityData(), SECP256K1.sign(hashToSign, nodeKeys), extraData.getValidators());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.ValidatorProvider;
|
||||
import net.consensys.pantheon.ethereum.blockcreation.BaseBlockScheduler;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.util.time.Clock;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
public class CliqueBlockScheduler extends BaseBlockScheduler {
|
||||
|
||||
private final int OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS = 500;
|
||||
|
||||
private final VoteTallyCache voteTallyCache;
|
||||
private final Address localNodeAddress;
|
||||
private final long secondsBetweenBlocks;
|
||||
|
||||
public CliqueBlockScheduler(
|
||||
final Clock clock,
|
||||
final VoteTallyCache voteTallyCache,
|
||||
final Address localNodeAddress,
|
||||
final long secondsBetweenBlocks) {
|
||||
super(clock);
|
||||
this.voteTallyCache = voteTallyCache;
|
||||
this.localNodeAddress = localNodeAddress;
|
||||
this.secondsBetweenBlocks = secondsBetweenBlocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibleForTesting
|
||||
public BlockCreationTimeResult getNextTimestamp(final BlockHeader parentHeader) {
|
||||
final long nextHeaderTimestamp = parentHeader.getTimestamp() + secondsBetweenBlocks;
|
||||
long milliSecondsUntilNextBlock = (nextHeaderTimestamp * 1000) - clock.millisecondsSinceEpoch();
|
||||
|
||||
final CliqueProposerSelector proposerSelector = new CliqueProposerSelector(voteTallyCache);
|
||||
final Address nextSelector = proposerSelector.selectProposerForNextBlock(parentHeader);
|
||||
if (!nextSelector.equals(localNodeAddress)) {
|
||||
milliSecondsUntilNextBlock +=
|
||||
calculatorOutOfTurnDelay(voteTallyCache.getVoteTallyAtBlock(parentHeader));
|
||||
}
|
||||
|
||||
return new BlockCreationTimeResult(
|
||||
nextHeaderTimestamp, Math.max(0, milliSecondsUntilNextBlock));
|
||||
}
|
||||
|
||||
private int calculatorOutOfTurnDelay(final ValidatorProvider validators) {
|
||||
int countSigners = validators.getCurrentValidators().size();
|
||||
int maxDelay = ((countSigners / 2) + 1) * OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS;
|
||||
Random r = new Random();
|
||||
return r.nextInt((maxDelay) + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Responsible for determining which member of the validator pool should create the next block.
|
||||
*
|
||||
* <p>It does this be determining the available validators at the previous block, then selecting the
|
||||
* appropriate validator based on the chain height.
|
||||
*/
|
||||
public class CliqueProposerSelector {
|
||||
|
||||
private final VoteTallyCache voteTallyCache;
|
||||
|
||||
public CliqueProposerSelector(final VoteTallyCache voteTallyCache) {
|
||||
checkNotNull(voteTallyCache);
|
||||
this.voteTallyCache = voteTallyCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which validator should create the block after that supplied.
|
||||
*
|
||||
* @param parentHeader The header of the previously received block.
|
||||
* @return The address of the node which is to propose a block for the provided Round.
|
||||
*/
|
||||
public Address selectProposerForNextBlock(final BlockHeader parentHeader) {
|
||||
|
||||
final VoteTally parentVoteTally = voteTallyCache.getVoteTallyAtBlock(parentHeader);
|
||||
final List<Address> validatorSet = new ArrayList<>(parentVoteTally.getCurrentValidators());
|
||||
|
||||
final long nextBlockNumber = parentHeader.getNumber() + 1L;
|
||||
final int indexIntoValidators = (int) (nextBlockNumber % validatorSet.size());
|
||||
|
||||
return validatorSet.get(indexIntoValidators);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueDifficultyCalculator;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueHelpers;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CliqueDifficultyValidationRule
|
||||
implements AttachedBlockHeaderValidationRule<CliqueContext> {
|
||||
|
||||
@Override
|
||||
public boolean validate(
|
||||
final BlockHeader header,
|
||||
final BlockHeader parent,
|
||||
final ProtocolContext<CliqueContext> protocolContext) {
|
||||
final Address actualBlockCreator = CliqueHelpers.getProposerOfBlock(header);
|
||||
|
||||
final CliqueDifficultyCalculator diffCalculator =
|
||||
new CliqueDifficultyCalculator(actualBlockCreator);
|
||||
final BigInteger expectedDifficulty = diffCalculator.nextDifficulty(0, parent, protocolContext);
|
||||
|
||||
final BigInteger actualDifficulty =
|
||||
new BigInteger(1, header.getDifficulty().getBytes().extractArray());
|
||||
|
||||
return expectedDifficulty.equals(actualDifficulty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueBlockHashing;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueExtraData;
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule;
|
||||
import net.consensys.pantheon.ethereum.rlp.RLPException;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class CliqueExtraDataValidationRule
|
||||
implements AttachedBlockHeaderValidationRule<CliqueContext> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private final EpochManager epochManager;
|
||||
|
||||
public CliqueExtraDataValidationRule(final EpochManager epochManager) {
|
||||
this.epochManager = epochManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for determining the validity of the extra data field. Ensures:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Bytes in the extra data field can be decoded as per Clique specification
|
||||
* <li>Proposer (derived from the proposerSeal) is a member of the validators
|
||||
* <li>Validators are only validated on epoch blocks.
|
||||
* </ul>
|
||||
*
|
||||
* @param header the block header containing the extraData to be validated.
|
||||
* @return True if the extraData successfully produces an CliqueExtraData object, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean validate(
|
||||
final BlockHeader header,
|
||||
final BlockHeader parent,
|
||||
final ProtocolContext<CliqueContext> protocolContext) {
|
||||
try {
|
||||
final VoteTally validatorProvider =
|
||||
protocolContext.getConsensusState().getVoteTallyCache().getVoteTallyAtBlock(parent);
|
||||
|
||||
final Collection<Address> storedValidators = validatorProvider.getCurrentValidators();
|
||||
return extraDataIsValid(storedValidators, header);
|
||||
|
||||
} catch (final RLPException ex) {
|
||||
LOGGER.trace("ExtraData field was unable to be deserialised into an Clique Struct.", ex);
|
||||
return false;
|
||||
} catch (final IllegalArgumentException ex) {
|
||||
LOGGER.trace("Failed to verify extra data", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean extraDataIsValid(
|
||||
final Collection<Address> expectedValidators, final BlockHeader header) {
|
||||
|
||||
final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(header.getExtraData());
|
||||
final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, cliqueExtraData);
|
||||
|
||||
if (!expectedValidators.contains(proposer)) {
|
||||
LOGGER.trace("Proposer sealing block is not a member of the validators.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (epochManager.isEpochBlock(header.getNumber())) {
|
||||
if (!Iterables.elementsEqual(cliqueExtraData.getValidators(), expectedValidators)) {
|
||||
LOGGER.trace(
|
||||
"Incorrect validators. Expected {} but got {}.",
|
||||
expectedValidators,
|
||||
cliqueExtraData.getValidators());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!cliqueExtraData.getValidators().isEmpty()) {
|
||||
LOGGER.trace("Validator list on non-epoch blocks must be empty.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueVoteTallyUpdater;
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule;
|
||||
|
||||
public class CoinbaseHeaderValidationRule implements DetachedBlockHeaderValidationRule {
|
||||
|
||||
private final EpochManager epochManager;
|
||||
|
||||
public CoinbaseHeaderValidationRule(final EpochManager epochManager) {
|
||||
this.epochManager = epochManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
// The coinbase field is used for voting nodes in/out of the validator group. However, no votes
|
||||
// are allowed to be cast on epoch blocks
|
||||
public boolean validate(final BlockHeader header, final BlockHeader parent) {
|
||||
if (epochManager.isEpochBlock(header.getNumber())) {
|
||||
return header.getCoinbase().equals(CliqueVoteTallyUpdater.NO_VOTE_SUBJECT);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueHelpers;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule;
|
||||
|
||||
public class SignerRateLimitValidationRule
|
||||
implements AttachedBlockHeaderValidationRule<CliqueContext> {
|
||||
|
||||
@Override
|
||||
public boolean validate(
|
||||
final BlockHeader header,
|
||||
final BlockHeader parent,
|
||||
final ProtocolContext<CliqueContext> protocolContext) {
|
||||
final Address blockSigner = CliqueHelpers.getProposerOfBlock(header);
|
||||
|
||||
return CliqueHelpers.addressIsAllowedToProduceNextBlock(blockSigner, protocolContext, parent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.jsonrpc.methods.CliqueGetSigners;
|
||||
import net.consensys.pantheon.consensus.clique.jsonrpc.methods.Discard;
|
||||
import net.consensys.pantheon.consensus.clique.jsonrpc.methods.Propose;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.chain.MutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.db.WorldStateArchive;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class CliqueJsonRpcMethodsFactory {
|
||||
|
||||
public Map<String, JsonRpcMethod> methods(final ProtocolContext<CliqueContext> context) {
|
||||
final MutableBlockchain blockchain = context.getBlockchain();
|
||||
final WorldStateArchive worldStateArchive = context.getWorldStateArchive();
|
||||
final BlockchainQueries blockchainQueries =
|
||||
new BlockchainQueries(blockchain, worldStateArchive);
|
||||
final JsonRpcParameter jsonRpcParameter = new JsonRpcParameter();
|
||||
final CliqueGetSigners cliqueGetSigners =
|
||||
new CliqueGetSigners(blockchainQueries, jsonRpcParameter);
|
||||
|
||||
final Propose proposeRpc =
|
||||
new Propose(context.getConsensusState().getVoteProposer(), jsonRpcParameter);
|
||||
final Discard discardRpc =
|
||||
new Discard(context.getConsensusState().getVoteProposer(), jsonRpcParameter);
|
||||
|
||||
final Map<String, JsonRpcMethod> rpcMethods = new HashMap<>();
|
||||
rpcMethods.put(cliqueGetSigners.getName(), cliqueGetSigners);
|
||||
rpcMethods.put(proposeRpc.getName(), proposeRpc);
|
||||
rpcMethods.put(discardRpc.getName(), discardRpc);
|
||||
return rpcMethods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static net.consensys.pantheon.consensus.clique.CliqueHelpers.getValidatorsOfBlock;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class CliqueGetSigners implements JsonRpcMethod {
|
||||
public static final String CLIQUE_GET_SIGNERS = "clique_getSigners";
|
||||
private final BlockchainQueries blockchainQueries;
|
||||
private final JsonRpcParameter parameters;
|
||||
|
||||
public CliqueGetSigners(
|
||||
final BlockchainQueries blockchainQueries, final JsonRpcParameter parameter) {
|
||||
this.blockchainQueries = blockchainQueries;
|
||||
this.parameters = parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return CLIQUE_GET_SIGNERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest request) {
|
||||
final Optional<BlockHeader> blockHeader = blockHeader(request);
|
||||
return blockHeader
|
||||
.<JsonRpcResponse>map(
|
||||
bh -> new JsonRpcSuccessResponse(request.getId(), getValidatorsOfBlock(bh)))
|
||||
.orElse(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
private Optional<BlockHeader> blockHeader(final JsonRpcRequest request) {
|
||||
final Optional<BlockParameter> blockParameter =
|
||||
parameters.optional(request.getParams(), 0, BlockParameter.class);
|
||||
final long latest = blockchainQueries.headBlockNumber();
|
||||
final long blockNumber = blockParameter.map(b -> b.getNumber().orElse(latest)).orElse(latest);
|
||||
return blockchainQueries.blockByNumber(blockNumber).map(BlockWithMetadata::getHeader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static net.consensys.pantheon.consensus.clique.CliqueHelpers.getValidatorsOfBlock;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class CliqueGetSignersAtHash implements JsonRpcMethod {
|
||||
public static final String CLIQUE_GET_SIGNERS_AT_HASH = "clique_getSignersAtHash";
|
||||
private final BlockchainQueries blockchainQueries;
|
||||
private final JsonRpcParameter parameters;
|
||||
|
||||
public CliqueGetSignersAtHash(
|
||||
final BlockchainQueries blockchainQueries, final JsonRpcParameter parameter) {
|
||||
this.blockchainQueries = blockchainQueries;
|
||||
this.parameters = parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return CLIQUE_GET_SIGNERS_AT_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest request) {
|
||||
final Optional<BlockHeader> blockHeader = blockHeader(request);
|
||||
return blockHeader
|
||||
.<JsonRpcResponse>map(
|
||||
bh -> new JsonRpcSuccessResponse(request.getId(), getValidatorsOfBlock(bh)))
|
||||
.orElse(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
private Optional<BlockHeader> blockHeader(final JsonRpcRequest request) {
|
||||
final Hash hash = parameters.required(request.getParams(), 0, Hash.class);
|
||||
return blockchainQueries.blockByHash(hash).map(BlockWithMetadata::getHeader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
public class Discard implements JsonRpcMethod {
|
||||
private static final String CLIQUE_DISCARD = "clique_discard";
|
||||
private final VoteProposer proposer;
|
||||
private final JsonRpcParameter parameters;
|
||||
|
||||
public Discard(final VoteProposer proposer, final JsonRpcParameter parameters) {
|
||||
this.proposer = proposer;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return CLIQUE_DISCARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest request) {
|
||||
final Address address = parameters.required(request.getParams(), 0, Address.class);
|
||||
proposer.discard(address);
|
||||
return new JsonRpcSuccessResponse(request.getId(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
public class Propose implements JsonRpcMethod {
|
||||
private final VoteProposer proposer;
|
||||
private final JsonRpcParameter parameters;
|
||||
|
||||
public Propose(final VoteProposer proposer, final JsonRpcParameter parameters) {
|
||||
this.proposer = proposer;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "clique_propose";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest request) {
|
||||
final Address address = parameters.required(request.getParams(), 0, Address.class);
|
||||
final Boolean auth = parameters.required(request.getParams(), 1, Boolean.class);
|
||||
if (auth) {
|
||||
proposer.auth(address);
|
||||
} else {
|
||||
proposer.drop(address);
|
||||
}
|
||||
// Return true regardless, the vote is always recorded
|
||||
return new JsonRpcSuccessResponse(request.getId(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.core.LogsBloomFilter;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
import net.consensys.pantheon.util.uint.UInt256;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueBlockHashingTest {
|
||||
|
||||
public static class LoadedBlockHeader {
|
||||
private final BlockHeader header;
|
||||
private final Hash parsedBlockHash;
|
||||
|
||||
public LoadedBlockHeader(final BlockHeader header, final Hash parsedBlockHash) {
|
||||
this.header = header;
|
||||
this.parsedBlockHash = parsedBlockHash;
|
||||
}
|
||||
|
||||
public BlockHeader getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
public Hash getParsedBlockHash() {
|
||||
return parsedBlockHash;
|
||||
}
|
||||
}
|
||||
|
||||
private LoadedBlockHeader expectedHeader = null;
|
||||
|
||||
// clique.getSignersAtHash("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e")
|
||||
private static final List<Address> VALIDATORS_IN_HEADER =
|
||||
Arrays.asList(
|
||||
Address.fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"),
|
||||
Address.fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"),
|
||||
Address.fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0"));
|
||||
private static final Hash KNOWN_BLOCK_HASH =
|
||||
Hash.fromHexString("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e");
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
expectedHeader = createKnownHeaderFromCapturedData();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoverProposerAddressFromSeal() {
|
||||
final CliqueExtraData cliqueExtraData =
|
||||
CliqueExtraData.decode(expectedHeader.getHeader().getExtraData());
|
||||
final Address proposerAddress =
|
||||
CliqueBlockHashing.recoverProposerAddress(expectedHeader.getHeader(), cliqueExtraData);
|
||||
|
||||
assertThat(VALIDATORS_IN_HEADER.contains(proposerAddress)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readValidatorListFromExtraData() {
|
||||
final CliqueExtraData cliqueExtraData =
|
||||
CliqueExtraData.decode(expectedHeader.getHeader().getExtraData());
|
||||
assertThat(cliqueExtraData.getValidators()).isEqualTo(VALIDATORS_IN_HEADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateBlockHash() {
|
||||
assertThat(expectedHeader.getHeader().getHash()).isEqualTo(KNOWN_BLOCK_HASH);
|
||||
}
|
||||
|
||||
private LoadedBlockHeader createKnownHeaderFromCapturedData() {
|
||||
// The following text was a dump from the geth console of the 30_000 block on Rinkeby.
|
||||
// eth.getBlock(30000)
|
||||
final BlockHeaderBuilder builder = new BlockHeaderBuilder();
|
||||
builder.difficulty(UInt256.of(2));
|
||||
builder.extraData(
|
||||
BytesValue.fromHexString(
|
||||
"0xd783010600846765746887676f312e372e33856c696e7578000000000000000042eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f0c5bc40d0535af16266714ccb26fc49448c10bdf2969411514707d7442956b3397b09a980f4bea9347f70eea52183326247a0239b6d01fa0b07afc44e8a05463301"));
|
||||
builder.gasLimit(4712388);
|
||||
builder.gasUsed(0);
|
||||
// Do not do Hash.
|
||||
builder.logsBloom(
|
||||
LogsBloomFilter.fromHexString(
|
||||
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"));
|
||||
builder.coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000"));
|
||||
builder.mixHash(
|
||||
Hash.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"));
|
||||
builder.nonce(0);
|
||||
builder.number(30000);
|
||||
builder.parentHash(
|
||||
Hash.fromHexString("0xff570bb9893cb9bac64e346419fb9ad51e203c1cf6da5cfcc0c0dff3351b454b"));
|
||||
builder.receiptsRoot(
|
||||
Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"));
|
||||
builder.ommersHash(
|
||||
Hash.fromHexString("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"));
|
||||
builder.stateRoot(
|
||||
Hash.fromHexString("0x829b58faacea7b0aa625617ba90c93e08150bed160364a7a96505e8205043d34"));
|
||||
builder.timestamp(1492460444);
|
||||
builder.transactionsRoot(
|
||||
Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"));
|
||||
|
||||
final Hash parsedHash =
|
||||
Hash.fromHexString("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e");
|
||||
|
||||
builder.blockHashFunction(MainnetBlockHashFunction::createHash);
|
||||
|
||||
return new LoadedBlockHeader(builder.buildBlockHeader(), parsedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueDifficultyCalculatorTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private Address localAddr;
|
||||
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
private ProtocolContext<CliqueContext> cliqueProtocolContext;
|
||||
private BlockHeaderTestFixture blockHeaderBuilder;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
|
||||
validatorList.add(localAddr);
|
||||
validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1));
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext);
|
||||
blockHeaderBuilder = new BlockHeaderTestFixture();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inTurnValidatorProducesDifficultyOfTwo() {
|
||||
final CliqueDifficultyCalculator calculator = new CliqueDifficultyCalculator(localAddr);
|
||||
|
||||
final BlockHeader parentHeader = blockHeaderBuilder.number(1).buildHeader();
|
||||
|
||||
assertThat(calculator.nextDifficulty(0, parentHeader, cliqueProtocolContext))
|
||||
.isEqualTo(BigInteger.valueOf(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outTurnValidatorProducesDifficultyOfOne() {
|
||||
final CliqueDifficultyCalculator calculator = new CliqueDifficultyCalculator(localAddr);
|
||||
|
||||
final BlockHeader parentHeader = blockHeaderBuilder.number(2).buildHeader();
|
||||
|
||||
assertThat(calculator.nextDifficulty(0, parentHeader, cliqueProtocolContext))
|
||||
.isEqualTo(BigInteger.valueOf(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueExtraDataTest {
|
||||
|
||||
@Test
|
||||
public void encodeAndDecodingDoNotAlterData() {
|
||||
final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0);
|
||||
final List<Address> validators =
|
||||
Arrays.asList(
|
||||
AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), AddressHelpers.ofValue(3));
|
||||
final BytesValue vanityData = BytesValue.fromHexString("11223344", 32);
|
||||
|
||||
final CliqueExtraData extraData = new CliqueExtraData(vanityData, proposerSeal, validators);
|
||||
|
||||
final BytesValue serialisedData = extraData.encode();
|
||||
|
||||
final CliqueExtraData decodedExtraData = CliqueExtraData.decode(serialisedData);
|
||||
|
||||
assertThat(decodedExtraData.getValidators()).isEqualTo(validators);
|
||||
assertThat(decodedExtraData.getProposerSeal().get()).isEqualTo(proposerSeal);
|
||||
assertThat(decodedExtraData.getVanityData()).isEqualTo(vanityData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseRinkebyGenesisBlockExtraData() {
|
||||
// Rinkeby gensis block extra data text found @ rinkeby.io
|
||||
final byte[] genesisBlockExtraData =
|
||||
Hex.decode(
|
||||
"52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData);
|
||||
|
||||
final CliqueExtraData extraData = CliqueExtraData.decode(bufferToInject);
|
||||
assertThat(extraData.getProposerSeal()).isEmpty();
|
||||
assertThat(extraData.getValidators().size()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void insufficientDataResultsInAnIllegalArgumentException() {
|
||||
final BytesValue illegalData =
|
||||
BytesValue.wrap(
|
||||
new byte[Signature.BYTES_REQUIRED + CliqueExtraData.EXTRA_VANITY_LENGTH - 1]);
|
||||
|
||||
assertThatThrownBy(() -> CliqueExtraData.decode(illegalData))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage(
|
||||
"Invalid BytesValue supplied - too short to produce a valid Clique Extra Data object.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sufficientlyLargeButIllegallySizedInputThrowsException() {
|
||||
final BytesValue illegalData =
|
||||
BytesValue.wrap(
|
||||
new byte
|
||||
[Signature.BYTES_REQUIRED
|
||||
+ CliqueExtraData.EXTRA_VANITY_LENGTH
|
||||
+ Address.SIZE
|
||||
- 1]);
|
||||
|
||||
assertThatThrownBy(() -> CliqueExtraData.decode(illegalData))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("BytesValue is of invalid size - i.e. contains unused bytes.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueProtocolScheduleTest {
|
||||
|
||||
@Test
|
||||
public void protocolSpecsAreCreatedAtBlockDefinedInJson() {
|
||||
final String jsonInput =
|
||||
"{\"chainId\": 4,\n"
|
||||
+ "\"homesteadBlock\": 1,\n"
|
||||
+ "\"eip150Block\": 2,\n"
|
||||
+ "\"eip155Block\": 3,\n"
|
||||
+ "\"eip158Block\": 3,\n"
|
||||
+ "\"byzantiumBlock\": 1035301}";
|
||||
|
||||
final JsonObject jsonObject = new JsonObject(jsonInput);
|
||||
|
||||
final ProtocolSchedule<CliqueContext> protocolSchedule =
|
||||
CliqueProtocolSchedule.create(jsonObject, KeyPair.generate());
|
||||
|
||||
final ProtocolSpec<CliqueContext> homesteadSpec = protocolSchedule.getByBlockNumber(1);
|
||||
final ProtocolSpec<CliqueContext> tangerineWhistleSpec = protocolSchedule.getByBlockNumber(2);
|
||||
final ProtocolSpec<CliqueContext> spuriousDragonSpec = protocolSchedule.getByBlockNumber(3);
|
||||
final ProtocolSpec<CliqueContext> byzantiumSpec = protocolSchedule.getByBlockNumber(1035301);
|
||||
|
||||
assertThat(homesteadSpec.equals(tangerineWhistleSpec)).isFalse();
|
||||
assertThat(tangerineWhistleSpec.equals(spuriousDragonSpec)).isFalse();
|
||||
assertThat(spuriousDragonSpec.equals(byzantiumSpec)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.Wei;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueProtocolSpecsTest {
|
||||
|
||||
CliqueProtocolSpecs protocolSpecs =
|
||||
new CliqueProtocolSpecs(
|
||||
15, 30_000, 5, AddressHelpers.ofValue(5), new MutableProtocolSchedule<>());
|
||||
|
||||
@Test
|
||||
public void homsteadParametersAlignWithMainnetWithAdjustments() {
|
||||
final ProtocolSpec<CliqueContext> homestead = protocolSpecs.homestead();
|
||||
|
||||
assertThat(homestead.getName()).isEqualTo("Homestead");
|
||||
assertThat(homestead.getBlockReward()).isEqualTo(Wei.ZERO);
|
||||
assertThat(homestead.getDifficultyCalculator()).isInstanceOf(CliqueDifficultyCalculator.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allSpecsInheritFromMainnetCounterparts() {
|
||||
final ProtocolSchedule<Void> mainnetProtocolSchedule = new MutableProtocolSchedule<>();
|
||||
|
||||
assertThat(protocolSpecs.frontier().getName())
|
||||
.isEqualTo(MainnetProtocolSpecs.frontier(mainnetProtocolSchedule).getName());
|
||||
assertThat(protocolSpecs.homestead().getName())
|
||||
.isEqualTo(MainnetProtocolSpecs.homestead(mainnetProtocolSchedule).getName());
|
||||
assertThat(protocolSpecs.tangerineWhistle().getName())
|
||||
.isEqualTo(MainnetProtocolSpecs.tangerineWhistle(mainnetProtocolSchedule).getName());
|
||||
assertThat(protocolSpecs.spuriousDragon().getName())
|
||||
.isEqualTo(MainnetProtocolSpecs.spuriousDragon(1, mainnetProtocolSchedule).getName());
|
||||
assertThat(protocolSpecs.byzantium().getName())
|
||||
.isEqualTo(MainnetProtocolSpecs.byzantium(1, mainnetProtocolSchedule).getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.consensus.common.VoteType;
|
||||
import net.consensys.pantheon.crypto.SECP256K1;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.chain.MutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueVoteTallyUpdaterTest {
|
||||
|
||||
private static final long EPOCH_LENGTH = 30_000;
|
||||
public static final Signature INVALID_SEAL =
|
||||
Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0);
|
||||
private final VoteTally voteTally = mock(VoteTally.class);
|
||||
private final MutableBlockchain blockchain = mock(MutableBlockchain.class);
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private final Address proposerAddress =
|
||||
Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes()));
|
||||
private final Address subject = Address.fromHexString("007f4a23ca00cd043d25c2888c1aa5688f81a344");
|
||||
private final Address validator1 =
|
||||
Address.fromHexString("00dae27b350bae20c5652124af5d8b5cba001ec1");
|
||||
|
||||
private final CliqueVoteTallyUpdater updater =
|
||||
new CliqueVoteTallyUpdater(new EpochManager(EPOCH_LENGTH));
|
||||
|
||||
@Test
|
||||
public void voteTallyUpdatedWithVoteFromBlock() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(1);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(subject);
|
||||
addProposer(headerBuilder);
|
||||
final BlockHeader header = headerBuilder.buildHeader();
|
||||
|
||||
updater.updateForBlock(header, voteTally);
|
||||
|
||||
verify(voteTally).addVote(proposerAddress, subject, VoteType.ADD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voteTallyNotUpdatedWhenBlockHasNoVoteSubject() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(1);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000"));
|
||||
addProposer(headerBuilder);
|
||||
final BlockHeader header = headerBuilder.buildHeader();
|
||||
|
||||
updater.updateForBlock(header, voteTally);
|
||||
|
||||
verifyZeroInteractions(voteTally);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outstandingVotesDiscardedWhenEpochReached() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(EPOCH_LENGTH);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000"));
|
||||
addProposer(headerBuilder);
|
||||
final BlockHeader header = headerBuilder.buildHeader();
|
||||
|
||||
updater.updateForBlock(header, voteTally);
|
||||
|
||||
verify(voteTally).discardOutstandingVotes();
|
||||
verifyNoMoreInteractions(voteTally);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVoteTallyByExtractingValidatorsFromGenesisBlock() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(0);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000"));
|
||||
addProposer(headerBuilder, asList(subject, validator1));
|
||||
final BlockHeader header = headerBuilder.buildHeader();
|
||||
|
||||
when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH);
|
||||
when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header));
|
||||
|
||||
final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain);
|
||||
assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVoteTallyByExtractingValidatorsFromEpochBlock() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(EPOCH_LENGTH);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000"));
|
||||
addProposer(headerBuilder, asList(subject, validator1));
|
||||
final BlockHeader header = headerBuilder.buildHeader();
|
||||
|
||||
when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH);
|
||||
when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header));
|
||||
|
||||
final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain);
|
||||
assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addVotesFromBlocksAfterMostRecentEpoch() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
headerBuilder.number(EPOCH_LENGTH);
|
||||
headerBuilder.nonce(VoteType.ADD.getNonceValue());
|
||||
headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000"));
|
||||
addProposer(headerBuilder, singletonList(validator1));
|
||||
final BlockHeader epochHeader = headerBuilder.buildHeader();
|
||||
|
||||
headerBuilder.number(EPOCH_LENGTH + 1);
|
||||
headerBuilder.coinbase(subject);
|
||||
final BlockHeader voteBlockHeader = headerBuilder.buildHeader();
|
||||
|
||||
when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH + 1);
|
||||
when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(epochHeader));
|
||||
when(blockchain.getBlockHeader(EPOCH_LENGTH + 1)).thenReturn(Optional.of(voteBlockHeader));
|
||||
|
||||
final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain);
|
||||
assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1);
|
||||
}
|
||||
|
||||
private void addProposer(final BlockHeaderTestFixture builder) {
|
||||
addProposer(builder, singletonList(proposerAddress));
|
||||
}
|
||||
|
||||
private void addProposer(final BlockHeaderTestFixture builder, final List<Address> validators) {
|
||||
|
||||
final CliqueExtraData initialIbftExtraData =
|
||||
new CliqueExtraData(
|
||||
BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]),
|
||||
INVALID_SEAL,
|
||||
validators);
|
||||
|
||||
builder.extraData(initialIbftExtraData.encode());
|
||||
final BlockHeader header = builder.buildHeader();
|
||||
final Hash proposerSealHash =
|
||||
CliqueBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData);
|
||||
|
||||
final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair);
|
||||
|
||||
final CliqueExtraData sealedData =
|
||||
new CliqueExtraData(
|
||||
BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]),
|
||||
proposerSignature,
|
||||
validators);
|
||||
|
||||
builder.extraData(sealedData.encode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import net.consensys.pantheon.crypto.SECP256K1;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TestHelpers {
|
||||
|
||||
public static BlockHeader createCliqueSignedBlockHeader(
|
||||
final BlockHeaderTestFixture blockHeaderBuilder,
|
||||
final KeyPair signer,
|
||||
final List<Address> validators) {
|
||||
|
||||
final CliqueExtraData unsignedExtraData =
|
||||
new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validators);
|
||||
blockHeaderBuilder.extraData(unsignedExtraData.encode());
|
||||
|
||||
final Hash signingHash =
|
||||
CliqueBlockHashing.calculateDataHashForProposerSeal(
|
||||
blockHeaderBuilder.buildHeader(), unsignedExtraData);
|
||||
|
||||
final Signature proposerSignature = SECP256K1.sign(signingHash, signer);
|
||||
|
||||
final CliqueExtraData signedExtraData =
|
||||
new CliqueExtraData(
|
||||
unsignedExtraData.getVanityData(),
|
||||
proposerSignature,
|
||||
unsignedExtraData.getValidators());
|
||||
|
||||
blockHeaderBuilder.extraData(signedExtraData.encode());
|
||||
|
||||
return blockHeaderBuilder.buildHeader();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package net.consensys.pantheon.consensus.clique;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.Block;
|
||||
import net.consensys.pantheon.ethereum.core.BlockBody;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction;
|
||||
import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage;
|
||||
import net.consensys.pantheon.services.kvstore.KeyValueStorage;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
public class VoteTallyCacheTest {
|
||||
|
||||
BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
|
||||
private Block createEmptyBlock(final long blockNumber, final Hash parentHash) {
|
||||
headerBuilder.number(blockNumber).parentHash(parentHash).coinbase(AddressHelpers.ofValue(0));
|
||||
return new Block(
|
||||
headerBuilder.buildHeader(), new BlockBody(Lists.emptyList(), Lists.emptyList()));
|
||||
}
|
||||
|
||||
DefaultMutableBlockchain blockChain;
|
||||
private Block genesisBlock;
|
||||
private Block block_1;
|
||||
private Block block_2;
|
||||
|
||||
@Before
|
||||
public void constructThreeBlockChain() {
|
||||
headerBuilder.extraData(
|
||||
new CliqueExtraData(
|
||||
BytesValue.wrap(new byte[32]),
|
||||
Signature.create(BigInteger.TEN, BigInteger.TEN, (byte) 1),
|
||||
Lists.emptyList())
|
||||
.encode());
|
||||
|
||||
genesisBlock = createEmptyBlock(0, Hash.ZERO);
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
block_1 = createEmptyBlock(1, genesisBlock.getHeader().getHash());
|
||||
block_2 = createEmptyBlock(1, block_1.getHeader().getHash());
|
||||
|
||||
blockChain.appendBlock(block_1, Lists.emptyList());
|
||||
blockChain.appendBlock(block_2, Lists.emptyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parentBlockVoteTallysAreCachedWhenChildVoteTallyRequested() {
|
||||
final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class);
|
||||
final VoteTallyCache cache =
|
||||
new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000));
|
||||
|
||||
// The votetallyUpdater should be invoked for the requested block, and all parents including
|
||||
// the epoch (genesis) block.
|
||||
final ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
cache.getVoteTallyAtBlock(block_2.getHeader());
|
||||
verify(tallyUpdater, times(3)).updateForBlock(varArgs.capture(), any());
|
||||
assertThat(varArgs.getAllValues())
|
||||
.isEqualTo(
|
||||
Arrays.asList(genesisBlock.getHeader(), block_1.getHeader(), block_2.getHeader()));
|
||||
|
||||
reset(tallyUpdater);
|
||||
|
||||
// Requesting the vote tally to the parent block should not invoke the voteTallyUpdater as the
|
||||
// vote tally was cached from previous operation.
|
||||
cache.getVoteTallyAtBlock(block_1.getHeader());
|
||||
verifyZeroInteractions(tallyUpdater);
|
||||
|
||||
cache.getVoteTallyAtBlock(block_2.getHeader());
|
||||
verifyZeroInteractions(tallyUpdater);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionThrownIfNoParentBlockExists() {
|
||||
final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class);
|
||||
final VoteTallyCache cache =
|
||||
new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000));
|
||||
|
||||
final Block orphanBlock = createEmptyBlock(4, Hash.ZERO);
|
||||
|
||||
assertThatExceptionOfType(UncheckedExecutionException.class)
|
||||
.isThrownBy(() -> cache.getVoteTallyAtBlock(orphanBlock.getHeader()))
|
||||
.withMessageContaining(
|
||||
"Supplied block was on a orphaned chain, unable to generate " + "VoteTally.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void walkBackStopsWhenACachedVoteTallyIsFound() {
|
||||
final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class);
|
||||
final VoteTallyCache cache =
|
||||
new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000));
|
||||
|
||||
// Load the Cache up to block_2
|
||||
cache.getVoteTallyAtBlock(block_2.getHeader());
|
||||
|
||||
reset(tallyUpdater);
|
||||
|
||||
// Append new blocks to the chain, and ensure the walkback only goes as far as block_2.
|
||||
final Block block_3 = createEmptyBlock(4, block_2.getHeader().getHash());
|
||||
// Load the Cache up to block_2
|
||||
cache.getVoteTallyAtBlock(block_3.getHeader());
|
||||
|
||||
// The votetallyUpdater should be invoked for the requested block, and all parents including
|
||||
// the epoch (genesis) block.
|
||||
final ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
verify(tallyUpdater, times(1)).updateForBlock(varArgs.capture(), any());
|
||||
assertThat(varArgs.getAllValues()).isEqualTo(Arrays.asList(block_3.getHeader()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueExtraData;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueHelpers;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueProtocolSchedule;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueProtocolSpecs;
|
||||
import net.consensys.pantheon.consensus.clique.TestHelpers;
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.chain.GenesisConfig;
|
||||
import net.consensys.pantheon.ethereum.chain.MutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.Block;
|
||||
import net.consensys.pantheon.ethereum.core.BlockBody;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.PendingTransactions;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.ethereum.core.Wei;
|
||||
import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.db.WorldStateArchive;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule;
|
||||
import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage;
|
||||
import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage;
|
||||
import net.consensys.pantheon.services.kvstore.KeyValueStorage;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueBlockCreatorTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
private final KeyPair otherKeyPair = KeyPair.generate();
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
|
||||
private final Block genesis = GenesisConfig.mainnet().getBlock();
|
||||
private final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
private final MutableBlockchain blockchain =
|
||||
new DefaultMutableBlockchain(genesis, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
private final WorldStateArchive stateArchive =
|
||||
new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage));
|
||||
|
||||
private ProtocolContext<CliqueContext> protocolContext;
|
||||
private final MutableProtocolSchedule<CliqueContext> protocolSchedule =
|
||||
new CliqueProtocolSchedule();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final CliqueProtocolSpecs specs =
|
||||
new CliqueProtocolSpecs(
|
||||
15,
|
||||
30_000,
|
||||
1,
|
||||
Util.publicKeyToAddress(proposerKeyPair.getPublicKey()),
|
||||
protocolSchedule);
|
||||
|
||||
protocolSchedule.putMilestone(0, specs.frontier());
|
||||
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherKeyPair.getPublicKey());
|
||||
validatorList.add(otherAddress);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, new VoteProposer());
|
||||
protocolContext = new ProtocolContext<>(blockchain, stateArchive, cliqueContext);
|
||||
|
||||
// Add a block above the genesis
|
||||
final BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture();
|
||||
headerTestFixture.number(1).parentHash(genesis.getHeader().getHash());
|
||||
final Block emptyBlock =
|
||||
new Block(
|
||||
TestHelpers.createCliqueSignedBlockHeader(
|
||||
headerTestFixture, otherKeyPair, validatorList),
|
||||
new BlockBody(Lists.newArrayList(), Lists.newArrayList()));
|
||||
blockchain.appendBlock(emptyBlock, Lists.newArrayList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proposerAddressCanBeExtractFromAConstructedBlock() {
|
||||
|
||||
final CliqueExtraData extraData =
|
||||
new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validatorList);
|
||||
|
||||
final Address coinbase = AddressHelpers.ofValue(1);
|
||||
final CliqueBlockCreator blockCreator =
|
||||
new CliqueBlockCreator(
|
||||
coinbase,
|
||||
parent -> extraData.encode(),
|
||||
new PendingTransactions(5),
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
gasLimit -> gasLimit,
|
||||
proposerKeyPair,
|
||||
Wei.ZERO,
|
||||
blockchain.getChainHeadHeader());
|
||||
|
||||
final Block createdBlock = blockCreator.createBlock(5L);
|
||||
|
||||
assertThat(CliqueHelpers.getProposerOfBlock(createdBlock.getHeader()))
|
||||
.isEqualTo(proposerAddress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.blockcreation.BaseBlockScheduler.BlockCreationTimeResult;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.util.time.Clock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueBlockSchedulerTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private Address localAddr;
|
||||
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
private VoteTallyCache voteTallyCache;
|
||||
private BlockHeaderTestFixture blockHeaderBuilder;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
|
||||
validatorList.add(localAddr);
|
||||
validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1));
|
||||
|
||||
voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
|
||||
blockHeaderBuilder = new BlockHeaderTestFixture();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inturnValidatorWaitsExactlyBlockInterval() {
|
||||
Clock clock = mock(Clock.class);
|
||||
final long currentSecondsSinceEpoch = 10L;
|
||||
final long secondsBetweenBlocks = 5L;
|
||||
when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000);
|
||||
CliqueBlockScheduler scheduler =
|
||||
new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks);
|
||||
|
||||
// There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore
|
||||
// parent block should be number 1.
|
||||
BlockHeader parentHeader =
|
||||
blockHeaderBuilder.number(1).timestamp(currentSecondsSinceEpoch).buildHeader();
|
||||
|
||||
BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader);
|
||||
|
||||
assertThat(result.getTimestampForHeader())
|
||||
.isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks);
|
||||
assertThat(result.getMillisecondsUntilValid()).isEqualTo(secondsBetweenBlocks * 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outOfturnValidatorWaitsLongerThanBlockInterval() {
|
||||
Clock clock = mock(Clock.class);
|
||||
final long currentSecondsSinceEpoch = 10L;
|
||||
final long secondsBetweenBlocks = 5L;
|
||||
when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000);
|
||||
CliqueBlockScheduler scheduler =
|
||||
new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks);
|
||||
|
||||
// There are 2 validators, therefore block 3 will put localAddr as the out-turn voter, therefore
|
||||
// parent block should be number 2.
|
||||
BlockHeader parentHeader =
|
||||
blockHeaderBuilder.number(2).timestamp(currentSecondsSinceEpoch).buildHeader();
|
||||
|
||||
BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader);
|
||||
|
||||
assertThat(result.getTimestampForHeader())
|
||||
.isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks);
|
||||
assertThat(result.getMillisecondsUntilValid()).isGreaterThan(secondsBetweenBlocks * 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inTurnValidatorCreatesBlockNowIFParentTimestampSufficientlyBehindNow() {
|
||||
Clock clock = mock(Clock.class);
|
||||
final long currentSecondsSinceEpoch = 10L;
|
||||
final long secondsBetweenBlocks = 5L;
|
||||
when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000);
|
||||
CliqueBlockScheduler scheduler =
|
||||
new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks);
|
||||
|
||||
// There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore
|
||||
// parent block should be number 1.
|
||||
BlockHeader parentHeader =
|
||||
blockHeaderBuilder
|
||||
.number(1)
|
||||
.timestamp(currentSecondsSinceEpoch - secondsBetweenBlocks)
|
||||
.buildHeader();
|
||||
|
||||
BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader);
|
||||
|
||||
assertThat(result.getTimestampForHeader()).isEqualTo(currentSecondsSinceEpoch);
|
||||
assertThat(result.getMillisecondsUntilValid()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.consensys.pantheon.consensus.clique.blockcreation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueProposerSelectorTest {
|
||||
|
||||
private final List<Address> validatorList =
|
||||
Arrays.asList(
|
||||
AddressHelpers.ofValue(1),
|
||||
AddressHelpers.ofValue(2),
|
||||
AddressHelpers.ofValue(3),
|
||||
AddressHelpers.ofValue(4));
|
||||
private final VoteTally voteTally = new VoteTally(validatorList);
|
||||
private VoteTallyCache voteTallyCache;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(voteTally);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proposerForABlockIsBasedOnModBlockNumber() {
|
||||
final BlockHeaderTestFixture headerBuilderFixture = new BlockHeaderTestFixture();
|
||||
|
||||
for (int prevBlockNumber = 0; prevBlockNumber < 10; prevBlockNumber++) {
|
||||
headerBuilderFixture.number(prevBlockNumber);
|
||||
final CliqueProposerSelector selector = new CliqueProposerSelector(voteTallyCache);
|
||||
final Address nextProposer =
|
||||
selector.selectProposerForNextBlock(headerBuilderFixture.buildHeader());
|
||||
assertThat(nextProposer)
|
||||
.isEqualTo(validatorList.get((prevBlockNumber + 1) % validatorList.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueBlockHashing;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueExtraData;
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.Signature;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
import net.consensys.pantheon.util.uint.UInt256;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueDifficultyValidationRuleTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
private ProtocolContext<CliqueContext> cliqueProtocolContext;
|
||||
private BlockHeaderTestFixture blockHeaderBuilder;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddress, 1));
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext);
|
||||
blockHeaderBuilder = new BlockHeaderTestFixture();
|
||||
}
|
||||
|
||||
private BlockHeader createCliqueSignedBlock(final BlockHeaderTestFixture blockHeaderBuilder) {
|
||||
|
||||
final CliqueExtraData unsignedExtraData =
|
||||
new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validatorList);
|
||||
blockHeaderBuilder.extraData(unsignedExtraData.encode());
|
||||
|
||||
final Hash signingHash =
|
||||
CliqueBlockHashing.calculateDataHashForProposerSeal(
|
||||
blockHeaderBuilder.buildHeader(), unsignedExtraData);
|
||||
|
||||
final Signature proposerSignature = SECP256K1.sign(signingHash, proposerKeyPair);
|
||||
|
||||
final CliqueExtraData signedExtraData =
|
||||
new CliqueExtraData(
|
||||
unsignedExtraData.getVanityData(),
|
||||
proposerSignature,
|
||||
unsignedExtraData.getValidators());
|
||||
|
||||
blockHeaderBuilder.extraData(signedExtraData.encode());
|
||||
|
||||
return blockHeaderBuilder.buildHeader();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isTrueIfInTurnValidatorSuppliesDifficultyOfTwo() {
|
||||
final long IN_TURN_BLOCK_NUMBER = validatorList.size(); // i.e. proposer is 'in turn'
|
||||
final UInt256 REPORTED_DIFFICULTY = UInt256.of(2);
|
||||
|
||||
blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER - 1L);
|
||||
final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY);
|
||||
final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule();
|
||||
assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isTrueIfOutTurnValidatorSuppliesDifficultyOfOne() {
|
||||
final long OUT_OF_TURN_BLOCK_NUMBER = validatorList.size() - 1L;
|
||||
final UInt256 REPORTED_DIFFICULTY = UInt256.of(1);
|
||||
|
||||
blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER - 1L);
|
||||
final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY);
|
||||
final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule();
|
||||
assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFalseIfOutTurnValidatorSuppliesDifficultyOfTwo() {
|
||||
final long OUT_OF_TURN_BLOCK_NUMBER = validatorList.size() - 1L;
|
||||
final UInt256 REPORTED_DIFFICULTY = UInt256.of(2);
|
||||
|
||||
blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER - 1L);
|
||||
final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY);
|
||||
final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule();
|
||||
assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFalseIfInTurnValidatorSuppliesDifficultyOfOne() {
|
||||
final long IN_TURN_BLOCK_NUMBER = validatorList.size();
|
||||
final UInt256 REPORTED_DIFFICULTY = UInt256.of(1);
|
||||
|
||||
blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER - 1L);
|
||||
final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY);
|
||||
final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder);
|
||||
|
||||
final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule();
|
||||
assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext))
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.CliqueExtraData;
|
||||
import net.consensys.pantheon.consensus.clique.TestHelpers;
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.EpochManager;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CliqueExtraDataValidationRuleTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private Address localAddr;
|
||||
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
private ProtocolContext<CliqueContext> cliqueProtocolContext;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
|
||||
validatorList.add(localAddr);
|
||||
validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1));
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, null);
|
||||
cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingSignerFailsValidation() {
|
||||
final CliqueExtraData extraData =
|
||||
new CliqueExtraData(BytesValue.wrap(new byte[32]), null, Lists.newArrayList());
|
||||
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(1).buildHeader();
|
||||
final BlockHeader child = headerBuilder.number(2).extraData(extraData.encode()).buildHeader();
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
|
||||
assertThat(rule.validate(child, parent, cliqueProtocolContext)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signerNotInExpectedValidatorsFailsValidation() {
|
||||
final KeyPair otherSigner = KeyPair.generate();
|
||||
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(1).buildHeader();
|
||||
headerBuilder.number(2);
|
||||
final BlockHeader badlySignedChild =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, otherSigner, Lists.newArrayList());
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
assertThat(rule.validate(badlySignedChild, parent, cliqueProtocolContext)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signerIsInValidatorsAndValidatorsNotPresentWhenNotEpochIsSuccessful() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(1).buildHeader();
|
||||
headerBuilder.number(2);
|
||||
final BlockHeader correctlySignedChild =
|
||||
TestHelpers.createCliqueSignedBlockHeader(
|
||||
headerBuilder, proposerKeyPair, Lists.newArrayList());
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void epochBlockContainsSameValidatorsAsContextIsSuccessful() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(9).buildHeader();
|
||||
headerBuilder.number(10);
|
||||
final BlockHeader correctlySignedChild =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void epochBlockWithMisMatchingListOfValidatorsFailsValidation() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(9).buildHeader();
|
||||
headerBuilder.number(10);
|
||||
final BlockHeader correctlySignedChild =
|
||||
TestHelpers.createCliqueSignedBlockHeader(
|
||||
headerBuilder,
|
||||
proposerKeyPair,
|
||||
Lists.newArrayList(AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), localAddr));
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonEpochBlockContainingValidatorsFailsValidation() {
|
||||
final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
final BlockHeader parent = headerBuilder.number(8).buildHeader();
|
||||
headerBuilder.number(9);
|
||||
final BlockHeader correctlySignedChild =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final CliqueExtraDataValidationRule rule =
|
||||
new CliqueExtraDataValidationRule(new EpochManager(10));
|
||||
assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package net.consensys.pantheon.consensus.clique.headervalidationrules;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.consensus.clique.CliqueContext;
|
||||
import net.consensys.pantheon.consensus.clique.TestHelpers;
|
||||
import net.consensys.pantheon.consensus.clique.VoteTallyCache;
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteTally;
|
||||
import net.consensys.pantheon.crypto.SECP256K1.KeyPair;
|
||||
import net.consensys.pantheon.ethereum.ProtocolContext;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.AddressHelpers;
|
||||
import net.consensys.pantheon.ethereum.core.Block;
|
||||
import net.consensys.pantheon.ethereum.core.BlockBody;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.core.Util;
|
||||
import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain;
|
||||
import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction;
|
||||
import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage;
|
||||
import net.consensys.pantheon.services.kvstore.KeyValueStorage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SignerRateLimitValidationRuleTest {
|
||||
|
||||
private final KeyPair proposerKeyPair = KeyPair.generate();
|
||||
private final KeyPair otherNodeKeyPair = KeyPair.generate();
|
||||
private final List<Address> validatorList = Lists.newArrayList();
|
||||
private final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture();
|
||||
private ProtocolContext<CliqueContext> cliqueProtocolContext;
|
||||
|
||||
DefaultMutableBlockchain blockChain;
|
||||
private Block genesisBlock;
|
||||
|
||||
private Block createEmptyBlock(final KeyPair blockSigner) {
|
||||
final BlockHeader header =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, blockSigner, validatorList);
|
||||
return new Block(header, new BlockBody(Lists.newArrayList(), Lists.newArrayList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void networkWithOneValidatorIsAllowedToCreateConsecutiveBlocks() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
|
||||
genesisBlock = createEmptyBlock(proposerKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
final BlockHeader nextBlockHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
|
||||
assertThat(
|
||||
validationRule.validate(
|
||||
nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void networkWithTwoValidatorsIsAllowedToProduceBlockIfNotPreviousBlockProposer() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(otherAddress);
|
||||
|
||||
genesisBlock = createEmptyBlock(otherNodeKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
final BlockHeader nextBlockHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
|
||||
assertThat(
|
||||
validationRule.validate(
|
||||
nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void networkWithTwoValidatorsIsNotAllowedToProduceBlockIfIsPreviousBlockProposer() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(otherAddress);
|
||||
|
||||
genesisBlock = createEmptyBlock(proposerKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
headerBuilder.parentHash(genesisBlock.getHash()).number(1);
|
||||
final Block block_1 = createEmptyBlock(proposerKeyPair);
|
||||
blockChain.appendBlock(block_1, Lists.newArrayList());
|
||||
|
||||
headerBuilder.parentHash(block_1.getHeader().getHash()).number(2);
|
||||
final BlockHeader block_2 =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
|
||||
assertThat(validationRule.validate(block_2, block_1.getHeader(), cliqueProtocolContext))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withThreeValidatorsMustHaveOneBlockBetweenSignings() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(otherAddress);
|
||||
validatorList.add(AddressHelpers.ofValue(1));
|
||||
|
||||
genesisBlock = createEmptyBlock(proposerKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
headerBuilder.parentHash(genesisBlock.getHash()).number(1);
|
||||
final Block block_1 = createEmptyBlock(proposerKeyPair);
|
||||
blockChain.appendBlock(block_1, Lists.newArrayList());
|
||||
|
||||
headerBuilder.parentHash(block_1.getHash()).number(2);
|
||||
final Block block_2 = createEmptyBlock(otherNodeKeyPair);
|
||||
blockChain.appendBlock(block_1, Lists.newArrayList());
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
BlockHeader nextBlockHeader;
|
||||
|
||||
// Should not be able to proposer ontop of Block_1 (which has the same sealer)
|
||||
headerBuilder.parentHash(block_1.getHash()).number(2);
|
||||
nextBlockHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
assertThat(validationRule.validate(nextBlockHeader, block_1.getHeader(), cliqueProtocolContext))
|
||||
.isFalse();
|
||||
|
||||
headerBuilder.parentHash(block_1.getHash()).number(3);
|
||||
nextBlockHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
assertThat(validationRule.validate(nextBlockHeader, block_2.getHeader(), cliqueProtocolContext))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signerIsValidIfInsufficientBlocksExistInHistory() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(otherAddress);
|
||||
validatorList.add(AddressHelpers.ofValue(1));
|
||||
|
||||
genesisBlock = createEmptyBlock(otherNodeKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
final BlockHeader nextBlockHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
|
||||
assertThat(
|
||||
validationRule.validate(
|
||||
nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionIsThrownIfOnAnOrphanedChain() {
|
||||
final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey());
|
||||
final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey());
|
||||
validatorList.add(localAddress);
|
||||
validatorList.add(otherAddress);
|
||||
|
||||
genesisBlock = createEmptyBlock(otherNodeKeyPair);
|
||||
|
||||
final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage();
|
||||
blockChain =
|
||||
new DefaultMutableBlockchain(
|
||||
genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash);
|
||||
|
||||
final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class);
|
||||
when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList));
|
||||
final VoteProposer voteProposer = new VoteProposer();
|
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer);
|
||||
cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext);
|
||||
|
||||
headerBuilder.parentHash(Hash.ZERO).number(4);
|
||||
final BlockHeader nextBlock =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList);
|
||||
|
||||
headerBuilder.parentHash(Hash.ZERO).number(3);
|
||||
final BlockHeader parentHeader =
|
||||
TestHelpers.createCliqueSignedBlockHeader(headerBuilder, otherNodeKeyPair, validatorList);
|
||||
|
||||
final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule();
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> validationRule.validate(nextBlock, parentHeader, cliqueProtocolContext))
|
||||
.isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("The block was on a orphaned chain.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static net.consensys.pantheon.ethereum.core.Address.fromHexString;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.assertj.core.api.AssertionsForClassTypes;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CliqueGetSignersAtHashTest {
|
||||
|
||||
private CliqueGetSignersAtHash method;
|
||||
private BlockHeader blockHeader;
|
||||
private List<Address> validators;
|
||||
|
||||
@Mock private BlockchainQueries blockchainQueries;
|
||||
|
||||
@Mock private BlockWithMetadata<TransactionWithMetadata, Hash> blockWithMetadata;
|
||||
public static final String BLOCK_HASH =
|
||||
"0xe36a3edf0d8664002a72ef7c5f8e271485e7ce5c66455a07cb679d855818415f";
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
method = new CliqueGetSignersAtHash(blockchainQueries, new JsonRpcParameter());
|
||||
|
||||
final byte[] genesisBlockExtraData =
|
||||
Hex.decode(
|
||||
"52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
|
||||
final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData);
|
||||
final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture();
|
||||
blockHeader = blockHeaderTestFixture.extraData(bufferToInject).buildHeader();
|
||||
|
||||
validators =
|
||||
asList(
|
||||
fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"),
|
||||
fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"),
|
||||
fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMethodName() {
|
||||
assertThat(method.getName()).isEqualTo("clique_getSignersAtHash");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void failsWhenNoParam() {
|
||||
final JsonRpcRequest request =
|
||||
new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {});
|
||||
|
||||
final Throwable thrown = AssertionsForClassTypes.catchThrowable(() -> method.response(request));
|
||||
|
||||
assertThat(thrown)
|
||||
.isInstanceOf(InvalidJsonRpcParameters.class)
|
||||
.hasMessage("Missing required json rpc parameter at index 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void returnsValidatorsForBlockHash() {
|
||||
final JsonRpcRequest request =
|
||||
new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {BLOCK_HASH});
|
||||
|
||||
when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH)))
|
||||
.thenReturn(Optional.of(blockWithMetadata));
|
||||
when(blockWithMetadata.getHeader()).thenReturn(blockHeader);
|
||||
|
||||
final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request);
|
||||
final List<Address> result = (List<Address>) response.getResult();
|
||||
assertEquals(validators, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failsOnInvalidBlockHash() {
|
||||
final JsonRpcRequest request =
|
||||
new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {BLOCK_HASH});
|
||||
|
||||
when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH)))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request);
|
||||
assertThat(response.getError().name()).isEqualTo(JsonRpcError.INTERNAL_ERROR.name());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static net.consensys.pantheon.ethereum.core.Address.fromHexString;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeader;
|
||||
import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture;
|
||||
import net.consensys.pantheon.ethereum.core.Hash;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import net.consensys.pantheon.util.bytes.BytesValue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CliqueGetSignersTest {
|
||||
private CliqueGetSigners method;
|
||||
private BlockHeader blockHeader;
|
||||
private List<Address> validators;
|
||||
|
||||
@Mock private BlockchainQueries blockchainQueries;
|
||||
|
||||
@Mock private BlockWithMetadata<TransactionWithMetadata, Hash> blockWithMetadata;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
method = new CliqueGetSigners(blockchainQueries, new JsonRpcParameter());
|
||||
|
||||
final byte[] genesisBlockExtraData =
|
||||
Hex.decode(
|
||||
"52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
|
||||
final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData);
|
||||
final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture();
|
||||
blockHeader = blockHeaderTestFixture.extraData(bufferToInject).buildHeader();
|
||||
|
||||
validators =
|
||||
asList(
|
||||
fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"),
|
||||
fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"),
|
||||
fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsMethodName() {
|
||||
assertThat(method.getName()).isEqualTo("clique_getSigners");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void returnsValidatorsWhenNoParam() {
|
||||
final JsonRpcRequest request = new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {});
|
||||
|
||||
when(blockchainQueries.headBlockNumber()).thenReturn(3065995L);
|
||||
when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata));
|
||||
when(blockWithMetadata.getHeader()).thenReturn(blockHeader);
|
||||
|
||||
final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request);
|
||||
final List<Address> result = (List<Address>) response.getResult();
|
||||
assertEquals(validators, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void returnsValidatorsForBlockNumber() {
|
||||
final JsonRpcRequest request =
|
||||
new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x2EC88B"});
|
||||
|
||||
when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata));
|
||||
when(blockWithMetadata.getHeader()).thenReturn(blockHeader);
|
||||
|
||||
final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request);
|
||||
final List<Address> result = (List<Address>) response.getResult();
|
||||
assertEquals(validators, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failsOnInvalidBlockNumber() {
|
||||
final JsonRpcRequest request =
|
||||
new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x1234"});
|
||||
|
||||
when(blockchainQueries.blockByNumber(4660)).thenReturn(Optional.empty());
|
||||
|
||||
final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request);
|
||||
assertThat(response.getError().name()).isEqualTo(JsonRpcError.INTERNAL_ERROR.name());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer.Vote;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class DiscardTest {
|
||||
private final String JSON_RPC_VERSION = "2.0";
|
||||
private final String METHOD = "clique_discard";
|
||||
|
||||
@Test
|
||||
public void discardEmpty() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Discard discard = new Discard(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
final JsonRpcResponse response = discard.response(requestWithParams(a0));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.empty());
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discardAuth() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Discard discard = new Discard(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.auth(a0);
|
||||
|
||||
final JsonRpcResponse response = discard.response(requestWithParams(a0));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.empty());
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discardDrop() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Discard discard = new Discard(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.drop(a0);
|
||||
|
||||
final JsonRpcResponse response = discard.response(requestWithParams(a0));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.empty());
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discardIsolation() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Discard discard = new Discard(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
final Address a1 = Address.fromHexString("1");
|
||||
|
||||
proposer.auth(a0);
|
||||
proposer.auth(a1);
|
||||
|
||||
final JsonRpcResponse response = discard.response(requestWithParams(a0));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.empty());
|
||||
assertThat(proposer.get(a1)).isEqualTo(Optional.of(Vote.AUTH));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discardWithoutAddress() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Discard discard = new Discard(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
assertThatThrownBy(() -> discard.response(requestWithParams()))
|
||||
.hasMessage("Missing required json rpc parameter at index 0")
|
||||
.isInstanceOf(InvalidJsonRpcParameters.class);
|
||||
}
|
||||
|
||||
private JsonRpcRequest requestWithParams(final Object... params) {
|
||||
return new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package net.consensys.pantheon.consensus.clique.jsonrpc.methods;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer;
|
||||
import net.consensys.pantheon.consensus.common.VoteProposer.Vote;
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType;
|
||||
import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ProposeTest {
|
||||
private final String JSON_RPC_VERSION = "2.0";
|
||||
private final String METHOD = "clique_propose";
|
||||
|
||||
@Test
|
||||
public void testAuth() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, true));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDrop() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, false));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatAuth() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.auth(a0);
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, true));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatDrop() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.drop(a0);
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, false));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeToAuth() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.drop(a0);
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, true));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeToDrop() {
|
||||
final VoteProposer proposer = new VoteProposer();
|
||||
final Propose propose = new Propose(proposer, new JsonRpcParameter());
|
||||
final Address a0 = Address.fromHexString("0");
|
||||
|
||||
proposer.auth(a0);
|
||||
final JsonRpcResponse response = propose.response(requestWithParams(a0, false));
|
||||
|
||||
assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP));
|
||||
assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS);
|
||||
final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response;
|
||||
assertThat(successResponse.getResult()).isEqualTo(true);
|
||||
}
|
||||
|
||||
private JsonRpcRequest requestWithParams(final Object... params) {
|
||||
return new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params);
|
||||
}
|
||||
}
|
||||
20
consensus/common/build.gradle
Executable file
20
consensus/common/build.gradle
Executable file
@@ -0,0 +1,20 @@
|
||||
apply plugin: 'java-library'
|
||||
|
||||
jar {
|
||||
baseName 'pantheon-consensus-common'
|
||||
manifest {
|
||||
attributes('Implementation-Title': baseName,
|
||||
'Implementation-Version': project.version)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':ethereum:core')
|
||||
implementation project(':util')
|
||||
implementation 'com.google.guava:guava'
|
||||
|
||||
testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts')
|
||||
testImplementation 'junit:junit'
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.consensys.pantheon.consensus.common;
|
||||
|
||||
public class EpochManager {
|
||||
|
||||
private final long epochLengthInBlocks;
|
||||
|
||||
public EpochManager(final long epochLengthInBlocks) {
|
||||
this.epochLengthInBlocks = epochLengthInBlocks;
|
||||
}
|
||||
|
||||
public boolean isEpochBlock(final long blockNumber) {
|
||||
return (blockNumber % epochLengthInBlocks) == 0;
|
||||
}
|
||||
|
||||
public long getLastEpochBlock(final long blockNumber) {
|
||||
return blockNumber - (blockNumber % epochLengthInBlocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.consensys.pantheon.consensus.common;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface ValidatorProvider {
|
||||
|
||||
// Returns the current list of validators
|
||||
Collection<Address> getCurrentValidators();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.consensys.pantheon.consensus.common;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Container for pending votes and selecting a vote for new blocks */
|
||||
public class VoteProposer {
|
||||
public enum Vote {
|
||||
AUTH,
|
||||
DROP
|
||||
}
|
||||
|
||||
private final Map<Address, Vote> proposals = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger votePosition = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* Identifies an address that should be voted into the validator pool
|
||||
*
|
||||
* @param address The address to be voted in
|
||||
*/
|
||||
public void auth(final Address address) {
|
||||
proposals.put(address, Vote.AUTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies an address that should be voted out of the validator pool
|
||||
*
|
||||
* @param address The address to be voted out
|
||||
*/
|
||||
public void drop(final Address address) {
|
||||
proposals.put(address, Vote.DROP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards a pending vote for an address if one exists
|
||||
*
|
||||
* @param address The address that should no longer be voted for
|
||||
*/
|
||||
public void discard(final Address address) {
|
||||
proposals.remove(address);
|
||||
}
|
||||
|
||||
/** Discards all pending votes */
|
||||
public void clear() {
|
||||
proposals.clear();
|
||||
}
|
||||
|
||||
public Optional<Vote> get(final Address address) {
|
||||
return Optional.ofNullable(proposals.get(address));
|
||||
}
|
||||
|
||||
private boolean voteNotYetCast(
|
||||
final Address localAddress,
|
||||
final Address voteAddress,
|
||||
final Vote vote,
|
||||
final Collection<Address> validators,
|
||||
final VoteTally tally) {
|
||||
|
||||
// Pre evaluate if we have a vote outstanding to auth or drop the target address
|
||||
final boolean votedAuth = tally.getOutstandingAddVotesFor(voteAddress).contains(localAddress);
|
||||
final boolean votedDrop =
|
||||
tally.getOutstandingRemoveVotesFor(voteAddress).contains(localAddress);
|
||||
|
||||
// if they're a validator, we want to see them dropped, and we haven't voted to drop them yet
|
||||
if (validators.contains(voteAddress) && !votedDrop && vote == Vote.DROP) {
|
||||
return true;
|
||||
// or if we've previously voted to auth them and we want to drop them
|
||||
} else if (votedAuth && vote == Vote.DROP) {
|
||||
return true;
|
||||
// if they're not currently a validator and we want to see them authed and we haven't voted to
|
||||
// auth them yet
|
||||
} else if (!validators.contains(voteAddress) && !votedAuth && vote == Vote.AUTH) {
|
||||
return true;
|
||||
// or if we've previously voted to drop them and we want to see them authed
|
||||
} else if (votedDrop && vote == Vote.AUTH) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a valid vote from our list of pending votes
|
||||
*
|
||||
* @param localAddress The address of this validator node
|
||||
* @param tally the vote tally at the height of the chain we need a vote for
|
||||
* @return Either an address with the vote (auth or drop) or no vote if we have no valid pending
|
||||
* votes
|
||||
*/
|
||||
public Optional<Map.Entry<Address, Vote>> getVote(
|
||||
final Address localAddress, final VoteTally tally) {
|
||||
final Collection<Address> validators = tally.getCurrentValidators();
|
||||
final List<Map.Entry<Address, Vote>> validVotes = new ArrayList<>();
|
||||
|
||||
proposals
|
||||
.entrySet()
|
||||
.forEach(
|
||||
proposal -> {
|
||||
if (voteNotYetCast(
|
||||
localAddress, proposal.getKey(), proposal.getValue(), validators, tally)) {
|
||||
validVotes.add(proposal);
|
||||
}
|
||||
});
|
||||
|
||||
if (validVotes.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Get the next position in the voting queue we should propose
|
||||
final int currentVotePosition = votePosition.updateAndGet(i -> ++i % validVotes.size());
|
||||
|
||||
// Get a vote from the valid votes and return it
|
||||
return Optional.of(validVotes.get(currentVotePosition));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package net.consensys.pantheon.consensus.common;
|
||||
|
||||
import net.consensys.pantheon.ethereum.core.Address;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/** Tracks the current list of validators and votes to add or drop validators. */
|
||||
public class VoteTally implements ValidatorProvider {
|
||||
|
||||
private final SortedSet<Address> currentValidators;
|
||||
|
||||
private final Map<Address, Set<Address>> addVotesBySubject;
|
||||
private final Map<Address, Set<Address>> removeVotesBySubject;
|
||||
|
||||
public VoteTally(final List<Address> initialValidators) {
|
||||
this(new TreeSet<>(initialValidators), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
|
||||
private VoteTally(
|
||||
final Collection<Address> initialValidators,
|
||||
final Map<Address, Set<Address>> addVotesBySubject,
|
||||
final Map<Address, Set<Address>> removeVotesBySubject) {
|
||||
this.currentValidators = new TreeSet<>(initialValidators);
|
||||
this.addVotesBySubject = addVotesBySubject;
|
||||
this.removeVotesBySubject = removeVotesBySubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a vote to the current tally. The current validator list will be updated if this vote takes
|
||||
* the tally past the required votes to approve the change.
|
||||
*
|
||||
* @param proposer the address of the validator casting the vote via block proposal
|
||||
* @param subject the validator the vote is about
|
||||
* @param voteType the type of vote, either add or drop
|
||||
*/
|
||||
public void addVote(final Address proposer, final Address subject, final VoteType voteType) {
|
||||
final Set<Address> addVotesForSubject =
|
||||
addVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>());
|
||||
final Set<Address> removeVotesForSubject =
|
||||
removeVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>());
|
||||
|
||||
if (voteType == VoteType.ADD) {
|
||||
addVotesForSubject.add(proposer);
|
||||
removeVotesForSubject.remove(proposer);
|
||||
} else {
|
||||
removeVotesForSubject.add(proposer);
|
||||
addVotesForSubject.remove(proposer);
|
||||
}
|
||||
|
||||
final int validatorLimit = validatorLimit();
|
||||
if (addVotesForSubject.size() >= validatorLimit) {
|
||||
currentValidators.add(subject);
|
||||
discardOutstandingVotesFor(subject);
|
||||
}
|
||||
if (removeVotesForSubject.size() >= validatorLimit) {
|
||||
currentValidators.remove(subject);
|
||||
discardOutstandingVotesFor(subject);
|
||||
addVotesBySubject.values().forEach(votes -> votes.remove(subject));
|
||||
removeVotesBySubject.values().forEach(votes -> votes.remove(subject));
|
||||
}
|
||||
}
|
||||
|
||||
private void discardOutstandingVotesFor(final Address subject) {
|
||||
addVotesBySubject.remove(subject);
|
||||
removeVotesBySubject.remove(subject);
|
||||
}
|
||||
|
||||
public Set<Address> getOutstandingAddVotesFor(final Address subject) {
|
||||
return Optional.ofNullable(addVotesBySubject.get(subject)).orElse(Collections.emptySet());
|
||||
}
|
||||
|
||||
public Set<Address> getOutstandingRemoveVotesFor(final Address subject) {
|
||||
return Optional.ofNullable(removeVotesBySubject.get(subject)).orElse(Collections.emptySet());
|
||||
}
|
||||
|
||||
private int validatorLimit() {
|
||||
return (currentValidators.size() / 2) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the outstanding vote tallies as required at each epoch. The current validator list is
|
||||
* unaffected.
|
||||
*/
|
||||
public void discardOutstandingVotes() {
|
||||
addVotesBySubject.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Address> getCurrentValidators() {
|
||||
return currentValidators;
|
||||
}
|
||||
|
||||
public VoteTally copy() {
|
||||
final Map<Address, Set<Address>> addVotesBySubject = Maps.newHashMap();
|
||||
final Map<Address, Set<Address>> removeVotesBySubject = Maps.newHashMap();
|
||||
|
||||
this.addVotesBySubject.forEach(
|
||||
(key, value) -> addVotesBySubject.put(key, new TreeSet<>(value)));
|
||||
this.removeVotesBySubject.forEach(
|
||||
(key, value) -> removeVotesBySubject.put(key, new TreeSet<>(value)));
|
||||
|
||||
return new VoteTally(
|
||||
new TreeSet<>(this.currentValidators), addVotesBySubject, removeVotesBySubject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.consensys.pantheon.consensus.common;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum VoteType {
|
||||
ADD(0x0L),
|
||||
DROP(0xFFFFFFFFFFFFFFFFL);
|
||||
|
||||
private final long nonceValue;
|
||||
|
||||
VoteType(final long nonceValue) {
|
||||
this.nonceValue = nonceValue;
|
||||
}
|
||||
|
||||
public long getNonceValue() {
|
||||
return nonceValue;
|
||||
}
|
||||
|
||||
public static Optional<VoteType> fromNonce(final long nonce) {
|
||||
for (final VoteType voteType : values()) {
|
||||
if (Long.compareUnsigned(voteType.nonceValue, nonce) == 0) {
|
||||
return Optional.of(voteType);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user