Initial commit

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
This commit is contained in:
PegaSys Admin
2018-10-09 15:17:20 +00:00
commit 7dfc2e4085
1319 changed files with 168036 additions and 0 deletions

4
.dockerignore Executable file
View File

@@ -0,0 +1,4 @@
.gradle
.idea
.vertx
build

5
.gitattributes vendored Executable file
View File

@@ -0,0 +1,5 @@
* text eol=lf
*.jar -text
*.bat -text
*.pcap binary
*.blocks binary

27
.gitignore vendored Executable file
View 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
View 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
View 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 codes 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
View 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
View 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
View 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
View File

@@ -0,0 +1,126 @@
# Pantheon Ethereum Client &middot; [![Build Status](https://circleci.com/gh/ConsenSys/pantheon.svg?style=shield&circle-token=fe99ba1f7e99c65632a1b1ae69a821ef52ee9bc4)](https://circleci.com/gh/ConsenSys/pantheon) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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
View 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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
});
}
}

View File

@@ -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;
}
});
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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"}]

View File

@@ -0,0 +1 @@
608060405234801561001057600080fd5b5060008054600160a060020a03191633179055610187806100326000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fa4f245811461005b5780636057361d1461008257806367e404ce1461009c575b600080fd5b34801561006757600080fd5b506100706100da565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061009a6004356100e0565b005b3480156100a857600080fd5b506100b161013f565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b60025490565b604080513381526020810183905281517fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5929181900390910190a16002556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a72305820f958aea7922a9538be4c34980ad3171806aad2d3fedb62682cef2ca4e1f1f3120029

View File

@@ -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;
}
}

View 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>

View 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)
```

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,5 @@
var Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};

View File

@@ -0,0 +1,5 @@
var Adoption = artifacts.require("Adoption");
module.exports = function(deployer) {
deployer.deploy(Adoption);
};

View 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");
}
}

View 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');
});
});
});

View 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)');
});
});
});

View 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
View 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)")
}

View 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
View File

@@ -0,0 +1 @@
jar { enabled = false }

28
consensus/clique/build.gradle Executable file
View 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'
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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.");
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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.");
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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
View 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'
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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