diff --git a/.gitignore b/.gitignore index 197e472e0..8745da007 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ build endtoend/node_modules +endtoend/results + diff --git a/Dockerfile b/Dockerfile index 4f46e6b88..49d1f285c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -#FROM openjdk:11 +#FROM openjdk:11-slim FROM adoptopenjdk/openjdk11:x86_64-alpine-jdk-11.0.9_11-slim as build-stage WORKDIR /usr/src/minima/ COPY gradle gradle COPY gradlew settings.gradle build.gradle ./ WORKDIR /usr/src/minima/ # Call gradlew before copying the source code to only download the gradle distribution once (layer will be cached) -#RUN ./gradlew --no-daemon -v +RUN ./gradlew --no-daemon -v COPY lib lib COPY src src COPY test test @@ -18,6 +18,9 @@ RUN ls -l build/libs/* RUN stat build/libs/minima-all.jar #RUN tar -cf minimajar.tar build/libs/minima-all.jar +#FROM openjdk:11-slim as production-stage +#RUN apt-get update +#RUN apt-get install -y curl FROM adoptopenjdk/openjdk11:x86_64-alpine-jdk-11.0.9_11-slim as production-stage RUN apk --no-cache add curl COPY --from=build-stage /usr/src/minima/build/libs/minima-all.jar /opt/minima/minima.jar diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 index ffea53f0d..0da55353e 100644 --- a/Dockerfile.arm64v8 +++ b/Dockerfile.arm64v8 @@ -1,4 +1,4 @@ -#FROM openjdk:11 +#FROM openjdk:11 as build-stage #FROM adoptopenjdk/openjdk11:x86_64-alpine-jdk-11.0.9_11-slim as build-stage FROM arm64v8/adoptopenjdk:11-jdk-hotspot-focal as build-stage WORKDIR /usr/src/minima/ @@ -6,7 +6,7 @@ COPY gradle gradle COPY gradlew settings.gradle build.gradle ./ WORKDIR /usr/src/minima/ # Call gradlew before copying the source code to only download the gradle distribution once (layer will be cached) -#RUN ./gradlew --no-daemon -v +RUN ./gradlew --no-daemon -v COPY lib lib COPY src src COPY test test @@ -19,8 +19,12 @@ RUN ls -l build/libs/* RUN stat build/libs/minima-all.jar #RUN tar -cf minimajar.tar build/libs/minima-all.jar +# no alpine multi arch images yet, so we build with 11-jdk-hotspot-focal to build a 500 MB image (:11 leads to 700 MB image) +#FROM openjdk:11 as production-stage #FROM adoptopenjdk/openjdk11:x86_64-alpine-jdk-11.0.9_11-slim as production-stage FROM arm64v8/adoptopenjdk:11-jdk-hotspot-focal as production-stage +#COPY --from=build-stage /usr/src/minima/build/resources/main/log4j2.xml /opt/minima/log4j2.xml +COPY --from=build-stage /usr/src/minima/src/resources/log4j2.xml /opt/minima/log4j2.xml COPY --from=build-stage /usr/src/minima/build/libs/minima-all.jar /opt/minima/minima.jar #COPY --from=build-stage /usr/src/minima/minimajar.tar /opt/minima/minimajar.tar WORKDIR /opt/minima @@ -33,4 +37,5 @@ RUN ls -l *.jar RUN stat minima.jar # 9001 minima protocol 9002 REST 9003 WebSocket 9004 MiniDapp Server EXPOSE 9001 9002 9003 9004 -ENTRYPOINT ["java", "-jar", "minima.jar"] +#ENTRYPOINT ["java", "-jar", "minima.jar"] +ENTRYPOINT ["java", "-Dlog4j.configurationFile=log4j2.xml", "-jar", "minima.jar"] diff --git a/build.gradle b/build.gradle index 2e7d79de2..b1a7291a7 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,9 @@ plugins { // Apply the java plugin to add support for Java id 'java' + // Support for kotlin (used by libraries) + id 'org.jetbrains.kotlin.jvm' version '1.4.20' + // Apply the application plugin to add support for building a CLI application. id 'application' @@ -28,8 +31,17 @@ repositories { // Use jcenter for resolving dependencies. // You can declare any Maven/Ivy/file repository here. jcenter() + + maven { + url "https://dl.cloudsmith.io/public/libp2p/jvm-libp2p/maven/" + } + maven { + url "https://artifacts.consensys.net/public/maven/maven/" + } + } + dependencies { // This dependency is used by the application. implementation 'com.google.guava:guava:29.0-jre' @@ -41,6 +53,44 @@ dependencies { implementation 'com.jcraft:jsch:0.1.55' + // build and install locally for Apple Silicon + implementation 'com.google.protobuf:protobuf-java:3.11.0' + implementation("org.bouncycastle:bcprov-jdk15on:1.62") + implementation("org.bouncycastle:bcpkix-jdk15on:1.62") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1") + + //implementation('io.libp2p:jvm-libp2p-minimal:0.8.1-RELEASE') + implementation 'io.libp2p:jvm-libp2p-minimal:0.8.1-RELEASE' + // implementation 'io.libp2p:jvm-libp2p-minimal' +// compile files('libs/jvm-libp2p-minimal-0.8.0-RELEASE.jar') + + api("io.netty:netty-all:4.1.36.Final") + + implementation("org.apache.logging.log4j:log4j-api:2.11.2") + implementation("org.apache.logging.log4j:log4j-core:2.11.2") + + // below is needed to actually capture the log messages from log4j + implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.0-alpha1' // '1.7.30' + + //implementation("org.slf4j:slf4j-api:2.0.0-alpha1") + + // Bytes object needed for collections such as KVS + implementation("org.apache.tuweni:bytes:1.3.0") + // commons-lang3 is needed for Pair<,> in ssz + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' + // not sure we need the below, as we imported teku fork of tuweni ssz... But maybe we could switch to it. + implementation group: 'org.apache.tuweni', name: 'tuweni-ssz', version: '1.1.0', ext: 'pom' + // https://mvnrepository.com/artifact/org.apache.tuweni/tuweni-units - needed for eth discovery v5 lib (tech.pegasys.discovery) + //implementation group: 'org.apache.tuweni', name: 'tuweni-units', version: '1.1.0', ext: 'pom' + implementation("org.apache.tuweni:units:1.3.0") + // awaitility is used by Waiter.java + implementation group: 'org.awaitility', name: 'awaitility', version: '4.0.3' + + implementation 'tech.pegasys.discovery:discovery:0.4.6' + // implementation 'tech.pegasys.discovery:discovery:0.4.3-dev-57c2fd81' + + implementation 'com.h2database:h2:1.4.200' // implementation group: 'com.h2database', name: 'h2', version: '1.4.200' @@ -76,9 +126,26 @@ application { //mainClass.set("org.minima.Start") // new syntax for gradle 6.7+, plugins not compatible yet } +// alternative main methods - see https://stackoverflow.com/questions/43937169/gradle-application-plugin-with-multiple-main-classes/46938169 + +// define a task for any alternative static void main method - first arg is gradle task name: ./gradlew runp2p +task(runp2p, dependsOn: 'classes', type: JavaExec) { + main = 'org.minima.system.network.base.P2PStart' // org/minima/system/network/base/P2PStart + classpath = sourceSets.main.runtimeClasspath + // args ''/ip4/127.0.0.1/tcp/63407/ipfs/QmRGduDqyGXEsAGxAw9gM6tZrJbg1NEKSvmqwnjQqKwRVk' // use this line to hardcode args + // systemProperty 'simple.message', 'Hello ' + systemProperty 'log4j2.debug', 'false' +// systemProperty 'log4j.configurationFile', 'log4j2-p2p.properties' + systemProperty 'log4j.configurationFile', 'log4j2.xml' +// use below to log to file +// systemProperty "log4j.configurationFile", "log4j2-test-discovery.xml" + +} + +// needed for log4j to work with fatjar jar { manifest { attributes 'Main-Class': 'org.minima.Start' + attributes "Multi-Release": true } } - diff --git a/endtoend/Dockerfile b/endtoend/Dockerfile index f25830f60..5319cc694 100644 --- a/endtoend/Dockerfile +++ b/endtoend/Dockerfile @@ -1,5 +1,5 @@ #FROM keymetrics/pm2:latest-alpine -FROM node:15-alpine +FROM node:16-alpine #FROM node:10 #FROM mhart/alpine-node:15 @@ -7,11 +7,19 @@ WORKDIR /app/ # install some packages on Alpine RUN apk --no-cache add \ - python \ - bash + python2 \ + bash \ + build-base \ + g++ \ + cairo-dev \ + jpeg-dev \ + pango-dev \ + giflib-dev + +RUN apk add --update --repository http://dl-3.alpinelinux.org/alpine/edge/testing libmount ttf-dejavu ttf-droid ttf-freefont ttf-liberation ttf-ubuntu-font-family fontconfig # Copy nodejs package files -COPY package-lock.json package.json /app/ +COPY package-lock.json package.json minima-api-1.0.0.tgz /app/ # Install packages #ENV NPM_CONFIG_LOGLEVEL warn @@ -26,4 +34,17 @@ COPY src /app/src CMD ["node", "src/index.js"] +ARG topology=star +ARG nbNodes=3 +ARG nodeFailure=2 +ARG graph=false + +ENV topology=$topology +ENV nbNodes=$nbNodes +ENV nodeFailure=$nodeFailure +ENV graph=$graph +RUN echo $topology +RUN echo $nbNodes +RUN echo $nodeFailure +RUN echo $graph diff --git a/endtoend/README.md b/endtoend/README.md index 27e0cb578..5498ca30f 100644 --- a/endtoend/README.md +++ b/endtoend/README.md @@ -7,7 +7,7 @@ Quick Start cd minima_root_dir docker network create minima-e2e-testnet docker build -t minima:latest . # OR on ARM: docker build -t minima:latest -f Dockerfile.arm64v8 . - cd end2end + cd endtoend docker build -t minima-e2e . && docker run -v /var/run/docker.sock:/var/run/docker.sock --network minima-e2e-testnet minima-e2e Setup @@ -19,15 +19,14 @@ build minima docker image (or pull it) - default image used is minima:latest #ARM: docker build -t minima:latest -f Dockerfile.arm64v8 . build nodejs tests docker image (same for ARM and x64): - cd end2end - docker build -t minima-e2e . + docker build -t minima-e2e endtoend stop all running docker images (useful to stop instances manually, otherwise script stops automatically old instances at restart): docker stop $(docker ps -a -q) run docker instance to create network and perform network connectivity check (requires at least one connection): - docker run -v /var/run/docker.sock:/var/run/docker.sock --network minima-e2e-testnet minima-e2e + docker run -v /var/run/docker.sock:/var/run/docker.sock --env nbNodes=3 --network minima-e2e-testnet minima-e2e All in one: - docker build -t minima-e2e . && docker run -v /var/run/docker.sock:/var/run/docker.sock --network minima-e2e-testnet minima-e2e + docker build -t minima-e2e endtoend && docker run -v /var/run/docker.sock:/var/run/docker.sock --env nbNodes=3 --network minima-e2e-testnet minima-e2e diff --git a/endtoend/api/package/index.js b/endtoend/api/package/index.js new file mode 100644 index 000000000..d535ba83f --- /dev/null +++ b/endtoend/api/package/index.js @@ -0,0 +1,284 @@ +const axios = require("axios"); + +const cfg = { + HTTP_TIMEOUT: 30000 +} + +var Minima_API = { + + rpchost : "http://127.0.0.1:9002", + + init: function(host) { + Minima_API.rpchost = `http://${host}:9002` + }, + + help: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/help", params) + }, + + tutorial: function() { + return get_minima_endpoint(Minima_API.rpchost, "/tutorial", "") + }, + + status: function() { + return get_minima_endpoint(Minima_API.rpchost, "/status", "") + }, + + topblock: function() { + return get_minima_endpoint(Minima_API.rpchost, "/topblock", "") + }, + + history: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/history", params) + }, + + backup: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/backup", params) + }, + + flushmempool: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/flushmempool", params) + }, + + check: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/check", params) + }, + + printdb: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/printdb", params) + }, + + printtree: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/printtree", params) + }, + + automine: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/automine", params) + }, + + trace: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/trace", params) + }, + + network: function() { + return get_minima_endpoint(Minima_API.rpchost, "/network", "") + }, + + connect: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/connect", params) + }, + + disconnect: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/disconnect", params) + }, + + reconnect: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/reconnect", params) + }, + + weblink: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/weblink", params) + }, + + gimme50: function() { + return get_minima_endpoint(Minima_API.rpchost, "/gimme50", "") + }, + + send: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/send", params) + }, + + sendpoll: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/sendpoll", params) + }, + + newaddress: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/newaddress", params) + }, + + balance: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/balance", params) + }, + + keys: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/keys", params) + }, + + exportkey: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/exportkey", params) + }, + + importkey: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/importkey", params) + }, + + coins: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/coins", params) + }, + + coinsimple: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/coinsimple", params) + }, + + keepcoin: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/keepcoin", params) + }, + + txpowsearch: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txpowsearch", params) + }, + + txpowinfo: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txpowinfo", params) + }, + + scripts: function() { + return get_minima_endpoint(Minima_API.rpchost, "/scripts", "") + }, + + newscript: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/newscript", params) + }, + + extrascript: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/extrascript", params) + }, + + cleanscript: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/cleanscript", params) + }, + + runscript: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/runscript", params) + }, + + tokens: function() { + return get_minima_endpoint(Minima_API.rpchost, "/tokens", "") + }, + + tokencreate: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/tokencreate", params) + }, + + tokenvalidate: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/tokenvalidate", params) + }, + + sign: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/sign", params) + }, + + verify: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/verify", params) + }, + + chainsha: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/chainsha", params) + }, + + random: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/random", params) + }, + + hash: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/hash", params) + }, + + maxima: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/maxima", params) + }, + + sshtunnel: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/sshtunnel", params) + }, + + minidapps: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/minidapps", params) + }, + + txnlist: function (params="") { + return get_minima_endpoint(Minima_API.rpchost, "/txnlist", params) + }, + + txncreate: function(params="") { + return get_minima_endpoint(Minima_API.rpchost, "/txncreate", params) + }, + + txndelete: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txndelete", params) + }, + + txnexport: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnexport", params) + }, + + txnimport: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnimport", params) + }, + + txninput: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txninput", params) + }, + + txnoutput: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnoutput", params) + }, + + txnreminput: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnreminput", params) + }, + + txnremoutput: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnremoutput", params) + }, + + txnstate: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnstate", params) + }, + + txnscript: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnscript", params) + }, + + txnsign: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnsign", params) + }, + + txnauto: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnauto", params) + }, + + txnsignauto: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnsignauto", params) + }, + + txnvalidate: function(params) { + return get_minima_endpoint(Minima_API.rpchost, "/txnvalidate", params) + }, + + quit: function() { + get_minima_endpoint(Minima_API.rpchost, "/quit", "") + } +} + +const get_minima_endpoint = async (host, endpoint, params="") => { + const url = host + endpoint + "+" + params.replace(/;/g, "+"); + try{ + const response = await axios.get(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) + + // handle success + if(response && response.status == 200) { + console.log(params, response.data); + if(response.data.status == true) { + return response.data.response; + } + } + } catch { error => { + // handle error + console.log(error); + } + } +} + +exports.Minima_API = Minima_API + diff --git a/endtoend/api/package/package.json b/endtoend/api/package/package.json new file mode 100644 index 000000000..82cc98a8e --- /dev/null +++ b/endtoend/api/package/package.json @@ -0,0 +1,15 @@ +{ + "name": "minima-api", + "version": "1.0.0", + "description": "node package for minima APIs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1" + } +} diff --git a/endtoend/minima-api-1.0.0.tgz b/endtoend/minima-api-1.0.0.tgz new file mode 100644 index 000000000..79df29f2d Binary files /dev/null and b/endtoend/minima-api-1.0.0.tgz differ diff --git a/endtoend/package-lock.json b/endtoend/package-lock.json index c0816d17d..bf3c3ee8f 100644 --- a/endtoend/package-lock.json +++ b/endtoend/package-lock.json @@ -10,10 +10,208 @@ "license": "ISC", "dependencies": { "axios": "^0.21.1", + "bn.js": "^4.11.9", + "bn.js-types": "^1.0.1", "chai": "^4.3.0", "chai-as-promised": "^7.1.1", "chai-bn": "^0.2.1", - "dockerode": "^3.2.1" + "chart.js": "^2.9.4", + "chartjs-node-canvas": "^3.2.0", + "csv-writer": "^1.6.0", + "dockerode": "^3.2.1", + "exceljs": "^4.2.1", + "fs": "^0.0.1-security", + "minima-api": "file:minima-api-1.0.0.tgz" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "dependencies": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "node_modules/archiver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz", + "integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.0", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/are-we-there-yet/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" } }, "node_modules/asn1": { @@ -32,6 +230,11 @@ "node": "*" } }, + "node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "node_modules/axios": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", @@ -40,6 +243,11 @@ "follow-redirects": "^1.10.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -67,6 +275,26 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -77,11 +305,32 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, "node_modules/bn.js": { "version": "4.11.9", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "peer": true + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + }, + "node_modules/bn.js-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bn.js-types/-/bn.js-types-1.0.1.tgz", + "integrity": "sha512-1kTB06ujmupJAPOCwno8pGYVwU1Ye33EoKT77ZwuFJbiBVlSx0TLhA8xlbrmY4DPaXS7fnpVeBVpGVimjMZEVQ==", + "dependencies": { + "@types/node": "^10.12.12" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, "node_modules/buffer": { "version": "5.7.1", @@ -106,6 +355,44 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/canvas": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", + "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.14.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.0.tgz", @@ -142,6 +429,55 @@ "chai": "^4.0.0" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "dependencies": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "node_modules/chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "dependencies": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "node_modules/chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/chartjs-node-canvas": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-3.2.0.tgz", + "integrity": "sha512-MT0K28VG8BXwCdh5BdRXNw4nzJWKzcN3t/xgTr8zJ8M6uOXl7hRdtIW8rEUcylkLED8LGsnJmSkcnqlhPy841g==", + "dependencies": { + "canvas": "^2.6.1", + "tslib": "^1.14.1" + }, + "peerDependencies": { + "chart.js": "^2.7.3" + } + }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -155,6 +491,98 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-convert/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, + "node_modules/dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "node_modules/debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -171,6 +599,17 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -182,6 +621,22 @@ "node": ">=0.12" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/docker-modem": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", @@ -208,6 +663,41 @@ "node": ">= 8.0" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -216,6 +706,45 @@ "once": "^1.4.0" } }, + "node_modules/exceljs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.2.1.tgz", + "integrity": "sha512-EogoTdXH1X1PxqD9sV8caYd1RIfXN3PVlCV+mA/87CgdO2h4X5xAEbr7CaiP8tffz7L4aBFwsdMbjfMXi29NjA==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.5.0", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/follow-redirects": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", @@ -235,11 +764,83 @@ } } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "node_modules/get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", @@ -248,6 +849,47 @@ "node": "*" } }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -267,21 +909,378 @@ } ] }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha1-SeKM1VkBNFjIFMVHnTxmOiG/qmw=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minima-api": { + "version": "1.0.0", + "resolved": "file:minima-api-1.0.0.tgz", + "integrity": "sha512-3fSeecdPWE3J/a7F1XJhwZAwwgpz9OONmebOtWVic/a8kArYXUt93j0+PR034x67RbSmOrzM3ogzHxJav4matA==", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "node_modules/moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -290,6 +1289,19 @@ "wrappy": "1" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -298,6 +1310,22 @@ "node": "*" } }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -320,6 +1348,28 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -344,6 +1394,83 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -389,6 +1516,46 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -415,6 +1582,38 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -428,18 +1627,284 @@ "node": ">=4" } }, + "node_modules/unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } } }, "dependencies": { + "@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" + } + } + }, + "@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + }, + "dependencies": { + "@types/node": { + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" + } + } + }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + } + }, + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "archiver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz", + "integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.0", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -453,6 +1918,11 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "axios": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", @@ -461,6 +1931,11 @@ "follow-redirects": "^1.10.0" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -474,6 +1949,20 @@ "tweetnacl": "^0.14.3" } }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -484,11 +1973,32 @@ "readable-stream": "^3.4.0" } }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, "bn.js": { "version": "4.11.9", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "peer": true + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + }, + "bn.js-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bn.js-types/-/bn.js-types-1.0.1.tgz", + "integrity": "sha512-1kTB06ujmupJAPOCwno8pGYVwU1Ye33EoKT77ZwuFJbiBVlSx0TLhA8xlbrmY4DPaXS7fnpVeBVpGVimjMZEVQ==", + "requires": { + "@types/node": "^10.12.12" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, "buffer": { "version": "5.7.1", @@ -499,6 +2009,31 @@ "ieee754": "^1.1.13" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" + }, + "canvas": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", + "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.14.0", + "simple-get": "^3.0.3" + } + }, "chai": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.0.tgz", @@ -526,6 +2061,49 @@ "integrity": "sha512-01jt2gSXAw7UYFPT5K8d7HYjdXj2vyeIuE+0T/34FWzlNcVbs1JkPxRu7rYMfQnJhrHT8Nr6qjSf5ZwwLU2EYg==", "requires": {} }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, + "chartjs-node-canvas": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-3.2.0.tgz", + "integrity": "sha512-MT0K28VG8BXwCdh5BdRXNw4nzJWKzcN3t/xgTr8zJ8M6uOXl7hRdtIW8rEUcylkLED8LGsnJmSkcnqlhPy841g==", + "requires": { + "canvas": "^2.6.1", + "tslib": "^1.14.1" + } + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -536,6 +2114,85 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + }, + "dependencies": { + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, + "crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" + }, + "dayjs": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -544,6 +2201,14 @@ "ms": "2.1.2" } }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -552,6 +2217,16 @@ "type-detect": "^4.0.0" } }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "docker-modem": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", @@ -572,6 +2247,43 @@ "tar-fs": "~2.0.1" } }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -580,41 +2292,466 @@ "once": "^1.4.0" } }, + "exceljs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.2.1.tgz", + "integrity": "sha512-EogoTdXH1X1PxqD9sV8caYd1RIfXN3PVlCV+mA/87CgdO2h4X5xAEbr7CaiP8tffz7L4aBFwsdMbjfMXi29NjA==", + "requires": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.5.0", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + } + }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, + "fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "requires": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + } + }, "follow-redirects": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha1-SeKM1VkBNFjIFMVHnTxmOiG/qmw=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, + "minima-api": { + "version": "file:minima-api-1.0.0.tgz", + "integrity": "sha512-3fSeecdPWE3J/a7F1XJhwZAwwgpz9OONmebOtWVic/a8kArYXUt93j0+PR034x67RbSmOrzM3ogzHxJav4matA==", + "requires": { + "axios": "^0.21.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -623,11 +2760,31 @@ "wrappy": "1" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -647,6 +2804,22 @@ "util-deprecate": "^1.0.1" } }, + "readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -657,6 +2830,57 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -693,6 +2917,44 @@ "safe-buffer": "~5.2.0" } }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + } + } + }, "tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -716,6 +2978,24 @@ "readable-stream": "^3.1.1" } }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -726,15 +3006,94 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, + "unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + } } } } diff --git a/endtoend/package.json b/endtoend/package.json index 27144e800..ef5775be2 100644 --- a/endtoend/package.json +++ b/endtoend/package.json @@ -10,9 +10,17 @@ "license": "ISC", "dependencies": { "axios": "^0.21.1", + "bn.js": "^4.11.9", + "bn.js-types": "^1.0.1", "chai": "^4.3.0", "chai-as-promised": "^7.1.1", "chai-bn": "^0.2.1", - "dockerode": "^3.2.1" + "chart.js": "^2.9.4", + "chartjs-node-canvas": "^3.2.0", + "csv-writer": "^1.6.0", + "dockerode": "^3.2.1", + "exceljs": "^4.2.1", + "fs": "^0.0.1-security", + "minima-api": "file:minima-api-1.0.0.tgz" } } diff --git a/endtoend/src/gen_graph.js b/endtoend/src/gen_graph.js new file mode 100644 index 000000000..dd6cb9d9e --- /dev/null +++ b/endtoend/src/gen_graph.js @@ -0,0 +1,172 @@ +const { CanvasRenderService } = require("chartjs-node-canvas"); +// const chartjs = require('chart.js'); +const fs = require("fs"); +var Excel = require("exceljs"); +var workbook = new Excel.Workbook(); + +var configuration = { + type: "line", + data: { + labels: [1, 2, 3, 4, 5], + datasets: [ + { + label: "Part1", + fill: false, + data: [2478, 5267, 734, 784, 433], + backgroundColor: "rgba(255, 99, 132, 1)", + borderColor: "rgba(255,99,132,1)", + }, + { + label: "Part2", + fill: false, + data: [1233, 3467, 7634, 384, 4533], + backgroundColor: "rgba(162, 99, 132, 1)", + borderColor: "rgba(162,99,132,1)", + }, + ], + }, + options: { + title: { + display: true, + text: 'Chart for p2p Test', + fontColor: "#07BDA7" + }, + scales: { + xAxes: [{ + scaleLabel: { + display: true, + labelString: "number of Minima nodes", + fontColor: "#028B7B" + }, + ticks: { + beginAtZero: true + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: "number of p2p neighbours", + fontColor: "#028B7B" + }, + ticks: { + beginAtZero: true + } + }] + } + } +}; + +const mkChart = async (params) => { + const canvasRenderService = new CanvasRenderService(400, 400); + return await canvasRenderService.renderToBuffer(configuration); +}; + +const test_graph_gen = async () => { + await readFiles("./results/"); + var image = await mkChart("test"); + + fs.writeFile("./results/graph.png", image, "base64", function (err) { + console.log(err); + }); +}; + +const readFiles = (dirname) => { + let label1 = [], + label2 = [], + set1 = [], + set2 = []; + return new Promise((resolve, reject) => { + fs.readdir(dirname, async function (err, filenames) { + if (err) { + console.log(err); + return; + } + for (let key in filenames) { + var nodes = [], + data = [], + sign = false; + var filename = filenames[key]; + var arr = filename.split("."); + if(arr[arr.length-1] != "csv") continue; + var worksheet = await workbook.csv.readFile(dirname + filename); + var partName = filename.split("-")[3].split(".")[0]; + + var column1 = worksheet.getColumn(1); + column1.eachCell(function (cell, rowNumber) { + if (cell.value != null) { + var temp = cell.value; + nodes.push(temp); + } + }); + nodes = nodes.slice(1); + let nodeCnt = nodes.length / 2; + if (partName === "part1") { + if (label1.indexOf(nodeCnt) > -1) { + sign = true; + } else { + label1.push(nodeCnt); + } + } else if (partName === "part2") { + if (label2.indexOf(nodeCnt) > -1) sign = true; + else label2.push(nodeCnt); + } + + var column2 = worksheet.getColumn(4); + column2.eachCell(function (cell, rowNumber) { + if (cell.value != null) { + var temp = cell.value; + data.push(temp); + } + }); + data = data.slice(1); + const reg = /\"p2pPeercount\"\:([0-9]+)\}/; + let counts = data.map((item, index) => index % 2 === 0 && parseInt(item.match(reg)[1])); + var count = counts.reduce(function(a, b) { return Math.max(a, b); }); + + if (!sign) { + if (partName === "part1") set1.push(count); + else if (partName === "part2") set2.push(count); + } + } + + //sorting values of x Axe + let temp = []; + for (let key in label1) { + let pair = {}; + pair.x = label1[key]; + pair.y = set1[key]; + temp.push(pair); + } + temp.sort((a,b) => { + return a.x - b.x + }); + label1 = temp.map(item => item.x); + set1 = temp.map(item => item.y); + + temp = []; + for (let key in label2) { + let pair = {}; + pair.x = label2[key]; + pair.y = set2[key]; + temp.push(pair); + } + temp.sort((a,b) => { + return a.x - b.x + }); + label2 = temp.map(item => item.x); + set2 = temp.map(item => item.y); + console.log("label1: ", label1); + console.log("label2: ", label2); + console.log("set1: ", set1); + console.log("set2: ", set2); + configuration.data.labels = label1; + configuration.data.datasets[0].data = set1; + configuration.data.datasets[1].data = set2; + resolve(); + }); + }); +}; + +module.exports = test_graph_gen + +// test_graph_gen() \ No newline at end of file diff --git a/endtoend/src/index.js b/endtoend/src/index.js index 61e7371b4..cc601e3ce 100644 --- a/endtoend/src/index.js +++ b/endtoend/src/index.js @@ -1,3 +1,14 @@ -var test_star_static = require('./test_star_static.js'); +const graph = process.env.graph; + +if (graph === "true") { + var test_graph_gen = require('./gen_graph.js'); + test_graph_gen(); +} else if(graph === "false") { + var test_star_static = require('./test_star_static.js'); + test_star_static(); +} + + + + -test_star_static(); diff --git a/endtoend/src/staticTests.changes.js b/endtoend/src/staticTests.changes.js new file mode 100644 index 000000000..fe5bb7511 --- /dev/null +++ b/endtoend/src/staticTests.changes.js @@ -0,0 +1,212 @@ +var Docker = require('dockerode'); +var docker = new Docker({socketPath: '/var/run/docker.sock'}); +const axios = require('axios').default; +const BN = require('bn.js'); + +require('chai') + .use(require('chai-as-promised')) +// .use(require('chai-bn')(require('bn.js'))) +.use(require('chai-bn')(BN)) +// .use(require('chai-bignumber')(BigNumber)) + .should(); + +require('chai').assert; + +// *** config *** +const cfg = { + image: 'minima:latest', // docker image name to run -> can be customised + docker_net: "minima-e2e-testnet", // docker private network name -> MUST BE CREATED MANUALLY + node1_args: ["-private", "-clean"], // only node 1 should be started with -private + node_prefix: "minima-node-", + HTTP_TIMEOUT: 30000, + hostCfg: { AutoRemove: true, NetworkMode: "minima-e2e-testnet" }, + // unused - can be applied on a node to expose its RPC port on localhost - not needed for our tests + hostCfgExpose: { AutoRemove: true, NetworkMode: "minima-e2e-testnet", PortBindings: {"9002/tcp": [ { "HostPort": "9002"} ] } }, + host_port: 9002, + TOPO_STAR: "star", + TOPO_LINE: "line" +} + +var nodes_args; // all other nodes get same args + +var containers = {}; +var ip_addrs = {}; + +createMinimaContainer = async function(cmd, name, hostConfig) { + return await docker.createContainer({ + AttachStderr: false, AttachStdin: false, AttachStdout: false, + Cmd: cmd, + Image: cfg.image, + OpenStdin: false, StdinOnce: false, Tty: false, + name: name, + HostConfig: hostConfig + }); +} + +const start_docker_node_1 = async function (topology, nbNodes, tests_collection) { + console.log("Creating container 1"); + // Create the container. + containers["1"] = await createMinimaContainer(cfg.node1_args, cfg.node_prefix + "1", cfg.hostCfg); + // Start the container. + await containers["1"].start(); + containers["1"].inspect(function (err, data) { + ip_addrs["1"] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + start_other_nodes(topology, nbNodes, tests_collection); + }); +} + +get_node_args = function(topology, pos) { + var parent = 0; + if(topology === cfg.TOPO_STAR) { + parent = ip_addrs["1"]; + } else if (topology === cfg.TOPO_LINE) { + parent = ip_addrs['' + (pos - 1)]; + } + const node_args = ["-connect", parent, "9001"]; + return node_args; +} + +start_other_nodes_star = async function(nbNodes, tests_collection) { + for (let pos = 2; pos < nbNodes+1; pos++) { + var node_args = get_node_args(cfg.TOPO_star, pos); + console.log("topo star node " + pos + " args: " + node_args); + containers[pos] = await createMinimaContainer(nodes_args, cfg.node_prefix + pos, cfg.hostCfg); + // Start the container. + await containers[pos].start(); + containers[pos].inspect(function (err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if(pos == nbNodes) { // run tests after we created last node + // need to sleep to let node sync with others + setTimeout(function () { tests_collection(ip_addrs) }, 2000); + } + }); + } +} + +start_other_nodes_line = async function (nbNodes, pos, tests_collection) { + if (pos < 2 || pos > nbNodes) { + return; + } + var node_args = get_node_args(cfg.TOPO_line, pos); + console.log("topo line node " + pos + " args: " + node_args); + containers[pos] = await createMinimaContainer(nodes_args, cfg.node_prefix + pos, cfg.hostCfg); + // Start the container. + await containers[pos].start(); + containers[pos].inspect(function (err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if (pos == nbNodes) { // run tests after we created last node + // need to sleep to let node sync with others + setTimeout(function () { tests_collection(ip_addrs) }, 2000); + } else { + start_other_nodes_line(nbNodes, pos+1, tests_collection); + } + }); +} + +start_other_nodes = async function (topology, nbNodes, tests_collection) { + if(topology === cfg.TOPO_STAR) { + start_other_nodes_star(nbNodes, tests_collection); + } else if(topology === cfg.TOPO_LINE) { + start_other_nodes_line(nbNodes, 2, tests_collection); + } else { + console.log("Unsupported topology! This error should be caught earlier."); + console.log(" topology="+topology); + } +} + +// this function calls HTTP GET on host:endpoint, expects a minima answer, and runs tests_to_run if server success. +run_some_tests_get = async function(host, endpoint, params="", tests) { + const url = "http://" + host + ":" + cfg.host_port + endpoint + params; + + axios.get(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) + .then(function (response) { + // handle success + if(response && response.status == 200) { + console.log(response.data); + if(response.data.status == true) { + tests(response.data.response); + } + } + }) + .catch(function (error) { + // handle error + console.log(error); + }) + .then(function () { + // always executed + }); +} + +// TODO / DO NOT USE +// this function calls HTTP POST on host:endpoint, expects a minima answer, and runs tests_to_run if server success. +run_some_tests_post = async function(host, endpoint, tests_to_run) { + const url = "http://" + host + ":" + cfg.host_port + endpoint; + axios.post(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) + .then(function (response) { + // handle success + if(response && response.status == 200) { + console.log(response.data); + if(response.data.status == true) { + tests_to_run(response.data.response); + } + } + }) + .catch(function (error) { + // handle error + console.log(error); + }) + .then(function () { + // always executed + }); +} + +stop_docker_nodes = async function() { + console.log("stop_docker_nodes"); + // iterate over all running containers and stop them if their name starts with node_prefix + docker.listContainers({ all:false }, + function (err, containers) { + if(containers) { + containers.forEach(function (containerInfo) { + if(containerInfo.Names[0].startsWith("/" + cfg.node_prefix)) { + console.log ("Found a minima node(" + containerInfo.Names[0] + "), stopping."); + docker.getContainer(containerInfo.Id).stop( + function () { + docker.getContainer(containerInfo.Id).remove(); + } + ); + docker.getContainer(containerInfo.Id).remove(); + } + }); + } else { + console.log("Found no running docker instances\n"); + } + }); +} + +// setup a network of nbNodes minima nodes in star topology and runs tests_collection on it with argument ip_addrs[node_prefix+ "01"] . +start_static_network_tests = async function (topology, nbNodes, tests_collection) { + if(!(topology === cfg.TOPO_STAR || topology === cfg.TOPO_LINE)) { + console.log("Error! Unsupported topology: " + topology); + return; + } + if(tests_collection == null) { + console.log("Error! Missing tests callback."); + return; + } + if(nbNodes < 1) { + console.log("Error! Unsupported number of nodes:" + nbNodes); + return; + } + if(nbNodes > 10) { + console.log("Warning! High number of nodes, tests may fail due to unresponsive nodes.\n Proceeding anyway.\n\n"); + } + await stop_docker_nodes(); + // give 5 seconds to stop all docker nodes (should depend on nbNodes but also system performance) + setTimeout(function() { start_docker_node_1(topology, nbNodes, tests_collection); }, 5000); +} + +exports.cfg = cfg; +exports.start_static_network_tests = start_static_network_tests; +exports.run_some_tests_get = run_some_tests_get; diff --git a/endtoend/src/staticTests.fix.js b/endtoend/src/staticTests.fix.js new file mode 100644 index 000000000..d6a894f4c --- /dev/null +++ b/endtoend/src/staticTests.fix.js @@ -0,0 +1,211 @@ +var Docker = require('dockerode'); +var docker = new Docker({socketPath: '/var/run/docker.sock'}); +const axios = require('axios').default; +const BN = require('bn.js'); + +require('chai') + .use(require('chai-as-promised')) +// .use(require('chai-bn')(require('bn.js'))) +.use(require('chai-bn')(BN)) +// .use(require('chai-bignumber')(BigNumber)) + .should(); + +require('chai').assert; + +// *** config *** +const cfg = { + image: 'minima:latest', // docker image name to run -> can be customised + docker_net: "minima-e2e-testnet", // docker private network name -> MUST BE CREATED MANUALLY + node1_args: ["-private", "-clean"], // only node 1 should be started with -private + node_prefix: "minima-node-", + HTTP_TIMEOUT: 30000, + DELAY_BEFORE_TESTS: 5000, + hostCfg: { AutoRemove: true, NetworkMode: "minima-e2e-testnet" }, + // unused - can be applied on a node to expose its RPC port on localhost - not needed for our tests + hostCfgExpose: { AutoRemove: true, NetworkMode: "minima-e2e-testnet", PortBindings: {"9002/tcp": [ { "HostPort": "9002"} ] } }, + host_port: 9002, + TOPO_STAR: "star", + TOPO_LINE: "line" +} + +var containers = {}; +var ip_addrs = {}; + +createMinimaContainer = async function(cmd, name, hostConfig) { + return await docker.createContainer({ + AttachStderr: false, AttachStdin: false, AttachStdout: false, + Cmd: cmd, + Image: cfg.image, + OpenStdin: false, StdinOnce: false, Tty: false, + name: name, + HostConfig: hostConfig + }); +} + +const start_docker_node_1 = async function (topology, nbNodes, tests_collection) { + // Create the container. + containers["1"] = await createMinimaContainer(cfg.node1_args, cfg.node_prefix + "1", cfg.hostCfg); + // Start the container. + await containers["1"].start(); + containers["1"].inspect(function (err, data) { + ip_addrs["1"] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + console.log("Started node 1 IP: " + ip_addrs["1"]); + start_other_nodes(topology, nbNodes, tests_collection); + }); +} + +get_node_args = function(topology, pos) { + var parent = 0; + if(topology === cfg.TOPO_STAR) { + parent = ip_addrs["1"]; + console.log("Parent IP: " + parent); + } else if (topology === cfg.TOPO_LINE) { + parent = ip_addrs['' + (pos - 1)]; + console.log("Parent IP: " + parent); + } else { + console.log("Unsupported topology! This error should be caught earlier."); + console.log(" topology="+topology); + } + const node_args = ["-connect", parent, "9001"]; + return node_args; +} + +start_other_nodes_star = async function(nbNodes, tests_collection) { + for (let pos = 2; pos < nbNodes+1; pos++) { + var node_args = get_node_args(cfg.TOPO_STAR, pos); + console.log("topo star node " + pos + " args: " + node_args); + containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostCfg); + // Start the container. + await containers[pos].start(); + containers[pos].inspect(function (err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if(pos == nbNodes) { // run tests after we created last node + // need to sleep to let node sync with others + setTimeout(function () { tests_collection(ip_addrs) }, cfg.DELAY_BEFORE_TESTS); + } + }); + } +} + +start_other_nodes_line = async function (nbNodes, pos, tests_collection) { + if (pos < 2 || pos > nbNodes) { + return; + } + var node_args = get_node_args(cfg.TOPO_line, pos); + console.log("topo line node " + pos + " args: " + node_args); + containers[pos] = await createMinimaContainer(nodes_args, cfg.node_prefix + pos, cfg.hostCfg); + // Start the container. + await containers[pos].start(); + containers[pos].inspect(function (err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if (pos == nbNodes) { // run tests after we created last node + // need to sleep to let node sync with others + setTimeout(function () { tests_collection(ip_addrs) }, cfg.DELAY_BEFORE_TESTS); + } else { + start_other_nodes_line(nbNodes, pos+1, tests_collection); + } + }); +} + +start_other_nodes = async function (topology, nbNodes, tests_collection) { + if(topology === cfg.TOPO_STAR) { + start_other_nodes_star(nbNodes, tests_collection); + } else if(topology === cfg.TOPO_LINE) { + start_other_nodes_line(nbNodes, 2, tests_collection); + } else { + console.log("Unsupported topology! This error should be caught earlier."); + console.log(" topology="+topology); + } +} + +// this function calls HTTP GET on host:endpoint, expects a minima answer, and runs tests_to_run if server success. +run_some_tests_get = async function(host, endpoint, params="", tests) { + const url = "http://" + host + ":" + cfg.host_port + endpoint + params; + + axios.get(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) + .then(function (response) { + // handle success + if(response && response.status == 200) { + console.log(response.data); + if(response.data.status == true) { + tests(response.data.response); + } + } + }) + .catch(function (error) { + // handle error + console.log(error); + }) + .then(function () { + // always executed + }); +} + +// TODO / DO NOT USE +// this function calls HTTP POST on host:endpoint, expects a minima answer, and runs tests_to_run if server success. +run_some_tests_post = async function(host, endpoint, tests_to_run) { + const url = "http://" + host + ":" + cfg.host_port + endpoint; + axios.post(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) + .then(function (response) { + // handle success + if(response && response.status == 200) { + console.log(response.data); + if(response.data.status == true) { + tests_to_run(response.data.response); + } + } + }) + .catch(function (error) { + // handle error + console.log(error); + }) + .then(function () { + // always executed + }); +} + +stop_docker_nodes = async function() { + console.log("stop_docker_nodes"); + // iterate over all running containers and stop them if their name starts with node_prefix + docker.listContainers({ all:false }, + function (err, containers) { + if(containers) { + containers.forEach(function (containerInfo) { + if(containerInfo.Names[0].startsWith("/" + cfg.node_prefix)) { + console.log ("Found a dangling minima node(" + containerInfo.Names[0] + "), stopping."); + docker.getContainer(containerInfo.Id).stop(); + } + }); + } else { + console.log("Found no running docker instances\n"); + } + }); +} + +// setup a network of nbNodes minima nodes in star topology and runs tests_collection on it with argument ip_addrs[node_prefix+ "01"] . +start_static_network_tests = async function (topology, nbNodes, tests_collection) { + if(!(topology === cfg.TOPO_STAR || topology === cfg.TOPO_LINE)) { + console.log("Error! Unsupported topology: " + topology); + return; + } + if(tests_collection == null) { + console.log("Error! Missing tests callback."); + return; + } + if(nbNodes < 1) { + console.log("Error! Unsupported number of nodes:" + nbNodes); + return; + } + if(nbNodes > 10) { + console.log("Warning! High number of nodes, tests may fail due to unresponsive nodes.\n Proceeding anyway.\n\n"); + } + await stop_docker_nodes(); + // give 5 seconds to stop all docker nodes (should depend on nbNodes but also system performance) + setTimeout(function() { start_docker_node_1(topology, nbNodes, tests_collection); }, 5000); +} + +exports.cfg = cfg; +exports.start_static_network_tests = start_static_network_tests; +exports.run_some_tests_get = run_some_tests_get; \ No newline at end of file diff --git a/endtoend/src/staticTests.js b/endtoend/src/staticTests.js index da5afccc0..5be0f73d5 100644 --- a/endtoend/src/staticTests.js +++ b/endtoend/src/staticTests.js @@ -13,23 +13,42 @@ require('chai') require('chai').assert; // *** config *** +DOCKER_P2P_PATH = '/root/.minima/p2p' +DOCKER_HOST_CONFIG_ROOT = '/Users/jeromerousselot/src/minima/Minima/data-e2e' +DOCKER_HOST_CONFIG_DIR = '/p2p' + const cfg = { image: 'minima:latest', // docker image name to run -> can be customised docker_net: "minima-e2e-testnet", // docker private network name -> MUST BE CREATED MANUALLY - node1_args: ["-private", "-clean"], // only node 1 should be started with -private + node1_args: ["-private"], // only node 1 should be started with -private node_prefix: "minima-node-", HTTP_TIMEOUT: 30000, DELAY_BEFORE_TESTS: 5000, - hostCfg: { AutoRemove: true, NetworkMode: "minima-e2e-testnet" }, + hostConfig1: { + AutoRemove: true, // comment this out to inspect stopped containers + NetworkMode: "minima-e2e-testnet", + 'Binds': [], + //'Binds': [ DOCKER_HOST_CONFIG_ROOT + '/node1' + DOCKER_HOST_CONFIG_DIR + ':' + DOCKER_P2P_PATH], + CpuShares: 10, // node 1 in private mode uses auto-mining and aim for 100% CPU usage, so we throttle it + }, + hostConfig: { + AutoRemove: true, // comment this out to inspect stopped containers + NetworkMode: "minima-e2e-testnet", + 'Binds': [], + CpuShares: 10, + }, // unused - can be applied on a node to expose its RPC port on localhost - not needed for our tests - hostCfgExpose: { AutoRemove: true, NetworkMode: "minima-e2e-testnet", PortBindings: {"9002/tcp": [ { "HostPort": "9002"} ] } }, + hostCfgExpose: { NetworkMode: "minima-e2e-testnet", PortBindings: {"9002/tcp": [ { "HostPort": "9002"} ] } }, host_port: 9002, TOPO_STAR: "star", - TOPO_LINE: "line" + TOPO_LINE: "line", + TOPO_CLUSTER: "cluster" } var containers = {}; var ip_addrs = {}; +var p2pdiscoveryaddr = ""; +var p2penr = ""; createMinimaContainer = async function(cmd, name, hostConfig) { return await docker.createContainer({ @@ -42,26 +61,60 @@ createMinimaContainer = async function(cmd, name, hostConfig) { }); } +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + const start_docker_node_1 = async function (topology, nbNodes, tests_collection) { console.log("Creating container 1"); + // set node 1 config path + cfg.hostConfig1.Binds = [ DOCKER_HOST_CONFIG_ROOT + '/node' + '1' + DOCKER_HOST_CONFIG_DIR + ':' + DOCKER_P2P_PATH]; // Create the container. - containers["1"] = await createMinimaContainer(cfg.node1_args, cfg.node_prefix + "1", cfg.hostCfg); + containers["1"] = await createMinimaContainer(cfg.node1_args, cfg.node_prefix + "1", cfg.hostConfig1); // Start the container. await containers["1"].start(); - containers["1"].inspect(function (err, data) { - ip_addrs["1"] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; - start_other_nodes(topology, nbNodes, tests_collection); - }); + process.stdout.write("Trying to sleep for 5 seconds..."); + await sleep(5000); + + await runContainerInspect(topology, nbNodes, tests_collection); +} + +const runContainerInspect = (topology, nbNodes, tests_collection) => { + return new Promise((resolve, reject) => { + containers["1"].inspect(async function(err, data) { + ip_addrs["1"] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + console.log("Started node 1," + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + get_node_p2p_params(ip_addrs["1"], function() { + resolve(start_other_nodes(topology, nbNodes, tests_collection)); + }); + }) + }) } get_node_args = function(topology, pos) { - var parent = 0; - if(topology === cfg.TOPO_STAR) { - parent = ip_addrs["1"]; + p2p = true; + var node_args = []; + if(p2p) { + + // these two fields must be retrieved programmatically from node1 + // node_args = ["-p2p-static","/ip4/172.18.0.2/udp/11522/p2p/16Uiu2HAkvSYiDo3G4Cw7XVicPK5BMjgm8vMHKYtGNCWQvskV3RdQ","-p2p-bootnode","enr:-Iu4QDirGhMYfgvNha7PVhMshqn1INf8ZjV2As0YkMgszLR1OlglWz68HjTLNxUml_BHbNGmq1C9zM3OyQiJzjX6YJYBgmlkgnY0gmlwhKwSAAKJc2VjcDI1NmsxoQIPFQyakHo15u_GazoWP_L3Qboxkjgpv2gK-Des9SMZj4N0Y3CCLQKDdWRwgi0C"]; + node_args = ["-p2p-static", p2pdiscoveryaddr, "-p2p-bootnode", p2penr]; + } else if (topology === cfg.TOPO_STAR) { + node_args = ["-connect", ip_addrs["1"], "9001"]; } else if (topology === cfg.TOPO_LINE) { - parent = ip_addrs['' + (pos - 1)]; + node_args = ["-connect", ip_addrs['' + (pos - 1)], "9001"]; + } else if (topology === cfg.TOPO_CLUSTER) { + if (pos < 3 + 1) { + for(let i = 1; i < pos; i++) { + node_args.push("-connect", ip_addrs['' + i], "9001"); + } + } else { + var rn = Math.ceil(Math.random() * 3); + node_args.push("-connect", ip_addrs['' + rn], '9001'); + } } - const node_args = ["-connect", parent, "9001"]; return node_args; } @@ -69,18 +122,29 @@ start_other_nodes_star = async function(nbNodes, tests_collection) { for (let pos = 2; pos < nbNodes+1; pos++) { var node_args = get_node_args(cfg.TOPO_STAR, pos); console.log("topo star node " + pos + " args: " + node_args); - containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostCfg); + cfg.hostConfig.Binds = [ DOCKER_HOST_CONFIG_ROOT + '/node' + pos + DOCKER_HOST_CONFIG_DIR + ':' + DOCKER_P2P_PATH]; + containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostConfig); // Start the container. + await containers[pos].start(); - containers[pos].inspect(function (err, data) { + + await sleep(5000); + await starContainerInspect(pos, nbNodes, tests_collection); + } +} + +const starContainerInspect = (pos, nbNodes, tests_collection) => { + return new Promise((resolve) => { + containers[''+pos].inspect(async function(err, data) { console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; - if(pos == nbNodes) { // run tests after we created last node - // need to sleep to let node sync with others - setTimeout(function () { tests_collection(ip_addrs) }, cfg.DELAY_BEFORE_TESTS); + if(pos == nbNodes) { + resolve(tests_collection(0, ip_addrs)) + } else { + resolve(null) } - }); - } + }) + }) } start_other_nodes_line = async function (nbNodes, pos, tests_collection) { @@ -89,71 +153,106 @@ start_other_nodes_line = async function (nbNodes, pos, tests_collection) { } var node_args = get_node_args(cfg.TOPO_LINE, pos); console.log("topo line node " + pos + " args: " + node_args); - containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostCfg); + containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostConfig); + // Start the container. await containers[pos].start(); - containers[pos].inspect(function (err, data) { - console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); - ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; - if (pos == nbNodes) { // run tests after we created last node - // need to sleep to let node sync with others - setTimeout(function () { tests_collection(ip_addrs) }, cfg.DELAY_BEFORE_TESTS); - } else { - start_other_nodes_line(nbNodes, pos+1, tests_collection); - } - }); + await sleep(5000); + + await lineContainerInspect(pos, nbNodes, tests_collection); +} + +const lineContainerInspect = (pos, nbNodes, tests_collection) => { + return new Promise((resolve) => { + containers[''+pos].inspect(async function(err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if(pos == nbNodes) { + resolve(tests_collection(0, ip_addrs)) + } else { + resolve(start_other_nodes_line(nbNodes, pos+1, tests_collection)) + } + }) + }) +} + +start_other_nodes_cluster = async function (nbNodes, pos, tests_collection) { + if (pos < 2 || pos > nbNodes) { + return; + } + var node_args = get_node_args(cfg.TOPO_CLUSTER, pos); + console.log("topo cluster node " + pos + " args: " + node_args); + containers[pos] = await createMinimaContainer(node_args, cfg.node_prefix + pos, cfg.hostConfig); + + // Start the container. + await containers[pos].start(); + await sleep(5000); + + await clusterContainerInspect(pos, nbNodes, tests_collection); +} + +const clusterContainerInspect = (pos, nbNodes, tests_collection) => { + return new Promise((resolve) => { + containers[''+pos].inspect(async function(err, data) { + console.log("Started node " + pos + " IP: " + JSON.stringify(data.NetworkSettings.Networks[cfg.docker_net].IPAddress)); + ip_addrs[pos] = data.NetworkSettings.Networks[cfg.docker_net].IPAddress; + if(pos == nbNodes) { + resolve(tests_collection(0, ip_addrs)) + } else { + resolve(start_other_nodes_cluster(nbNodes, pos+1, tests_collection)) + } + }) + }) } start_other_nodes = async function (topology, nbNodes, tests_collection) { if(topology === cfg.TOPO_STAR) { - start_other_nodes_star(nbNodes, tests_collection); + await start_other_nodes_star(nbNodes, tests_collection); } else if(topology === cfg.TOPO_LINE) { - start_other_nodes_line(nbNodes, 2, tests_collection); + await start_other_nodes_line(nbNodes, 2, tests_collection); + } else if(topology === cfg.TOPO_CLUSTER) { + await start_other_nodes_cluster(nbNodes, 2, tests_collection); } else { console.log("Unsupported topology! This error should be caught earlier."); - console.log(" topology="+topology); + console.log(" topology=" + topology); } } -// this function calls HTTP GET on host:endpoint, expects a minima answer, and runs tests_to_run if server success. -run_some_tests_get = async function(host, endpoint, params="", tests) { - const url = "http://" + host + ":" + cfg.host_port + endpoint + params; - +get_node_p2p_params = function(host, cb) { + const url = "http://" + host + ":" + cfg.host_port + '/status'; + var disc = ''; + var enr = ''; axios.get(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) .then(function (response) { // handle success if(response && response.status == 200) { - console.log(response.data); + console.log("received axios answer code 200"); + //console.log("response: " + JSON.stringify(response)); + console.log("response.data.response: " + response.data.response); + //data = JSON.parse(response.data); + console.log("response.data.status: " + response.data.status); + console.log("response.data.response.p2pEnr: " + response.data.response.p2pEnr); + //TODO: check values for p2pEnr and p2pDiscoveryAddr, block if empty + //console.log("data: " + data); + //console.log("data.status: " + data.status); + //console.log("data.response.p2penr: " + data.response.p2penr); if(response.data.status == true) { - tests(response.data.response); - } - } - }) - .catch(function (error) { - // handle error - console.log(error); - }) - .then(function () { - // always executed - }); -} - -// TODO / DO NOT USE -// this function calls HTTP POST on host:endpoint, expects a minima answer, and runs tests_to_run if server success. -run_some_tests_post = async function(host, endpoint, tests_to_run) { - const url = "http://" + host + ":" + cfg.host_port + endpoint; - axios.post(url, {timeout: cfg.HTTP_TIMEOUT}, {maxContentLength: 3000}, {responseType: 'plain'}) - .then(function (response) { - // handle success - if(response && response.status == 200) { - console.log(response.data); - if(response.data.status == true) { - tests_to_run(response.data.response); + console.log("received data with status = true, extracting p2p fields"); + disc = response.data.response.p2pDiscoveryaddr; + enr = response.data.response.p2pEnr; + console.log("disc=" + disc + " enr=" + enr); + p2pdiscoveryaddr = disc; + p2penr = enr; + cb(); + } else { + console.log("json data incorrect, not calling callback"); + console.log("data.status was: " + response.data.status); } } }) .catch(function (error) { // handle error + console.log("axios error handler:"); console.log(error); }) .then(function () { @@ -164,24 +263,23 @@ run_some_tests_post = async function(host, endpoint, tests_to_run) { stop_docker_nodes = async function() { console.log("stop_docker_nodes"); // iterate over all running containers and stop them if their name starts with node_prefix - docker.listContainers({ all:false }, - function (err, containers) { - if(containers) { - containers.forEach(function (containerInfo) { - if(containerInfo.Names[0].startsWith("/" + cfg.node_prefix)) { - console.log ("Found a minima node(" + containerInfo.Names[0] + "), stopping."); - docker.getContainer(containerInfo.Id).stop(); - } - }); - } else { - console.log("Found no running docker instances\n"); - } - }); + docker.listContainers({ all:false }, function (err, containers) { + if(containers) { + containers.forEach(function (containerInfo) { + if(containerInfo.Names[0].startsWith("/" + cfg.node_prefix)) { + console.log ("Found a minima node(" + containerInfo.Names[0] + "), stopping."); + docker.getContainer(containerInfo.Id).stop(); + } + }); + } else { + console.log("Found no running docker instances\n"); + } + }); } // setup a network of nbNodes minima nodes in star topology and runs tests_collection on it with argument ip_addrs[node_prefix+ "01"] . -start_static_network_tests = async function (topology, nbNodes, tests_collection) { - if(!(topology === cfg.TOPO_STAR || topology === cfg.TOPO_LINE)) { +start_static_network_tests = async function (topology, nbNodes, nodeFailure, tests_collection) { + if(!(topology === cfg.TOPO_STAR || topology === cfg.TOPO_LINE || topology === cfg.TOPO_CLUSTER)) { console.log("Error! Unsupported topology: " + topology); return; } @@ -198,9 +296,14 @@ start_static_network_tests = async function (topology, nbNodes, tests_collection } await stop_docker_nodes(); // give 5 seconds to stop all docker nodes (should depend on nbNodes but also system performance) - setTimeout(function() { start_docker_node_1(topology, nbNodes, tests_collection); }, 5000); + await sleep(5000); + await start_docker_node_1(topology, nbNodes, tests_collection); + + //stop one node and run tests + await containers[''+nodeFailure].stop(); + console.log("node " + nodeFailure + " stopping..."); + await tests_collection(1, ip_addrs); } exports.cfg = cfg; exports.start_static_network_tests = start_static_network_tests; -exports.run_some_tests_get = run_some_tests_get; \ No newline at end of file diff --git a/endtoend/src/test_star_static.js b/endtoend/src/test_star_static.js index dbac89230..fdfa940a5 100644 --- a/endtoend/src/test_star_static.js +++ b/endtoend/src/test_star_static.js @@ -4,47 +4,73 @@ require('chai') .use(require('chai-as-promised')); require('chai').assert; -const nbNodes = 3; +var { Minima_API } = require('minima-api'); +var fs = require('fs'); +const createCsvWriter = require("csv-writer").createObjectCsvWriter; + +const topology = process.env.topology; +const nbNodes = parseInt(process.env.nbNodes); +const nodeFailure = parseInt(process.env.nodeFailure); + +console.log("test===>", topology, nbNodes, nodeFailure) + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} test_star_static = function () { - // number of nodes, list of tests - staticTests.start_static_network_tests("star", nbNodes, function (ip_addrs) { + staticTests.start_static_network_tests(topology, nbNodes, nodeFailure, async function (flag, ip_addrs) { console.log("tests collection"); - // test 0: healthcheck - are we connected? - // curl -s 127.0.0.1:9002/status | jq '.response.connections' - staticTests.run_some_tests_get(ip_addrs["1"], '/status', "", function (response) { - response.connections.should.be.above(0); - response.connections.should.be.equal(nbNodes-1); - }); + //wait for processing(should depend on nbNodes but also system performance) + await sleep(60000); - //1. send funds with no money and assert failure - //staticTests.run_some_tests_get(ip_addrs["1"], '/send', {"amount": 1, "address": "0xFF", "tokenid": "0x00"}, - staticTests.run_some_tests_get(ip_addrs["1"], '/send', params="+1+0xFF", - tests=function (response) { - console.log("send response: " + JSON.stringify(response)); - }); + if (!fs.existsSync("./results")){ + fs.mkdirSync("./results"); + } + + let current = new Date(); - // 2. generate 50 coins - staticTests.run_some_tests_get(ip_addrs["1"], '/gimme50', "", tests = function (response) { - // response.connections.should.be.above(0); - // response.chainlength.should.be.above(1); - console.log("gimme50 response: " + JSON.stringify(response)); + let csvWriter = createCsvWriter({ + path: flag == 0 ? `./results/result-${current.getDate()}_${current.getMonth()+1}_${current.getFullYear()}-${current.getHours()}_${current.getMinutes()}-part1.csv` + : `./results/result-${current.getDate()}_${current.getMonth()+1}_${current.getFullYear()}-${current.getHours()}_${current.getMinutes()}-part2.csv`, + header: [ + { id: "node", title: "Node" }, + { id: "ip", title: "IP" }, + { id: "request", title: "Request" }, + { id: "response", title: "Response" }, + ], }); - // 3. send funds with money - //staticTests.run_some_tests_get(ip_addrs["1"], '/send', {"amount": 1, "address": "0xFF", "tokenid": "0x00"}, - setTimeout(function () { - staticTests.run_some_tests_get(ip_addrs["1"], '/send', params="+1+0xFF", - tests=function (response) { - console.log("send response: " + JSON.stringify(response.txpow.body.txn)); - response.txpow.body.txn.inputs[0].amount.should.be.equal("25"); - response.txpow.body.txn.outputs[0].amount.should.be.equal("1"); - response.txpow.body.txn.outputs[1].amount.should.be.equal("24"); - response.txpow.body.txn.outputs[0].address.should.be.equal("0xFF"); - })}, 10000); + let data = []; + + for(child = 1; child < nbNodes+1; child++) { + if (flag == 1 && child == nodeFailure) continue; + console.log("connecting to node " + child + " to verify status."); + + await Minima_API.init(ip_addrs[child.toString()]); + + let midData = {}; + midData["node"] = child; + midData["ip"] = ip_addrs[child.toString()]; + + var status = await Minima_API.status(); + midData["request"] = "status"; + midData["response"] = JSON.stringify(status); + data.push(midData); + + let tempData = {} + var network = await Minima_API.network(); + tempData["node"] = child; + tempData["ip"] = ip_addrs[child.toString()]; + tempData["request"] = "network"; + tempData["response"] = JSON.stringify(network); + data.push(tempData); } - ); + await csvWriter.writeRecords(data) + }) } module.exports = test_star_static diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..1f218a8c7 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs='-Dlog4j.configurationFile=resources/log4j2.xml' + diff --git a/resources/log4j2.properties b/resources/log4j2.properties new file mode 100644 index 000000000..6038cd72a --- /dev/null +++ b/resources/log4j2.properties @@ -0,0 +1,17 @@ + +log=/tmp/minima-logstxt + +# Define the root loogger with appender +log4j.rootlogger=DEBUG, stdout, KPLOGFILE + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n + +log4j.appender.KPLOGFILE=org.apache.log4j.FileAppender +log4j.appender.KPLOGFILE.File=${log}/kplog.out +log4j.appender.KPLOGFILE.layout=org.apache.log4j.PatternLayout +log4j.appender.KPLOGFILE.layout.conversionpattern=%m%n + +log4j.logger.kplogger=DEBUG, KPLOGFILE + diff --git a/resources/log4j2.xml b/resources/log4j2.xml new file mode 100644 index 000000000..d953bc3fc --- /dev/null +++ b/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/log4j.properties b/src/log4j.properties new file mode 100644 index 000000000..9a538dcc7 --- /dev/null +++ b/src/log4j.properties @@ -0,0 +1,8 @@ +# This sets the global logging level and specifies the appenders +log4j.rootLogger=INFO, theConsoleAppender + +# settings for the console appender +log4j.appender.theConsoleAppender=org.apache.log4j.ConsoleAppender +log4j.appender.theConsoleAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.theConsoleAppender.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n + diff --git a/src/log4j2.xml b/src/log4j2.xml new file mode 100644 index 000000000..609151a79 --- /dev/null +++ b/src/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2-test.properties b/src/main/resources/log4j2-test.properties new file mode 100644 index 000000000..24bdc81ee --- /dev/null +++ b/src/main/resources/log4j2-test.properties @@ -0,0 +1,3 @@ +# Root logger option +log4j.rootLogger=TRACE, stdout + diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 000000000..609151a79 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/org/minima/Start.java b/src/org/minima/Start.java index eb2b3f60f..55fe887dc 100644 --- a/src/org/minima/Start.java +++ b/src/org/minima/Start.java @@ -9,6 +9,8 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Random; +import java.util.Set; +import java.util.HashSet; import org.minima.system.Main; import org.minima.system.brains.BackupManager; @@ -25,7 +27,7 @@ public class Start { /** * A list of default valid nodes to connect to at startup.. */ - public static final String[] VALID_BOOTSTRAP_NODES = + public static String[] VALID_BOOTSTRAP_NODES = {"35.204.181.120", "35.204.119.15", "34.91.220.49", @@ -121,6 +123,9 @@ public static void main(String[] zArgs){ boolean privatenetwork = false; boolean daemon = false; boolean automine = false; + + Set p2pStaticSet = new HashSet(); + Set p2pBootnodeSet = new HashSet(); //Configuration folder File conf = new File(System.getProperty("user.home"),".minima"); @@ -154,6 +159,9 @@ public static void main(String[] zArgs){ MinimaLogger.log(" -connect [host] [port] : Don't connect to MainNet but connect to this node instead."); MinimaLogger.log(" -daemon : Accepts no input from STDIN. Can run in background process."); MinimaLogger.log(" -externalurl : Send a POST request to this URL with Minima JSON information."); + MinimaLogger.log(" -p2p-static : static peer address for p2p network discovery."); + MinimaLogger.log(" -p2p-bootnode : bootnode record for p2p network discovery."); + MinimaLogger.log(" -help : Show this help"); MinimaLogger.log(""); MinimaLogger.log("With zero parameters Minima will start and connect to a set of default nodes."); @@ -194,7 +202,16 @@ public static void main(String[] zArgs){ }else if(arg.equals("-test")) { //Use the Test PARAMS! TestParams.setTestParams(); - + } else if(arg.equals("-p2p-static")) { + p2pStaticSet.add(zArgs[counter++]); + // if this set is not empty we must ignore valid_bootstrap_nodes and connecthost / connectport + VALID_BOOTSTRAP_NODES = new String[0]; + connecthost = ""; + } else if(arg.equals("-p2p-bootnode")) { + p2pBootnodeSet.add(zArgs[counter++]); + // if this set is not empty we must ignore valid_bootstrap_nodes and connecthost / connectport + VALID_BOOTSTRAP_NODES = new String[0]; + connecthost = ""; }else if(arg.equals("")) { //Do nothing.. @@ -217,15 +234,18 @@ public static void main(String[] zArgs){ //Wipe webroot too.. BackupManager.deleteWebRoot(conffile); } - + // TODO: replace array with dynamic set and extract array when calling Main + // TODO: manage more than one staticpeer / bootnode from commandline by pushing to set + // TODO: read staticpeers and bootnodes from config file depending on some flag - maybe combined with command line args + //Start the main Minima server - Main rcmainserver = new Main(host, port, conffile.getAbsolutePath()); + Main rcmainserver = new Main(host, port, conffile.getAbsolutePath(), p2pStaticSet.toArray(new String[0]), p2pBootnodeSet.toArray(new String[0])); //Link it. mMainServer = rcmainserver; //Have we added any connect hosts.. - if(connectlist.size() == 0 && connect) { + if((connectlist.size() == 0) && connecthost != null && !connecthost.isEmpty() && connect) { rcmainserver.addAutoConnectHostPort(connecthost+":"+connectport); }else { for(String hostport : connectlist) { diff --git a/src/org/minima/system/Main.java b/src/org/minima/system/Main.java index 04e6847d9..1f30df251 100644 --- a/src/org/minima/system/Main.java +++ b/src/org/minima/system/Main.java @@ -10,6 +10,7 @@ import org.minima.system.brains.SendManager; import org.minima.system.input.InputHandler; import org.minima.system.network.NetworkHandler; +import org.minima.system.network.base.P2PStart; import org.minima.system.txpow.TxPoWMiner; import org.minima.utils.MinimaLogger; import org.minima.utils.SQLHandler; @@ -65,6 +66,17 @@ public static Main getMainHandler() { */ private BackupManager mBackup; + /** + * P2P nodes discovery layer + */ + + private P2PStart mP2P; + + /** + * Are we creating a network from scratch + */ + boolean mGenesis = false; + /** * Default nodes to connect to */ @@ -81,7 +93,7 @@ public static Main getMainHandler() { * @param zPort * @param zGenesis */ - public Main(String zHost, int zPort, String zConfFolder) { + public Main(String zHost, int zPort, String zConfFolder, String[] p2pStaticNodes, String[] p2pBootnodes) { super("MAIN"); mMainHandler = this; @@ -101,6 +113,7 @@ public Main(String zHost, int zPort, String zConfFolder) { mTXMiner = new TxPoWMiner(); mConsensus = new ConsensusHandler(); mSendManager = new SendManager(); + mP2P = new P2PStart(zConfFolder, mNetwork, p2pStaticNodes, p2pBootnodes); /** * Introduction.. @@ -165,6 +178,10 @@ public BackupManager getBackupManager() { return mBackup; } + public P2PStart getP2P() { + return mP2P; + } + public TxPoWMiner getMiner() { return mTXMiner; } diff --git a/src/org/minima/system/brains/ConsensusPrint.java b/src/org/minima/system/brains/ConsensusPrint.java index 67d7db83a..c6cd3b9cd 100644 --- a/src/org/minima/system/brains/ConsensusPrint.java +++ b/src/org/minima/system/brains/ConsensusPrint.java @@ -8,6 +8,7 @@ import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; +import java.util.Optional; import org.minima.GlobalParams; import org.minima.database.MinimaDB; @@ -1067,6 +1068,13 @@ public int compare(JSONObject o1, JSONObject o2) { //Add the network connections ArrayList nets = main.getNetworkHandler().getNetClients(); status.put("connections", nets.size()); + String nodeid = main.getP2P().getNodeId().toString(); + status.put("p2pNodeid", nodeid==null?"unavailable":nodeid); + Optional discAddr = main.getP2P().getDiscoveryAddress(); + status.put("p2pDiscoveryaddr", discAddr.isPresent()?discAddr.get():"unavailable"); + String enr = main.getP2P().getENR(); + status.put("p2pEnr", enr!=null?enr:"unavailable"); + status.put("p2pPeercount", main.getP2P().getP2PPeerCount()); //Add it to the output InputHandler.endResponse(zMessage, true, ""); diff --git a/src/org/minima/system/network/base/AsyncRunner.java b/src/org/minima/system/network/base/AsyncRunner.java new file mode 100644 index 000000000..4b95d3e0b --- /dev/null +++ b/src/org/minima/system/network/base/AsyncRunner.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import com.google.common.base.Preconditions; +import java.time.Duration; +import java.util.function.Consumer; + +public interface AsyncRunner { + + default SafeFuture runAsync(final ExceptionThrowingRunnable action) { + return runAsync(() -> SafeFuture.fromRunnable(action)); + } + + SafeFuture runAsync(final ExceptionThrowingFutureSupplier action); + + SafeFuture runAfterDelay(ExceptionThrowingFutureSupplier action, final Duration delay); + + default SafeFuture runAfterDelay( + final ExceptionThrowingRunnable action, final Duration delay) { + return runAfterDelay(() -> SafeFuture.fromRunnable(action), delay); + } + + void shutdown(); + + default SafeFuture runAsync(final ExceptionThrowingSupplier action) { + return runAsync(() -> SafeFuture.of(action)); + } + + default SafeFuture getDelayedFuture(final Duration delay) { + return runAfterDelay(() -> SafeFuture.COMPLETE, delay); + } + + /** + * Schedules the recurrent task which will be repeatedly executed with the specified delay. + * + *

The returned instance can be used to cancel the task. Note that {@link Cancellable#cancel()} + * doesn't interrupt already running task. + * + *

Whenever the {@code runnable} throws exception it is notified to the {@code + * exceptionHandler} and the task recurring executions are not interrupted + */ + default Cancellable runWithFixedDelay( + final ExceptionThrowingRunnable runnable, + final Duration delay, + final Consumer exceptionHandler) { + Preconditions.checkNotNull(exceptionHandler); + + Cancellable cancellable = FutureUtil.createCancellable(); + FutureUtil.runWithFixedDelay(this, runnable, cancellable, delay, exceptionHandler); + return cancellable; + } + + /** + * Execute the future supplier until it completes normally up to some maximum number of retries. + * + * @param action The action to run + * @param retryDelay The time to wait before retrying + * @param maxRetries The maximum number of retries. A value of 0 means the action is run only once + * (no retries). + * @param The value returned by the action future + * @return A future that resolves with the first successful result, or else an error if the + * maximum retries are exhausted. + */ + default SafeFuture runWithRetry( + final ExceptionThrowingFutureSupplier action, + final Duration retryDelay, + final int maxRetries) { + + return SafeFuture.of(action) + .exceptionallyCompose( + err -> { + if (maxRetries > 0) { + // Retry after delay, decrementing the remaining available retries + final int remainingRetries = maxRetries - 1; + return runAfterDelay( + () -> runWithRetry(action, retryDelay, remainingRetries), retryDelay); + } else { + return SafeFuture.failedFuture(err); + } + }); + } +} diff --git a/src/org/minima/system/network/base/Cancellable.java b/src/org/minima/system/network/base/Cancellable.java new file mode 100644 index 000000000..9b65a8d7a --- /dev/null +++ b/src/org/minima/system/network/base/Cancellable.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +public interface Cancellable { + + void cancel(); + + boolean isCancelled(); +} diff --git a/src/org/minima/system/network/base/ConnectionManager.java b/src/org/minima/system/network/base/ConnectionManager.java new file mode 100644 index 000000000..b412193c5 --- /dev/null +++ b/src/org/minima/system/network/base/ConnectionManager.java @@ -0,0 +1,236 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static java.util.stream.Collectors.toList; + +import java.time.Duration; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.minima.system.network.base.metrics.Counter; +import org.minima.system.network.base.metrics.LabelledMetric; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.metrics.TekuMetricCategory; +import org.minima.system.network.base.peer.DisconnectReason; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.Peer; +// // import org.hyperledger.besu.plugin.services.MetricsSystem; +// // import org.hyperledger.besu.plugin.services.metrics.Counter; +// // import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +// import tech.pegasys.teku.infrastructure.async.AsyncRunner; +// import tech.pegasys.teku.infrastructure.async.Cancellable; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +// import tech.pegasys.teku.networking.p2p.connection.PeerPools.PeerPool; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryService; +// import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.DisconnectReason; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.service.serviceutils.Service; +import org.minima.system.network.base.peer.PeerAddress; +import org.minima.system.network.base.peer.PeerPools; +import org.minima.system.network.base.peer.PeerSelectionStrategy; +import org.minima.system.network.base.peer.PeerPools.PeerPool; + +public class ConnectionManager extends Service { + private static final Logger LOG = LogManager.getLogger(ConnectionManager.class); + private static final Duration RECONNECT_TIMEOUT = Duration.ofSeconds(20); + private static final Duration DISCOVERY_INTERVAL = Duration.ofSeconds(30); + private final AsyncRunner asyncRunner; + private final P2PNetwork network; + private final Set staticPeers; + private final DiscoveryService discoveryService; + private final PeerSelectionStrategy peerSelectionStrategy; + private final Counter attemptedConnectionCounter; + private final Counter successfulConnectionCounter; + private final Counter failedConnectionCounter; + private final PeerPools peerPools = new PeerPools(); + private final Collection> peerPredicates = new CopyOnWriteArrayList<>(); + + private volatile long peerConnectedSubscriptionId; + private volatile Cancellable periodicPeerSearch; + + public ConnectionManager( + final MetricsSystem metricsSystem, + final DiscoveryService discoveryService, + final AsyncRunner asyncRunner, + final P2PNetwork network, + final PeerSelectionStrategy peerSelectionStrategy, + final List peerAddresses) { + this.asyncRunner = asyncRunner; + this.network = network; + this.staticPeers = new HashSet<>(peerAddresses); + this.discoveryService = discoveryService; + this.peerSelectionStrategy = peerSelectionStrategy; + + final LabelledMetric connectionAttemptCounter = + metricsSystem.createLabelledCounter( + TekuMetricCategory.NETWORK, + "peer_connection_attempt_count", + "Total number of outbound connection attempts made", + "status"); + attemptedConnectionCounter = connectionAttemptCounter.labels("attempted"); + successfulConnectionCounter = connectionAttemptCounter.labels("successful"); + failedConnectionCounter = connectionAttemptCounter.labels("failed"); + } + + @Override + protected SafeFuture doStart() { + LOG.trace("Starting discovery manager"); + synchronized (this) { + staticPeers.forEach(this::createPersistentConnection); + } + periodicPeerSearch = + asyncRunner.runWithFixedDelay( + this::searchForPeers, + DISCOVERY_INTERVAL, + error -> LOG.error("Error while searching for peers", error)); + connectToKnownPeers(); + searchForPeers(); + peerConnectedSubscriptionId = network.subscribeConnect(this::onPeerConnected); + return SafeFuture.COMPLETE; + } + + private void connectToKnownPeers() { + peerSelectionStrategy + .selectPeersToConnect( + network, + peerPools, + () -> discoveryService.streamKnownPeers().filter(this::isPeerValid).collect(toList())) + .forEach(this::attemptConnection); + } + + private void searchForPeers() { + if (!isRunning()) { + LOG.debug("Not running so not searching for peers"); + return; + } + LOG.debug("Searching for peers"); + discoveryService + .searchForPeers() + .orTimeout(10, TimeUnit.SECONDS) + .finish( + this::connectToKnownPeers, + error -> { + LOG.debug("Discovery failed", error); + connectToKnownPeers(); + }); + } + + private void attemptConnection(final PeerAddress peerAddress) { + LOG.debug("Attempting to connect to {}", peerAddress.getId()); + attemptedConnectionCounter.inc(); + network + .connect(peerAddress) + .finish( + peer -> { + LOG.debug("Successfully connected to peer {}", peer.getId()); + successfulConnectionCounter.inc(); + peer.subscribeDisconnect( + (reason, locallyInitiated) -> peerPools.forgetPeer(peer.getId())); + }, + error -> { + LOG.debug(() -> "Failed to connect to peer: " + peerAddress.getId(), error); + failedConnectionCounter.inc(); + peerPools.forgetPeer(peerAddress.getId()); + }); + } + + private void onPeerConnected(final Peer peer) { + peerSelectionStrategy + .selectPeersToDisconnect(network, peerPools) + .forEach( + peerToDrop -> + peerToDrop.disconnectCleanly(DisconnectReason.TOO_MANY_PEERS).reportExceptions()); + } + + @Override + protected SafeFuture doStop() { + network.unsubscribeConnect(peerConnectedSubscriptionId); + final Cancellable peerSearchTask = this.periodicPeerSearch; + if (peerSearchTask != null) { + peerSearchTask.cancel(); + } + return SafeFuture.COMPLETE; + } + + public synchronized void addStaticPeer(final PeerAddress peerAddress) { + if (!staticPeers.contains(peerAddress)) { + staticPeers.add(peerAddress); + createPersistentConnection(peerAddress); + } + } + + private void createPersistentConnection(final PeerAddress peerAddress) { + maintainPersistentConnection(peerAddress).reportExceptions(); + } + + private SafeFuture maintainPersistentConnection(final PeerAddress peerAddress) { + if (!isRunning()) { + // We've been stopped so halt the process. + return new SafeFuture<>(); + } + LOG.debug("Connecting to peer {}", peerAddress); + peerPools.addPeerToPool(peerAddress.getId(), PeerPool.STATIC); + attemptedConnectionCounter.inc(); + return network + .connect(peerAddress) + .thenApply( + peer -> { + LOG.debug("Connection to peer {} was successful", peer.getId()); + successfulConnectionCounter.inc(); + peer.subscribeDisconnect( + (reason, locallyInitiated) -> { + LOG.debug( + "Peer {} disconnected. Will try to reconnect in {} sec", + peerAddress, + RECONNECT_TIMEOUT.toSeconds()); + asyncRunner + .runAfterDelay( + () -> maintainPersistentConnection(peerAddress), RECONNECT_TIMEOUT) + .reportExceptions(); + }); + return peer; + }) + .exceptionallyCompose( + error -> { + LOG.debug( + "Connection to {} failed: {}. Will retry in {} sec", + peerAddress, + error, + RECONNECT_TIMEOUT.toSeconds()); + failedConnectionCounter.inc(); + return asyncRunner.runAfterDelay( + () -> maintainPersistentConnection(peerAddress), RECONNECT_TIMEOUT); + }); + } + + public void addPeerPredicate(final Predicate predicate) { + peerPredicates.add(predicate); + } + + private boolean isPeerValid(DiscoveryPeer peer) { + return !peer.getNodeAddress().getAddress().isAnyLocalAddress() + && peerPredicates.stream().allMatch(predicate -> predicate.test(peer)); + } +} diff --git a/src/org/minima/system/network/base/DelayedExecutorAsyncRunner.java b/src/org/minima/system/network/base/DelayedExecutorAsyncRunner.java new file mode 100644 index 000000000..eff7cf3b3 --- /dev/null +++ b/src/org/minima/system/network/base/DelayedExecutorAsyncRunner.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * An AsyncRunner that uses the common ForkJoinPool so that it is guaranteed not to leak threads + * even if the test doesn't shut it down. + */ +public class DelayedExecutorAsyncRunner implements AsyncRunner { + private static final Logger LOG = LogManager.getLogger(); + private final ExecutorFactory executorFactory; + + private DelayedExecutorAsyncRunner(ExecutorFactory executorFactory) { + this.executorFactory = executorFactory; + } + + public static DelayedExecutorAsyncRunner create() { + return new DelayedExecutorAsyncRunner(CompletableFuture::delayedExecutor); + } + + @Override + public SafeFuture runAsync(final ExceptionThrowingFutureSupplier action) { + final Executor executor = getAsyncExecutor(); + return runAsync(action, executor); + } + + @Override + public SafeFuture runAfterDelay( + ExceptionThrowingFutureSupplier action, Duration delay) { + final Executor executor = getDelayedExecutor(delay.toMillis(), TimeUnit.MILLISECONDS); + return runAsync(action, executor); + } + + @Override + public void shutdown() {} + + @VisibleForTesting + SafeFuture runAsync( + final ExceptionThrowingFutureSupplier action, final Executor executor) { + final SafeFuture result = new SafeFuture<>(); + try { + executor.execute(() -> SafeFuture.of(action).propagateTo(result)); + } catch (final RejectedExecutionException ex) { + LOG.debug("shutting down ", ex); + } catch (final Throwable t) { + result.completeExceptionally(t); + } + return result; + } + + private Executor getAsyncExecutor() { + return getDelayedExecutor(-1, TimeUnit.SECONDS); + } + + private Executor getDelayedExecutor(long delayAmount, TimeUnit delayUnit) { + return executorFactory.create(delayAmount, delayUnit); + } + + private interface ExecutorFactory { + Executor create(long delayAmount, TimeUnit delayUnit); + } +} diff --git a/src/org/minima/system/network/base/DelegatingP2PNetwork.java b/src/org/minima/system/network/base/DelegatingP2PNetwork.java new file mode 100644 index 000000000..7733df2bc --- /dev/null +++ b/src/org/minima/system/network/base/DelegatingP2PNetwork.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +// import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.gossip.TopicChannel; +// import tech.pegasys.teku.networking.p2p.gossip.TopicHandler; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipTopicsScoringConfig; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; + +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.gossip.TopicChannel; +import org.minima.system.network.base.gossip.TopicHandler; +import org.minima.system.network.base.gossip.config.GossipTopicsScoringConfig; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.NodeId; +import org.minima.system.network.base.peer.Peer; +import org.minima.system.network.base.peer.PeerAddress; + +public abstract class DelegatingP2PNetwork implements P2PNetwork { + private final P2PNetwork network; + + protected DelegatingP2PNetwork(final P2PNetwork network) { + this.network = network; + } + + @Override + public SafeFuture connect(final PeerAddress peer) { + return network.connect(peer); + } + + @Override + public PeerAddress createPeerAddress(final DiscoveryPeer discoveryPeer) { + return network.createPeerAddress(discoveryPeer); + } + + @Override + public NodeId parseNodeId(final String nodeId) { + return network.parseNodeId(nodeId); + } + + @Override + public boolean isConnected(final PeerAddress peerAddress) { + return network.isConnected(peerAddress); + } + + @Override + public Bytes getPrivateKey() { + return network.getPrivateKey(); + } + + @Override + public PeerAddress createPeerAddress(final String peerAddress) { + return network.createPeerAddress(peerAddress); + } + + @Override + public int getPeerCount() { + return network.getPeerCount(); + } + + @Override + public String getNodeAddress() { + return network.getNodeAddress(); + } + + @Override + public NodeId getNodeId() { + return network.getNodeId(); + } + + @Override + public Optional getEnr() { + return network.getEnr(); + } + + @Override + public Optional getDiscoveryAddress() { + return network.getDiscoveryAddress(); + } + + @Override + public int getListenPort() { + return network.getListenPort(); + } + + @Override + public SafeFuture start() { + return network.start(); + } + + @Override + public SafeFuture stop() { + return network.stop(); + } + + @Override + public SafeFuture gossip(final String topic, final Bytes data) { + return network.gossip(topic, data); + } + + @Override + public TopicChannel subscribe(final String topic, final TopicHandler topicHandler) { + return network.subscribe(topic, topicHandler); + } + + @Override + public Map> getSubscribersByTopic() { + return network.getSubscribersByTopic(); + } + + @Override + public void updateGossipTopicScoring(final GossipTopicsScoringConfig config) { + network.updateGossipTopicScoring(config); + } +} diff --git a/src/org/minima/system/network/base/DiscV5Service.java b/src/org/minima/system/network/base/DiscV5Service.java new file mode 100644 index 000000000..3e85ae9cf --- /dev/null +++ b/src/org/minima/system/network/base/DiscV5Service.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +//import static tech.pegasys.teku.util.config.Constants.ATTESTATION_SUBNET_COUNT; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.MultiaddrUtil; +import org.minima.system.network.base.ssz.SszBitvector; +import org.apache.tuweni.units.bigints.UInt64; +import org.ethereum.beacon.discovery.DiscoverySystem; +import org.ethereum.beacon.discovery.DiscoverySystemBuilder; +import org.ethereum.beacon.discovery.schema.EnrField; +import org.ethereum.beacon.discovery.schema.NodeRecord; +import org.ethereum.beacon.discovery.schema.NodeRecordBuilder; +import org.ethereum.beacon.discovery.schema.NodeRecordInfo; +import org.ethereum.beacon.discovery.schema.NodeStatus; +import org.ethereum.beacon.discovery.storage.NewAddressHandler; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; + +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryService; +// import tech.pegasys.teku.networking.p2p.libp2p.MultiaddrUtil; +// import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +// import tech.pegasys.teku.service.serviceutils.Service; +// import tech.pegasys.teku.ssz.collections.SszBitvector; +// import tech.pegasys.teku.ssz.schema.collections.SszBitvectorSchema; +// import tech.pegasys.teku.storage.store.KeyValueStore; +import org.minima.system.network.base.ssz.SszBitvectorSchema; + +public class DiscV5Service extends Service implements DiscoveryService { + private static final String SEQ_NO_STORE_KEY = "local-enr-seqno"; + static final SszBitvectorSchema SUBNET_SUBSCRIPTIONS_SCHEMA = + SszBitvectorSchema.create(DiscoveryNetwork.ATTESTATION_SUBNET_COUNT); + + public static DiscoveryService create( + final DiscoveryConfig discoConfig, + final NetworkConfig p2pConfig, + final KeyValueStore kvStore, + final Bytes privateKey) { + return new DiscV5Service(discoConfig, p2pConfig, kvStore, privateKey); + } + + private final DiscoverySystem discoverySystem; + private final KeyValueStore kvStore; + + private static final Logger LOG = LogManager.getLogger(DiscV5Service.class); + + private DiscV5Service( + final DiscoveryConfig discoConfig, + NetworkConfig p2pConfig, + KeyValueStore kvStore, + final Bytes privateKey) { + final String listenAddress = p2pConfig.getNetworkInterface(); + final int listenPort = p2pConfig.getListenPort(); + final String advertisedAddress = p2pConfig.getAdvertisedIp(); + final int advertisedPort = p2pConfig.getAdvertisedPort(); + final List bootnodes = discoConfig.getBootnodes(); + final UInt64 seqNo = + kvStore.get(SEQ_NO_STORE_KEY).map(UInt64::fromBytes).orElse(UInt64.ZERO).add(1); + final NewAddressHandler maybeUpdateNodeRecordHandler = + maybeUpdateNodeRecord(p2pConfig.hasUserExplicitlySetAdvertisedIp()); + discoverySystem = + new DiscoverySystemBuilder() + .listen(listenAddress, listenPort) + .privateKey(privateKey) + .bootnodes(bootnodes.toArray(new String[0])) + .localNodeRecord( + new NodeRecordBuilder() + .privateKey(privateKey) + .address(advertisedAddress, advertisedPort) + .seq(seqNo) + .build()) + .newAddressHandler(maybeUpdateNodeRecordHandler) + .localNodeRecordListener(this::localNodeRecordUpdated) + .build(); + this.kvStore = kvStore; + } + + private NewAddressHandler maybeUpdateNodeRecord(boolean userExplicitlySetAdvertisedIpOrPort) { + return (oldRecord, proposedNewRecord) -> { + if (userExplicitlySetAdvertisedIpOrPort) { + return Optional.of(oldRecord); + } else { + return Optional.of(proposedNewRecord); + } + }; + } + + private void localNodeRecordUpdated(NodeRecord oldRecord, NodeRecord newRecord) { + LOG.info("Updating NodeRecord for " + newRecord.getNodeId() + "(" + newRecord.getTcpAddress() + ")"); + kvStore.put(SEQ_NO_STORE_KEY, newRecord.getSeq().toBytes()); + } + + @Override + protected SafeFuture doStart() { + LOG.info("Starting discovery system"); + return SafeFuture.of(discoverySystem.start()); + } + + @Override + protected SafeFuture doStop() { + LOG.info("Stopping discovery system"); + discoverySystem.stop(); + return SafeFuture.completedFuture(null); + } + + @Override + public Stream streamKnownPeers() { +// LOG.info("Returning all active nodes as known peers - " + activeNodes().count()); + return activeNodes().map(NodeRecordConverter::convertToDiscoveryPeer).flatMap(Optional::stream); + } + + @Override + public SafeFuture searchForPeers() { + LOG.info("Searching for new peers"); + return SafeFuture.of(discoverySystem.searchForNewPeers()); + } + + @Override + public Optional getEnr() { + return Optional.of(discoverySystem.getLocalNodeRecord().asEnr()); + } + + @Override + public Optional getDiscoveryAddress() { + final NodeRecord nodeRecord = discoverySystem.getLocalNodeRecord(); + if (nodeRecord.getUdpAddress().isEmpty()) { + return Optional.empty(); + } + final DiscoveryPeer discoveryPeer = + new DiscoveryPeer( + (Bytes) nodeRecord.get(EnrField.PKEY_SECP256K1), + nodeRecord.getUdpAddress().get(), + Optional.empty(), + SUBNET_SUBSCRIPTIONS_SCHEMA.getDefault()); + + return Optional.of(MultiaddrUtil.fromDiscoveryPeerAsUdp(discoveryPeer).toString()); + } + + @Override + public void updateCustomENRField(String fieldName, Bytes value) { + discoverySystem.updateCustomFieldValue(fieldName, value); + } + + private Stream activeNodes() { + // LOG.info("Returning all nodes known by discovery system and active"); + return discoverySystem + .streamKnownNodes() + .filter(record -> record.getStatus() == NodeStatus.ACTIVE) + .map(NodeRecordInfo::getNode); + } +} diff --git a/src/org/minima/system/network/base/DiscoveryConfig.java b/src/org/minima/system/network/base/DiscoveryConfig.java new file mode 100644 index 000000000..0acf98428 --- /dev/null +++ b/src/org/minima/system/network/base/DiscoveryConfig.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collections; +import java.util.List; + +public class DiscoveryConfig { + private final boolean isDiscoveryEnabled; + private final List staticPeers; + private final List bootnodes; + private final int minPeers; + private final int maxPeers; + private final int minRandomlySelectedPeers; + + private DiscoveryConfig( + final boolean isDiscoveryEnabled, + final List staticPeers, + final List bootnodes, + final int minPeers, + final int maxPeers, + final int minRandomlySelectedPeers) { + this.isDiscoveryEnabled = isDiscoveryEnabled; + this.staticPeers = staticPeers; + this.bootnodes = bootnodes; + this.minPeers = minPeers; + this.maxPeers = maxPeers; + this.minRandomlySelectedPeers = minRandomlySelectedPeers; + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isDiscoveryEnabled() { + return isDiscoveryEnabled; + } + + public List getStaticPeers() { + return staticPeers; + } + + public List getBootnodes() { + return bootnodes; + } + + public int getMinPeers() { + return minPeers; + } + + public int getMaxPeers() { + return maxPeers; + } + + public int getMinRandomlySelectedPeers() { + return minRandomlySelectedPeers; + } + + public static class Builder { + private Boolean isDiscoveryEnabled = true; + private List staticPeers = Collections.emptyList(); + private List bootnodes = Collections.emptyList(); + private int minPeers = 64; + private int maxPeers = 74; + private int minRandomlySelectedPeers = 2; + + private Builder() {} + + public Builder isDiscoveryEnabled(final Boolean discoveryEnabled) { + checkNotNull(discoveryEnabled); + isDiscoveryEnabled = discoveryEnabled; + return this; + } + + public DiscoveryConfig build() { + return new DiscoveryConfig( + isDiscoveryEnabled, staticPeers, bootnodes, minPeers, maxPeers, minRandomlySelectedPeers); + } + + public Builder staticPeers(final List staticPeers) { + checkNotNull(staticPeers); + this.staticPeers = staticPeers; + return this; + } + + public Builder bootnodes(final List bootnodes) { + checkNotNull(bootnodes); + this.bootnodes = bootnodes; + return this; + } + + public Builder minPeers(final Integer minPeers) { + checkNotNull(minPeers); + this.minPeers = minPeers; + return this; + } + + public Builder maxPeers(final Integer maxPeers) { + checkNotNull(maxPeers); + this.maxPeers = maxPeers; + return this; + } + + public Builder minRandomlySelectedPeers(final Integer minRandomlySelectedPeers) { + checkNotNull(minRandomlySelectedPeers); + this.minRandomlySelectedPeers = minRandomlySelectedPeers; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/DiscoveryNetwork.java b/src/org/minima/system/network/base/DiscoveryNetwork.java new file mode 100644 index 000000000..24ee7cfa8 --- /dev/null +++ b/src/org/minima/system/network/base/DiscoveryNetwork.java @@ -0,0 +1,255 @@ + +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static java.util.stream.Collectors.toList; +//import static tech.pegasys.teku.util.config.Constants.ATTESTATION_SUBNET_COUNT; + +import java.util.Optional; +import java.util.stream.Stream; + +import com.google.common.io.ByteSink; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.NodeId; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +// import org.hyperledger.besu.plugin.services.MetricsSystem; +// import tech.pegasys.teku.infrastructure.async.AsyncRunner; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.logging.StatusLogger; +// import tech.pegasys.teku.infrastructure.unsigned.UInt64; +// import tech.pegasys.teku.networking.p2p.connection.ConnectionManager; +// import tech.pegasys.teku.networking.p2p.connection.PeerSelectionStrategy; +// import tech.pegasys.teku.networking.p2p.discovery.discv5.DiscV5Service; +// import tech.pegasys.teku.networking.p2p.discovery.noop.NoOpDiscoveryService; +// import tech.pegasys.teku.networking.p2p.network.DelegatingP2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; +// import tech.pegasys.teku.ssz.schema.collections.SszBitvectorSchema; +// import tech.pegasys.teku.ssz.type.Bytes4; +import org.minima.system.network.base.peer.Peer; +import org.minima.system.network.base.peer.PeerConnectedSubscriber; +import org.minima.system.network.base.peer.PeerSelectionStrategy; +import org.minima.system.network.base.ssz.SszBitvectorSchema; + +// import tech.pegasys.teku.storage.store.KeyValueStore; + +public class DiscoveryNetwork

extends DelegatingP2PNetwork

{ + private static final Logger LOG = LogManager.getLogger(DiscoveryNetwork.class); + + public static final String ATTESTATION_SUBNET_ENR_FIELD = "attnets"; + public static final String ETH2_ENR_FIELD = "eth2"; + + // from tech.pegasys.teku.util.config.Constants.ATTESTATION_SUBNET_COUNT + public static final int ATTESTATION_SUBNET_COUNT = 64; + + private final P2PNetwork

p2pNetwork; + private final DiscoveryService discoveryService; + private final ConnectionManager connectionManager; + + private volatile Optional enrForkId = Optional.empty(); + + private String mENR; + + DiscoveryNetwork( + final P2PNetwork

p2pNetwork, + final DiscoveryService discoveryService, + final ConnectionManager connectionManager) { + super(p2pNetwork); + this.p2pNetwork = p2pNetwork; + this.discoveryService = discoveryService; + this.connectionManager = connectionManager; + initialize(); + } + + public void initialize() { + //TODO: set ENR data set by setpregenesisforkinfo + //setPreGenesisForkInfo(); + //TODO: log this in another way + // getEnr().ifPresent(StatusLogger.STATUS_LOG::listeningForDiscv5PreGenesis); + + // Set connection manager peer predicate so that we don't attempt to connect peers with + // different fork digests +// connectionManager.addPeerPredicate(this::dontConnectPeersWithDifferentForkDigests); + } + + public static

DiscoveryNetwork

create( + final MetricsSystem metricsSystem, + final AsyncRunner asyncRunner, + final KeyValueStore kvStore, + final P2PNetwork

p2pNetwork, + final PeerSelectionStrategy peerSelectionStrategy, + final DiscoveryConfig discoveryConfig, + final NetworkConfig p2pConfig + ) { + final DiscoveryService discoveryService = + createDiscoveryService(discoveryConfig, p2pConfig, kvStore, Bytes.wrap(p2pNetwork.getPrivateKey())); + final ConnectionManager connectionManager = + new ConnectionManager( + metricsSystem, + discoveryService, + asyncRunner, + p2pNetwork, + peerSelectionStrategy, + discoveryConfig.getStaticPeers().stream() + .map(p2pNetwork::createPeerAddress) + .collect(toList())); + return new DiscoveryNetwork<>(p2pNetwork, discoveryService, connectionManager); + } + + private static DiscoveryService createDiscoveryService( + final DiscoveryConfig discoConfig, + final NetworkConfig p2pConfig, + final KeyValueStore kvStore, + final Bytes privateKey) { + final DiscoveryService discoveryService; + if (discoConfig.isDiscoveryEnabled()) { + System.out.println("P2P: Starting DiscV5 service"); + discoveryService = DiscV5Service.create(discoConfig, p2pConfig, kvStore, privateKey); + //discoveryService = new NoOpDiscoveryService(); + } else { + System.out.println("P2P: Starting NoOp Disc service"); + discoveryService = new NoOpDiscoveryService(); + } + return discoveryService; + } + + public String getENR() { + return mENR; + } + + @Override + public SafeFuture start() { + return SafeFuture.allOfFailFast(p2pNetwork.start(), discoveryService.start()) + .thenCompose(__ -> connectionManager.start()) + .thenRun(() -> getEnr().ifPresent( + enr -> { + LOG.warn("logwarn: listening for discv5: " + enr); + System.out.println("sysout: listening for discv5: " + enr); + mENR = enr; + })); + } //::listeningForDiscv5 + + @Override + public SafeFuture stop() { + return connectionManager + .stop() + .handleComposed( + (__, err) -> { + if (err != null) { + LOG.warn("Error shutting down connection manager", err); + } + return SafeFuture.allOf(p2pNetwork.stop(), discoveryService.stop()); + }); + } + + public void addStaticPeer(final String peerAddress) { + connectionManager.addStaticPeer(p2pNetwork.createPeerAddress(peerAddress)); + } + + @Override + public Optional getEnr() { + return discoveryService.getEnr(); + } + + @Override + public Optional getDiscoveryAddress() { + return discoveryService.getDiscoveryAddress(); + } + + public void setLongTermAttestationSubnetSubscriptions(Iterable subnetIds) { + discoveryService.updateCustomENRField( + ATTESTATION_SUBNET_ENR_FIELD, + SszBitvectorSchema.create(ATTESTATION_SUBNET_COUNT).ofBits(subnetIds).sszSerialize()); + } + + // public void setPreGenesisForkInfo() { + // final Bytes4 genesisForkVersion = spec.getGenesisSpecConfig().getGenesisForkVersion(); + // final EnrForkId enrForkId = + // new EnrForkId( + // spec.getGenesisBeaconStateUtil().computeForkDigest(genesisForkVersion, Bytes32.ZERO), + // genesisForkVersion, + // SpecConfig.FAR_FUTURE_EPOCH); + // discoveryService.updateCustomENRField(ETH2_ENR_FIELD, enrForkId.sszSerialize()); + // this.enrForkId = Optional.of(enrForkId); + // } + + // public void setForkInfo(final ForkInfo currentForkInfo, final Optional nextForkInfo) { + // // If no future fork is planned, set next_fork_version = current_fork_version to signal this + // final Bytes4 nextVersion = + // nextForkInfo + // .map(Fork::getCurrent_version) + // .orElse(currentForkInfo.getFork().getCurrent_version()); + // // If no future fork is planned, set next_fork_epoch = FAR_FUTURE_EPOCH to signal this + // final UInt64 nextForkEpoch = + // nextForkInfo.map(Fork::getEpoch).orElse(SpecConfig.FAR_FUTURE_EPOCH); + + // final Bytes4 forkDigest = currentForkInfo.getForkDigest(); + // final EnrForkId enrForkId = new EnrForkId(forkDigest, nextVersion, nextForkEpoch); + // final Bytes encodedEnrForkId = enrForkId.sszSerialize(); + + // discoveryService.updateCustomENRField(ETH2_ENR_FIELD, encodedEnrForkId); + // this.enrForkId = Optional.of(enrForkId); + // } + + // private boolean dontConnectPeersWithDifferentForkDigests(DiscoveryPeer peer) { + // return enrForkId + // .map(EnrForkId::getForkDigest) + // .flatMap( + // localForkDigest -> + // peer.getEnrForkId() + // .map(EnrForkId::getForkDigest) + // .map(peerForkDigest -> peerForkDigest.equals(localForkDigest))) + // .orElse(false); + // } + + @Override + public long subscribeConnect(final PeerConnectedSubscriber

subscriber) { + return p2pNetwork.subscribeConnect(subscriber); + } + + @Override + public void unsubscribeConnect(final long subscriptionId) { + p2pNetwork.unsubscribeConnect(subscriptionId); + } + + @Override + public Optional

getPeer(final NodeId id) { + return p2pNetwork.getPeer(id); + } + + @Override + public Stream

streamPeers() { + return p2pNetwork.streamPeers(); + } + + public Stream streamKnownDiscoveryPeers() { + return discoveryService.streamKnownPeers(); + } + + public int getP2PPeerCount() { + LibP2PNetwork net = (LibP2PNetwork) p2pNetwork; + return net.getPeerCount(); + } + +} + diff --git a/src/org/minima/system/network/base/DiscoveryNetworkFactory.java b/src/org/minima/system/network/base/DiscoveryNetworkFactory.java new file mode 100644 index 000000000..c63c773f3 --- /dev/null +++ b/src/org/minima/system/network/base/DiscoveryNetworkFactory.java @@ -0,0 +1,164 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.net.BindException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.libp2p.core.crypto.PrivKey; + +// import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +// import tech.pegasys.teku.infrastructure.async.DelayedExecutorAsyncRunner; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.async.Waiter; +// import tech.pegasys.teku.infrastructure.time.StubTimeProvider; +// import tech.pegasys.teku.network.p2p.jvmlibp2p.PrivateKeyGenerator; +// import tech.pegasys.teku.network.p2p.peer.SimplePeerSelectionStrategy; +// import tech.pegasys.teku.networking.p2p.connection.PeerSelectionStrategy; +// import tech.pegasys.teku.networking.p2p.connection.TargetPeerRange; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork; +// import tech.pegasys.teku.networking.p2p.libp2p.LibP2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; +// import tech.pegasys.teku.spec.Spec; +// import tech.pegasys.teku.spec.SpecFactory; +// import tech.pegasys.teku.storage.store.MemKeyValueStore; +// import tech.pegasys.teku.util.config.Constants; +import org.minima.system.network.base.metrics.NoOpMetricsSystem; +import org.minima.system.network.base.peer.Peer; +import org.minima.system.network.base.peer.PeerSelectionStrategy; +import org.minima.system.network.base.peer.ReputationManager; +import org.minima.system.network.base.peer.SimplePeerSelectionStrategy; +import org.minima.system.network.base.peer.TargetPeerRange; + +public class DiscoveryNetworkFactory { + + protected static final Logger LOG = LogManager.getLogger(DiscoveryNetworkFactory.class); + protected static final NoOpMetricsSystem METRICS_SYSTEM = new NoOpMetricsSystem(); + private static final int MIN_PORT = 9000; + private static final int MAX_PORT = 12000; + + private final List> networks = new ArrayList<>(); + + // from tech.pegasys.teku.util.config.Constants + public final class Constants { + public static final int REPUTATION_MANAGER_CAPACITY = 1024; + public static final int ATTESTATION_SUBNET_COUNT = 64; + } + + public DiscoveryNetworkBuilder builder() { + return new DiscoveryNetworkBuilder(); + } + + public void stopAll() throws InterruptedException, ExecutionException, TimeoutException { + Waiter.waitFor( + SafeFuture.allOf(networks.stream().map(DiscoveryNetwork::stop).toArray(SafeFuture[]::new))); + } + + public class DiscoveryNetworkBuilder { + private final List staticPeers = new ArrayList<>(); + private final List bootnodes = new ArrayList<>(); + private PrivKey privKey; + + private DiscoveryNetworkBuilder() {} + + public DiscoveryNetworkBuilder staticPeer(final String staticPeer) { + this.staticPeers.add(staticPeer); + return this; + } + + public DiscoveryNetworkBuilder bootnode(final String bootnode) { + this.bootnodes.add(bootnode); + return this; + } + + public DiscoveryNetworkBuilder setPrivKey(final PrivKey privKey) { + this.privKey = privKey; + return this; + } + public DiscoveryNetwork buildAndStart(int _port) throws Exception { + int attempt = 1; + while (true) { + final int port; + if(_port == 0) { + final Random random = new Random(); + port = MIN_PORT + random.nextInt(MAX_PORT - MIN_PORT); + } else { + port = _port; + } + final DiscoveryConfig discoveryConfig = + DiscoveryConfig.builder().staticPeers(staticPeers).bootnodes(bootnodes).build(); + final NetworkConfig config = + NetworkConfig.builder().listenPort(port).networkInterface("0.0.0.0").build(); + final NoOpMetricsSystem metricsSystem = new NoOpMetricsSystem(); + final ReputationManager reputationManager = + new ReputationManager( + metricsSystem, + StubTimeProvider.withTimeInSeconds(1000), + Constants.REPUTATION_MANAGER_CAPACITY); + final PeerSelectionStrategy peerSelectionStrategy = + new SimplePeerSelectionStrategy(new TargetPeerRange(20, 30, 0)); + + final DiscoveryNetwork network = + DiscoveryNetwork.create( + metricsSystem, + DelayedExecutorAsyncRunner.create(), + new MemKeyValueStore<>(), + new LibP2PNetwork( + DelayedExecutorAsyncRunner.create(), + config, + privKey, + reputationManager, + METRICS_SYSTEM, + Collections.emptyList(), + Collections.emptyList(), + (__1, __2) -> { + throw new UnsupportedOperationException(); + }, + topic -> true), + peerSelectionStrategy, + discoveryConfig, + config); + try { + network.start().get(5, TimeUnit.SECONDS); + networks.add(network); + return network; + } catch (final ExecutionException e) { + if (e.getCause() instanceof BindException) { + if (attempt > 10) { + throw new RuntimeException("Failed to find a free port after multiple attempts", e); + } + LOG.info( + "Port conflict detected, retrying with a new port. Original message: {}", + e.getMessage()); + attempt++; + Waiter.waitFor(network.stop()); + } else { + throw e; + } + } + } + } + } +} diff --git a/src/org/minima/system/network/base/DiscoveryService.java b/src/org/minima/system/network/base/DiscoveryService.java new file mode 100644 index 000000000..184eb9935 --- /dev/null +++ b/src/org/minima/system/network/base/DiscoveryService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; +import org.minima.system.network.base.peer.DiscoveryPeer; + +public interface DiscoveryService { + + SafeFuture start(); + + SafeFuture stop(); + + Stream streamKnownPeers(); + + SafeFuture searchForPeers(); + + Optional getEnr(); + + Optional getDiscoveryAddress(); + + void updateCustomENRField(String fieldName, Bytes value); +} diff --git a/src/org/minima/system/network/base/EnrForkId.java b/src/org/minima/system/network/base/EnrForkId.java new file mode 100644 index 000000000..08bf7670f --- /dev/null +++ b/src/org/minima/system/network/base/EnrForkId.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import org.minima.system.network.base.ssz.Bytes4; +import org.minima.system.network.base.ssz.Container3; +import org.minima.system.network.base.ssz.ContainerSchema3; +import org.minima.system.network.base.ssz.SszBytes4; +import org.minima.system.network.base.ssz.SszPrimitiveSchemas; +import org.minima.system.network.base.ssz.SszUInt64; +import org.minima.system.network.base.ssz.UInt64; +import org.minima.system.network.base.ssz.TreeNode; + +// import tech.pegasys.teku.infrastructure.unsigned.UInt64; +// import tech.pegasys.teku.ssz.containers.Container3; +// import tech.pegasys.teku.ssz.containers.ContainerSchema3; +// import tech.pegasys.teku.ssz.primitive.SszBytes4; +// import tech.pegasys.teku.ssz.primitive.SszUInt64; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.type.Bytes4; + +public class EnrForkId extends Container3 { + + public static class EnrForkIdSchema + extends ContainerSchema3 { + + public EnrForkIdSchema() { + super( + "EnrForkId", + namedSchema("forkDigest", SszPrimitiveSchemas.BYTES4_SCHEMA), + namedSchema("nextForkVersion", SszPrimitiveSchemas.BYTES4_SCHEMA), + namedSchema("nextForkEpoch", SszPrimitiveSchemas.UINT64_SCHEMA)); + } + + @Override + public EnrForkId createFromBackingNode(TreeNode node) { + return new EnrForkId(this, node); + } + } + + public static final EnrForkIdSchema SSZ_SCHEMA = new EnrForkIdSchema(); + + private EnrForkId(EnrForkIdSchema type, TreeNode backingNode) { + super(type, backingNode); + } + + public EnrForkId( + final Bytes4 forkDigest, final Bytes4 nextForkVersion, final UInt64 nextForkEpoch) { + super( + SSZ_SCHEMA, + SszBytes4.of(forkDigest), + SszBytes4.of(nextForkVersion), + SszUInt64.of(nextForkEpoch)); + } + + public Bytes4 getForkDigest() { + return getField0().get(); + } + + public Bytes4 getNextForkVersion() { + return getField1().get(); + } + + public UInt64 getNextForkEpoch() { + return getField2().get(); + } +} diff --git a/src/org/minima/system/network/base/ExceptionThrowingConsumer.java b/src/org/minima/system/network/base/ExceptionThrowingConsumer.java new file mode 100644 index 000000000..2cc4ce007 --- /dev/null +++ b/src/org/minima/system/network/base/ExceptionThrowingConsumer.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +public interface ExceptionThrowingConsumer { + + void accept(V value) throws Throwable; +} diff --git a/src/org/minima/system/network/base/ExceptionThrowingFunction.java b/src/org/minima/system/network/base/ExceptionThrowingFunction.java new file mode 100644 index 000000000..58bee42d1 --- /dev/null +++ b/src/org/minima/system/network/base/ExceptionThrowingFunction.java @@ -0,0 +1,18 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +public interface ExceptionThrowingFunction { + O apply(I value) throws Throwable; +} diff --git a/src/org/minima/system/network/base/ExceptionThrowingFutureSupplier.java b/src/org/minima/system/network/base/ExceptionThrowingFutureSupplier.java new file mode 100644 index 000000000..1648b11b3 --- /dev/null +++ b/src/org/minima/system/network/base/ExceptionThrowingFutureSupplier.java @@ -0,0 +1,8 @@ + +package org.minima.system.network.base; + +import java.util.concurrent.CompletionStage; + +public interface ExceptionThrowingFutureSupplier { + CompletionStage get() throws Throwable; +} diff --git a/src/org/minima/system/network/base/ExceptionThrowingRunnable.java b/src/org/minima/system/network/base/ExceptionThrowingRunnable.java new file mode 100644 index 000000000..4ea79c94a --- /dev/null +++ b/src/org/minima/system/network/base/ExceptionThrowingRunnable.java @@ -0,0 +1,18 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +public interface ExceptionThrowingRunnable { + void run() throws Throwable; +} diff --git a/src/org/minima/system/network/base/ExceptionThrowingSupplier.java b/src/org/minima/system/network/base/ExceptionThrowingSupplier.java new file mode 100644 index 000000000..b7ac9b693 --- /dev/null +++ b/src/org/minima/system/network/base/ExceptionThrowingSupplier.java @@ -0,0 +1,6 @@ + +package org.minima.system.network.base; + +public interface ExceptionThrowingSupplier { + O get() throws Throwable; +} diff --git a/src/org/minima/system/network/base/Firewall.java b/src/org/minima/system/network/base/Firewall.java new file mode 100644 index 000000000..91bcaf37e --- /dev/null +++ b/src/org/minima/system/network/base/Firewall.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.timeout.WriteTimeoutException; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +//import tech.pegasys.teku.infrastructure.async.FutureUtil; + +/** + * The very first Netty handler in the Libp2p connection pipeline. Sets up Netty Channel options and + * doing other duties preventing DoS attacks + */ +@Sharable +public class Firewall extends ChannelInboundHandlerAdapter { + private static final Logger LOG = LogManager.getLogger(); + + private final Duration writeTimeout; + + public Firewall(Duration writeTimeout) { + this.writeTimeout = writeTimeout; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + ctx.channel().config().setWriteBufferWaterMark(new WriteBufferWaterMark(100, 1024)); + ctx.pipeline().addLast(new WriteTimeoutHandler(writeTimeout.toMillis(), TimeUnit.MILLISECONDS)); + ctx.pipeline().addLast(new FirewallExceptionHandler()); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(ctx.channel().isWritable()); + ctx.fireChannelWritabilityChanged(); + } + + class FirewallExceptionHandler extends ChannelInboundHandlerAdapter { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof WriteTimeoutException) { + LOG.debug("Firewall closed channel by write timeout. No writes during " + writeTimeout); + } else { + LOG.debug("Error in Firewall, disconnecting" + cause); + FutureUtil.ignoreFuture(ctx.close()); + } + } + } +} diff --git a/src/org/minima/system/network/base/FutureUtil.java b/src/org/minima/system/network/base/FutureUtil.java new file mode 100644 index 000000000..c06646e8d --- /dev/null +++ b/src/org/minima/system/network/base/FutureUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.time.Duration; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class FutureUtil { + private static final Logger LOG = LogManager.getLogger(); + + public static void ignoreFuture(final Future future) {} + + static void runWithFixedDelay( + AsyncRunner runner, + ExceptionThrowingRunnable runnable, + Cancellable task, + final Duration duration, + Consumer exceptionHandler) { + + runner + .runAfterDelay( + () -> { + if (!task.isCancelled()) { + try { + runnable.run(); + } catch (Throwable throwable) { + try { + exceptionHandler.accept(throwable); + } catch (Exception e) { + LOG.warn("Exception in exception handler", e); + } + } finally { + runWithFixedDelay(runner, runnable, task, duration, exceptionHandler); + } + } + }, + duration) + .finish(() -> {}, exceptionHandler); + } + + static Cancellable createCancellable() { + return new Cancellable() { + private volatile boolean cancelled; + + @Override + public void cancel() { + cancelled = true; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + }; + } +} diff --git a/src/org/minima/system/network/base/InvalidConfigurationException.java b/src/org/minima/system/network/base/InvalidConfigurationException.java new file mode 100644 index 000000000..b239e9db5 --- /dev/null +++ b/src/org/minima/system/network/base/InvalidConfigurationException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +public class InvalidConfigurationException extends RuntimeException { + public InvalidConfigurationException(final String message) { + super(message); + } + + public InvalidConfigurationException(final String message, final Throwable cause) { + super(message, cause); + } + + public InvalidConfigurationException(final Throwable cause) { + super(cause.getMessage(), cause); + } +} diff --git a/src/org/minima/system/network/base/KeyValueStore.java b/src/org/minima/system/network/base/KeyValueStore.java new file mode 100644 index 000000000..95d07ca62 --- /dev/null +++ b/src/org/minima/system/network/base/KeyValueStore.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +/** + * Generic simple key-value store interface Both key and value are not allowed to be null + * + * @param key type + * @param value type + */ +public interface KeyValueStore { + + /** Puts a new value. If the value is {@code null} then the entry is removed if exist */ + void put(@NotNull TKey key, @NotNull TValue value); + + /** Removes entry with the specified key */ + void remove(@NotNull TKey key); + + /** + * Returns a value corresponding to the key or {{@link Optional#empty()}} if entry doesn't exist + * in the store + */ + Optional get(@NotNull TKey key); + + /** + * Performs batch store update. + * + *

The implementation may override this default method and declare it to be an atomic store + * update. Though this generic interface makes no restrictions on atomicity of this method + */ + default void updateAll(Iterable> data) { + data.forEach( + update -> { + if (update.getType() == UpdateType.UPDATE) { + put(update.getKey(), update.getValue()); + } else if (update.getType() == UpdateType.REMOVE) { + remove(update.getKey()); + } else { + throw new IllegalArgumentException("Unknown type: " + update.getType()); + } + }); + } + + enum UpdateType { + UPDATE, + REMOVE + } + + /** Represents a batched update entry */ + class EntryUpdate { + private final UpdateType type; + private final K key; + private final V value; + + public static EntryUpdate update(K key, V value) { + return new EntryUpdate<>(UpdateType.UPDATE, key, value); + } + + public static EntryUpdate remove(K key) { + return new EntryUpdate<>(UpdateType.REMOVE, key, null); + } + + private EntryUpdate(UpdateType type, K key, V value) { + this.type = type; + this.key = key; + this.value = value; + } + + public UpdateType getType() { + return type; + } + + public K getKey() { + return key; + } + + public V getValue() { + if (getType() != UpdateType.UPDATE) { + throw new IllegalStateException("No value for this update"); + } + return value; + } + } +} diff --git a/src/org/minima/system/network/base/LRUCache.java b/src/org/minima/system/network/base/LRUCache.java new file mode 100644 index 000000000..34da71967 --- /dev/null +++ b/src/org/minima/system/network/base/LRUCache.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +//import tech.pegasys.teku.infrastructure.collections.LimitedMap; + +import org.minima.system.network.base.ssz.Cache; + +/** + * Cache made around LRU-map with fixed size, removing eldest entries (by added) when the space is + * over + * + * @param Keys type + * @param Values type + */ +public class LRUCache implements Cache { + + private final Map cacheData; + private final int maxCapacity; + + /** + * Creates cache + * + * @param capacity Size of the cache + */ + public LRUCache(int capacity) { + this(capacity, Collections.emptyMap()); + } + + private LRUCache(int capacity, Map initialCachedContent) { + this.maxCapacity = capacity; + Map cacheMap = LimitedMap.create(maxCapacity); + // copy safely, initialCachedContent is always a SynchronizedMap instance + synchronized (initialCachedContent) { + cacheMap.putAll(initialCachedContent); + } + this.cacheData = cacheMap; + } + + @Override + public Cache copy() { + return new LRUCache<>(maxCapacity, cacheData); + } + + /** + * Queries value from the cache. If it's not found there, fallback function is used to calculate + * value. After calculation result is put in cache and returned. + * + * @param key Key to query + * @param fallback Fallback function for calculation of the result in case of missed cache entry + * @return expected value result for provided key + */ + @Override + public V get(K key, Function fallback) { + V result = cacheData.get(key); + + if (result == null) { + result = fallback.apply(key); + if (result != null) { + cacheData.put(key, result); + } + } + + return result; + } + + @Override + public Optional getCached(K key) { + return Optional.ofNullable(cacheData.get(key)); + } + + @Override + public void invalidate(K key) { + cacheData.remove(key); + } + + @Override + public void clear() { + cacheData.clear(); + } + + @Override + public int size() { + return cacheData.size(); + } +} diff --git a/src/org/minima/system/network/base/LibP2PNetwork.java b/src/org/minima/system/network/base/LibP2PNetwork.java new file mode 100644 index 000000000..51381dcfd --- /dev/null +++ b/src/org/minima/system/network/base/LibP2PNetwork.java @@ -0,0 +1,332 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static org.minima.system.network.base.SafeFuture.failedFuture; +//import static tech.pegasys.teku.infrastructure.logging.StatusLogger.STATUS_LOG; + +import identify.pb.IdentifyOuterClass; +import io.libp2p.core.Host; +import io.libp2p.core.PeerId; +import io.libp2p.core.crypto.PrivKey; +import io.libp2p.core.dsl.Builder.Defaults; +import io.libp2p.core.dsl.BuilderJKt; +import io.libp2p.core.multiformats.Multiaddr; +import io.libp2p.core.multistream.ProtocolBinding; +import io.libp2p.core.mux.StreamMuxerProtocol; +import io.libp2p.etc.types.ByteArrayExtKt; +//import io.libp2p.etc.util.P2PService.PeerHandler; +import io.libp2p.protocol.Identify; +import io.libp2p.protocol.Ping; +import io.libp2p.security.noise.NoiseXXSecureChannel; +import io.libp2p.transport.tcp.TcpTransport; +import io.netty.handler.logging.LogLevel; +import java.net.InetSocketAddress; +import java.time.Duration; +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.AtomicReference; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.gossip.PreparedGossipMessageFactory; +import org.minima.system.network.base.gossip.TopicChannel; +import org.minima.system.network.base.gossip.TopicHandler; +import org.minima.system.network.base.gossip.config.GossipTopicsScoringConfig; +import org.minima.system.network.base.libp2p.gossip.GossipTopicFilter; +import org.minima.system.network.base.libp2p.gossip.LibP2PGossipNetwork; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.LibP2PNodeId; +import org.minima.system.network.base.peer.MultiaddrPeerAddress; +import org.minima.system.network.base.peer.MultiaddrUtil; +import org.minima.system.network.base.peer.NodeId; +//import org.hyperledger.besu.plugin.services.MetricsSystem; +// import tech.pegasys.teku.infrastructure.async.AsyncRunner; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.version.VersionProvider; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; +// import tech.pegasys.teku.networking.p2p.gossip.TopicChannel; +// import tech.pegasys.teku.networking.p2p.gossip.TopicHandler; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipTopicsScoringConfig; +// import tech.pegasys.teku.networking.p2p.libp2p.gossip.GossipTopicFilter; +// import tech.pegasys.teku.networking.p2p.libp2p.gossip.LibP2PGossipNetwork; +// import tech.pegasys.teku.networking.p2p.libp2p.rpc.RpcHandler; +// import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.network.PeerHandler; +// import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; +// import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; +// import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; +import org.minima.system.network.base.peer.Peer; +import org.minima.system.network.base.peer.PeerAddress; +import org.minima.system.network.base.peer.PeerConnectedSubscriber; +import org.minima.system.network.base.peer.PeerManager; +import org.minima.system.network.base.peer.PeerHandler; +import org.minima.system.network.base.peer.ReputationManager; +import org.minima.system.network.base.peer.RpcHandler; +import org.minima.system.network.base.peer.RpcMethod; + +public class LibP2PNetwork implements P2PNetwork { + + private static final Logger LOG = LogManager.getLogger(LibP2PNetwork.class); + private static final int REMOTE_OPEN_STREAMS_RATE_LIMIT = 256; + private static final int REMOTE_PARALLEL_OPEN_STREAMS_COUNT_LIMIT = 256; + + private final PrivKey privKey; + private final NodeId nodeId; + + private final Host host; + private final PeerManager peerManager; + private final Multiaddr advertisedAddr; + private final LibP2PGossipNetwork gossipNetwork; + + private final AtomicReference state = new AtomicReference<>(State.IDLE); + private final Map rpcHandlers = new ConcurrentHashMap<>(); + private final int listenPort; + + public LibP2PNetwork( + final AsyncRunner asyncRunner, + final NetworkConfig config, + final PrivKey privKey, + final ReputationManager reputationManager, + final MetricsSystem metricsSystem, + final List rpcMethods, + final List peerHandlers, + final PreparedGossipMessageFactory defaultMessageFactory, + final GossipTopicFilter gossipTopicFilter) { + + this.privKey = privKey; + this.nodeId = new LibP2PNodeId(PeerId.fromPubKey(privKey.publicKey())); + + System.out.println("LibP2PNetwork - privKey = " + privKey.toString()); + System.out.println("LibP2PNetwork - nodeId = " + nodeId); + advertisedAddr = + MultiaddrUtil.fromInetSocketAddress( + new InetSocketAddress(config.getAdvertisedIp(), config.getAdvertisedPort()), nodeId); + this.listenPort = config.getListenPort(); + + // Setup gossip + gossipNetwork = + LibP2PGossipNetwork.create( + metricsSystem, + config.getGossipConfig(), + defaultMessageFactory, + gossipTopicFilter, + config.getWireLogsConfig().isLogWireGossip()); + + // Setup rpc methods + rpcMethods.forEach(method -> rpcHandlers.put(method, new RpcHandler(asyncRunner, method))); + + // Setup peers + peerManager = new PeerManager(metricsSystem, reputationManager, peerHandlers, rpcHandlers); + + final Multiaddr listenAddr = + MultiaddrUtil.fromInetSocketAddress( + new InetSocketAddress(config.getNetworkInterface(), config.getListenPort())); + host = + BuilderJKt.hostJ( + Defaults.None, + b -> { + b.getIdentity().setFactory(() -> privKey); + b.getTransports().add(TcpTransport::new); + b.getSecureChannels().add(NoiseXXSecureChannel::new); + b.getMuxers().add(StreamMuxerProtocol.getMplex()); + + b.getNetwork().listen(listenAddr.toString()); + + b.getProtocols().addAll(getDefaultProtocols()); + b.getProtocols().addAll(rpcHandlers.values()); + + if (config.getWireLogsConfig().isLogWireCipher()) { + b.getDebug().getBeforeSecureHandler().addLogger(LogLevel.DEBUG, "wire.ciphered"); + } + Firewall firewall = new Firewall(Duration.ofSeconds(30)); + b.getDebug().getBeforeSecureHandler().addNettyHandler(firewall); + + if (config.getWireLogsConfig().isLogWirePlain()) { + b.getDebug().getAfterSecureHandler().addLogger(LogLevel.DEBUG, "wire.plain"); + } + if (config.getWireLogsConfig().isLogWireMuxFrames()) { + b.getDebug().getMuxFramesHandler().addLogger(LogLevel.DEBUG, "wire.mux"); + } + + b.getConnectionHandlers().add(peerManager); + + MplexFirewall mplexFirewall = + new MplexFirewall( + REMOTE_OPEN_STREAMS_RATE_LIMIT, REMOTE_PARALLEL_OPEN_STREAMS_COUNT_LIMIT); + b.getDebug().getMuxFramesHandler().addHandler(mplexFirewall); + }); + } + + private List> getDefaultProtocols() { + final Ping ping = new Ping(); + IdentifyOuterClass.Identify identifyMsg = + IdentifyOuterClass.Identify.newBuilder() + .setProtocolVersion("ipfs/0.1.0") + .setAgentVersion("Minima_0.98.0-testss-p2p-Zulu-OpenJDK-11-AARCH64") + .setPublicKey(ByteArrayExtKt.toProtobuf(privKey.publicKey().bytes())) + .addListenAddrs(ByteArrayExtKt.toProtobuf(advertisedAddr.getBytes())) + .setObservedAddr(ByteArrayExtKt.toProtobuf(advertisedAddr.getBytes())) + .addAllProtocols(ping.getProtocolDescriptor().getAnnounceProtocols()) + .addAllProtocols( + gossipNetwork.getGossip().getProtocolDescriptor().getAnnounceProtocols()) + .build(); + return List.of(ping, new Identify(identifyMsg), gossipNetwork.getGossip()); + } + + @Override + public SafeFuture start() { + if (!state.compareAndSet(State.IDLE, State.RUNNING)) { + return SafeFuture.failedFuture(new IllegalStateException("Network already started")); + } + LOG.info("Starting libp2p network..."); + return SafeFuture.of(host.start()) + .thenApply( + i -> { + //STATUS_LOG.listeningForLibP2P(getNodeAddress()); + LOG.debug("Listening for LibP2P - " + getNodeAddress()); + return null; + }); + } + + @Override + public String getNodeAddress() { + return advertisedAddr.toString(); + } + + @Override + public SafeFuture connect(final PeerAddress peer) { + return peer.as(MultiaddrPeerAddress.class) + .map(staticPeer -> peerManager.connect(staticPeer, host.getNetwork())) + .orElseGet( + () -> + failedFuture( + new IllegalArgumentException( + "Unsupported peer address: " + peer.getClass().getName()))); + } + + @Override + public PeerAddress createPeerAddress(final String peerAddress) { + return MultiaddrPeerAddress.fromAddress(peerAddress); + } + + @Override + public PeerAddress createPeerAddress(final DiscoveryPeer discoveryPeer) { + return MultiaddrPeerAddress.fromDiscoveryPeer(discoveryPeer); + } + + @Override + public long subscribeConnect(final PeerConnectedSubscriber subscriber) { + return peerManager.subscribeConnect(subscriber); + } + + @Override + public void unsubscribeConnect(final long subscriptionId) { + peerManager.unsubscribeConnect(subscriptionId); + } + + @Override + public boolean isConnected(final PeerAddress peerAddress) { + return peerManager.getPeer(peerAddress.getId()).isPresent(); + } + + @Override + public Bytes getPrivateKey() { + return Bytes.wrap(privKey.raw()); + } + + @Override + public Optional getPeer(final NodeId id) { + return peerManager.getPeer(id); + } + + @Override + public Stream streamPeers() { + return peerManager.streamPeers(); + } + + @Override + public NodeId parseNodeId(final String nodeId) { + return new LibP2PNodeId(PeerId.fromBase58(nodeId)); + } + + @Override + public int getPeerCount() { + return peerManager.getPeerCount(); + } + + @Override + public int getListenPort() { + return listenPort; + } + + @Override + public SafeFuture stop() { + if (!state.compareAndSet(State.RUNNING, State.STOPPED)) { + return SafeFuture.COMPLETE; + } + LOG.debug("JvmLibP2PNetwork.stop()"); + return SafeFuture.of(host.stop()); + } + + @Override + public NodeId getNodeId() { + return nodeId; + } + + @Override + public Optional getEnr() { + return Optional.empty(); + } + + @Override + public Optional getDiscoveryAddress() { + return Optional.empty(); + } + + @Override + public SafeFuture gossip(final String topic, final Bytes data) { + return gossipNetwork.gossip(topic, data); + } + + @Override + public TopicChannel subscribe(final String topic, final TopicHandler topicHandler) { + return gossipNetwork.subscribe(topic, topicHandler); + } + + @Override + public Map> getSubscribersByTopic() { + return gossipNetwork.getSubscribersByTopic(); + } + + @Override + public void updateGossipTopicScoring(final GossipTopicsScoringConfig config) { + gossipNetwork.updateGossipTopicScoring(config); + } + + @FunctionalInterface + public interface PrivateKeyProvider { + PrivKey get(); + } +} diff --git a/src/org/minima/system/network/base/LibP2PParamsFactory.java b/src/org/minima/system/network/base/LibP2PParamsFactory.java new file mode 100644 index 000000000..1fe0cfbe7 --- /dev/null +++ b/src/org/minima/system/network/base/LibP2PParamsFactory.java @@ -0,0 +1,155 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import io.libp2p.core.PeerId; +import io.libp2p.pubsub.gossip.GossipParams; +import io.libp2p.pubsub.gossip.GossipPeerScoreParams; +import io.libp2p.pubsub.gossip.GossipScoreParams; +import io.libp2p.pubsub.gossip.GossipTopicScoreParams; +import io.libp2p.pubsub.gossip.GossipTopicsScoreParams; +import io.libp2p.pubsub.gossip.builders.GossipPeerScoreParamsBuilder; +import java.util.Map; +import java.util.stream.Collectors; +import kotlin.jvm.functions.Function1; +import org.minima.system.network.base.gossip.config.GossipConfig; +import org.minima.system.network.base.gossip.config.GossipPeerScoringConfig; +import org.minima.system.network.base.gossip.config.GossipScoringConfig; +import org.minima.system.network.base.gossip.config.GossipTopicScoringConfig; +import org.minima.system.network.base.peer.LibP2PNodeId; +import org.minima.system.network.base.gossip.config.GossipTopicScoringConfig; + +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipPeerScoringConfig; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipScoringConfig; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipTopicScoringConfig; +//import tech.pegasys.teku.networking.p2p.libp2p.LibP2PNodeId; + +public class LibP2PParamsFactory { + public static GossipParams createGossipParams(final GossipConfig gossipConfig) { + return GossipParams.builder() + .D(gossipConfig.getD()) + .DLow(gossipConfig.getDLow()) + .DHigh(gossipConfig.getDHigh()) + .DLazy(gossipConfig.getDLazy()) + // Calculate dScore and dOut based on other params + .DScore(gossipConfig.getD() * 2 / 3) + .DOut(Math.min(gossipConfig.getD() / 2, Math.max(0, gossipConfig.getDLow()) - 1)) + .fanoutTTL(gossipConfig.getFanoutTTL()) + .gossipSize(gossipConfig.getAdvertise()) + .gossipHistoryLength(gossipConfig.getHistory()) + .heartbeatInterval(gossipConfig.getHeartbeatInterval()) + .floodPublish(true) + .seenTTL(gossipConfig.getSeenTTL()) + .maxPublishedMessages(1000) + .maxTopicsPerPublishedMessage(1) + .maxSubscriptions(200) + .maxGraftMessages(200) + .maxPruneMessages(200) + .maxPeersPerPruneMessage(1000) + .maxIHaveLength(5000) + .maxIWantMessageIds(5000) + .build(); + } + + public static GossipScoreParams createGossipScoreParams(final GossipScoringConfig config) { + return GossipScoreParams.builder() + .peerScoreParams(createPeerScoreParams(config.getPeerScoringConfig())) + .topicsScoreParams(createTopicsScoreParams(config)) + .gossipThreshold(config.getGossipThreshold()) + .publishThreshold(config.getPublishThreshold()) + .graylistThreshold(config.getGraylistThreshold()) + .acceptPXThreshold(config.getAcceptPXThreshold()) + .opportunisticGraftThreshold(config.getOpportunisticGraftThreshold()) + .build(); + } + + public static GossipPeerScoreParams createPeerScoreParams(final GossipPeerScoringConfig config) { + final GossipPeerScoreParamsBuilder builder = + GossipPeerScoreParams.builder() + .topicScoreCap(config.getTopicScoreCap()) + .appSpecificWeight(config.getAppSpecificWeight()) + .ipColocationFactorWeight(config.getIpColocationFactorWeight()) + .ipColocationFactorThreshold(config.getIpColocationFactorThreshold()) + .behaviourPenaltyWeight(config.getBehaviourPenaltyWeight()) + .behaviourPenaltyDecay(config.getBehaviourPenaltyDecay()) + .behaviourPenaltyThreshold(config.getBehaviourPenaltyThreshold()) + .decayInterval(config.getDecayInterval()) + .decayToZero(config.getDecayToZero()) + .retainScore(config.getRetainScore()); + + // Configure optional params + config + .getAppSpecificScorer() + .ifPresent( + scorer -> { + final Function1 appSpecificScore = + peerId -> scorer.scorePeer(new LibP2PNodeId(peerId)); + builder.appSpecificScore(appSpecificScore); + }); + + config + .getDirectPeerManager() + .ifPresent( + mgr -> { + final Function1 isDirectPeer = + peerId -> mgr.isDirectPeer(new LibP2PNodeId(peerId)); + builder.isDirect(isDirectPeer); + }); + + config + .getWhitelistManager() + .ifPresent( + mgr -> { + // Ip whitelisting + final Function1 isIpWhitelisted = mgr::isWhitelisted; + builder.ipWhitelisted(isIpWhitelisted); + }); + + return builder.build(); + } + + public static GossipTopicsScoreParams createTopicsScoreParams(final GossipScoringConfig config) { + final GossipTopicScoreParams defaultTopicParams = + createTopicScoreParams(config.getDefaultTopicScoringConfig()); + final Map topicParams = + config.getTopicScoringConfig().entrySet().stream() + .collect( + Collectors.toMap(Map.Entry::getKey, e -> createTopicScoreParams(e.getValue()))); + return new GossipTopicsScoreParams(defaultTopicParams, topicParams); + } + + public static GossipTopicScoreParams createTopicScoreParams( + final GossipTopicScoringConfig config) { + return GossipTopicScoreParams.builder() + .topicWeight(config.getTopicWeight()) + .timeInMeshWeight(config.getTimeInMeshWeight()) + .timeInMeshQuantum(config.getTimeInMeshQuantum()) + .timeInMeshCap(config.getTimeInMeshCap()) + .firstMessageDeliveriesWeight(config.getFirstMessageDeliveriesWeight()) + .firstMessageDeliveriesDecay(config.getFirstMessageDeliveriesDecay()) + .firstMessageDeliveriesCap(config.getFirstMessageDeliveriesCap()) + .meshMessageDeliveriesWeight(config.getMeshMessageDeliveriesWeight()) + .meshMessageDeliveriesDecay(config.getMeshMessageDeliveriesDecay()) + .meshMessageDeliveriesThreshold(config.getMeshMessageDeliveriesThreshold()) + .meshMessageDeliveriesCap(config.getMeshMessageDeliveriesCap()) + .meshMessageDeliveriesActivation(config.getMeshMessageDeliveriesActivation()) + .meshMessageDeliveryWindow(config.getMeshMessageDeliveryWindow()) + .meshFailurePenaltyWeight(config.getMeshFailurePenaltyWeight()) + .meshFailurePenaltyDecay(config.getMeshFailurePenaltyDecay()) + .invalidMessageDeliveriesWeight(config.getInvalidMessageDeliveriesWeight()) + .invalidMessageDeliveriesDecay(config.getInvalidMessageDeliveriesDecay()) + .build(); + } +} diff --git a/src/org/minima/system/network/base/LimitedMap.java b/src/org/minima/system/network/base/LimitedMap.java new file mode 100644 index 000000000..d009a3b2e --- /dev/null +++ b/src/org/minima/system/network/base/LimitedMap.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import com.google.common.cache.CacheBuilder; +import java.util.Map; + + +/** Helper that creates a map with a maximum capacity. */ +public final class LimitedMap { + private LimitedMap() {} + + /** + * Creates a limited map. The returned map is safe for concurrent access and evicts the least + * recently used items. + * + * @param maxSize The maximum number of elements to keep in the map. + * @param The key type of the map. + * @param The value type of the map. + * @return A map that will evict elements when the max size is exceeded. + */ + public static Map create(final int maxSize) { + return defaultBuilder(maxSize).build().asMap(); + } + + /** + * Creates a limited map using soft references for values. The returned map is safe for concurrent + * access and evicts the least recently used items. + * + *

Items may be evicted before maxSize is reached if the garbage collector needs to free up + * memory. + * + * @param maxSize The maximum number of elements to keep in the map. + * @param The key type of the map. + * @param The value type of the map. + * @return A map that will evict elements when the max size is exceeded or when the GC evicts + * them. + */ + public static Map createSoft(final int maxSize) { + return defaultBuilder(maxSize).softValues().build().asMap(); + } + + private static CacheBuilder defaultBuilder(final int maxSize) { + return CacheBuilder.newBuilder().maximumSize(maxSize); + } +} + + diff --git a/src/org/minima/system/network/base/LimitedSet.java b/src/org/minima/system/network/base/LimitedSet.java new file mode 100644 index 000000000..fba78c09d --- /dev/null +++ b/src/org/minima/system/network/base/LimitedSet.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Collections; +import java.util.Set; + +/** Helper that creates a set with a maximum capacity. */ +public final class LimitedSet { + + private LimitedSet() {} + + /** + * Creates a limited set. The returned set is safe for concurrent access and evicts the least + * recently used items. + * + * @param maxSize The maximum number of elements to keep in the set. + * @param The type of object held in the set. + * @return A set that will evict elements when the max size is exceeded. + */ + public static Set create(final int maxSize) { + return Collections.newSetFromMap(LimitedMap.create(maxSize)); + } +} + diff --git a/src/org/minima/system/network/base/MemKeyValueStore.java b/src/org/minima/system/network/base/MemKeyValueStore.java new file mode 100644 index 000000000..8eb21af27 --- /dev/null +++ b/src/org/minima/system/network/base/MemKeyValueStore.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; + +/** Simple {@link java.util.HashMap} backed {@link KeyValueStore} implementation */ +public class MemKeyValueStore implements KeyValueStore { + + private final Map store = new ConcurrentHashMap<>(); + + @Override + public void put(@NotNull K key, @NotNull V value) { + store.put(key, value); + } + + @Override + public void remove(@NotNull K key) { + store.remove(key); + } + + @Override + public Optional get(@NotNull K key) { + return Optional.ofNullable(store.get(key)); + } +} diff --git a/src/org/minima/system/network/base/MinimaClient.java b/src/org/minima/system/network/base/MinimaClient.java index 4862e5194..cb8a61f46 100644 --- a/src/org/minima/system/network/base/MinimaClient.java +++ b/src/org/minima/system/network/base/MinimaClient.java @@ -71,9 +71,14 @@ public class MinimaClient extends MessageProcessor { //The UID String mUID; + // NodeID + private String nodeID; // this should never change + private String nodeRecord; // this is updated when node IP changes + //The Host and Port String mHost; int mPort; + int mLocalPort; //Ping each other to know you are still up and running.. every 10 mins.. public static final int PING_INTERVAL = 1000 * 60 * 10; @@ -84,7 +89,26 @@ public class MinimaClient extends MessageProcessor { */ boolean mReconnect = false; int mReconnectAttempts = 0; + + /** + * Incoming or Outgoing + */ + boolean mIncoming; + /** + * Constructor + * + * @param zSock + * @param zNetwork + * @throws IOException + * @throws UnknownHostException + */ + public MinimaClient(String zHost, int zPort, NetworkHandler zNetwork, String nodeID, String nodeRecord) { + this(zHost, zPort, zNetwork); + this.nodeID = nodeID; + this.nodeRecord = nodeRecord; + } + /** * Constructor * @@ -99,6 +123,7 @@ public MinimaClient(String zHost, int zPort, NetworkHandler zNetwork) { //Store mHost = zHost; mPort = zPort; + mLocalPort = zPort; //We will attempt to reconnect if this connection breaks.. mReconnect = true; @@ -109,6 +134,9 @@ public MinimaClient(String zHost, int zPort, NetworkHandler zNetwork) { //Create a UID mUID = ""+Math.abs(new Random().nextInt()); + //Outgoing connection + mIncoming = false; + //Start the connection PostMessage(NETCLIENT_INITCONNECT); } @@ -118,13 +146,14 @@ public MinimaClient(Socket zSock, NetworkHandler zNetwork) { //This is an incoming connection.. no reconnect attempt mReconnect = false; - + //Store mSocket = zSock; //Store mHost = mSocket.getInetAddress().getHostAddress(); mPort = mSocket.getPort(); + mLocalPort = mSocket.getLocalPort(); //Main network Handler mNetworkMain = zNetwork; @@ -132,6 +161,9 @@ public MinimaClient(Socket zSock, NetworkHandler zNetwork) { //Create a UID mUID = ""+Math.abs(new Random().nextInt()); + //Incoming connection + mIncoming = true; + //Start the system.. PostMessage(NETCLIENT_STARTUP); } @@ -160,6 +192,22 @@ public String getUID() { return mUID; } + public boolean isIncoming() { + return mIncoming; + } + + public int getLocalPort() { + return mLocalPort; + } + + public String getNodeID() { + return nodeID; + } + + public String getNodeRecord() { + return nodeRecord; + } + public NetworkHandler getNetworkHandler() { return mNetworkMain; } @@ -170,6 +218,8 @@ public JSONObject toJSON() { ret.put("uid", mUID); ret.put("host", getHost()); ret.put("port", getPort()); + ret.put("localport", getLocalPort()); + ret.put("incoming", mIncoming); return ret; } diff --git a/src/org/minima/system/network/base/MplexFirewall.java b/src/org/minima/system/network/base/MplexFirewall.java new file mode 100644 index 000000000..117fc8f12 --- /dev/null +++ b/src/org/minima/system/network/base/MplexFirewall.java @@ -0,0 +1,128 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import com.google.common.annotations.VisibleForTesting; +import io.libp2p.core.ChannelVisitor; +import io.libp2p.core.Connection; +import io.libp2p.etc.util.netty.mux.MuxId; +import io.libp2p.mux.MuxFrame; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +//import tech.pegasys.teku.infrastructure.async.FutureUtil; + +public class MplexFirewall implements ChannelVisitor { + + private static final Logger LOG = LogManager.getLogger(); + private static final long ONE_SECOND = 1000; + + private final int remoteOpenStreamsRateLimit; + private final int remoteParallelOpenStreamsLimit; + private final Supplier currentTimeSupplier; + + public MplexFirewall(int remoteOpenStreamsRateLimit, int remoteParallelOpenStreamsLimit) { + this(remoteOpenStreamsRateLimit, remoteParallelOpenStreamsLimit, System::currentTimeMillis); + } + + @VisibleForTesting + MplexFirewall( + int remoteOpenStreamsRateLimit, + int remoteParallelOpenStreamsLimit, + Supplier currentTimeSupplier) { + this.remoteOpenStreamsRateLimit = remoteOpenStreamsRateLimit; + this.remoteParallelOpenStreamsLimit = remoteParallelOpenStreamsLimit; + this.currentTimeSupplier = currentTimeSupplier; + } + + protected void remoteParallelOpenStreamLimitExceeded(MplexFirewallHandler peerMplexHandler) { + LOG.debug("Abruptly closing peer connection due to exceeding parallel open streams limit"); + FutureUtil.ignoreFuture(peerMplexHandler.getConnection().close()); + } + + protected void remoteOpenFrameRateLimitExceeded(MplexFirewallHandler peerMplexHandler) { + LOG.debug("Abruptly closing peer connection due to exceeding open mplex frame rate limit"); + FutureUtil.ignoreFuture(peerMplexHandler.getConnection().close()); + } + + @Override + public void visit(@NotNull Connection connection) { + MplexFirewallHandler firewallHandler = new MplexFirewallHandler(connection); + connection.pushHandler(firewallHandler); + } + + private class MplexFirewallHandler extends ChannelDuplexHandler { + + private final Connection connection; + private int openFrameCounter = 0; + private long startCounterTime = 0; + private final Set remoteOpenedStreamIds = new HashSet<>(); + + public MplexFirewallHandler(Connection connection) { + this.connection = connection; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + MuxFrame muxFrame = (MuxFrame) msg; + boolean blockFrame = false; + if (muxFrame.getFlag() == MuxFrame.Flag.OPEN) { + remoteOpenedStreamIds.add(muxFrame.getId()); + if (remoteOpenedStreamIds.size() > remoteParallelOpenStreamsLimit) { + remoteParallelOpenStreamLimitExceeded(this); + blockFrame = true; + } + + long curTime = currentTimeSupplier.get(); + if (curTime - startCounterTime > ONE_SECOND) { + startCounterTime = curTime; + openFrameCounter = 0; + } else { + openFrameCounter++; + if (openFrameCounter > remoteOpenStreamsRateLimit) { + remoteOpenFrameRateLimitExceeded(this); + blockFrame = true; + } + } + } else if (muxFrame.getFlag() == MuxFrame.Flag.CLOSE + || muxFrame.getFlag() == MuxFrame.Flag.RESET) { + remoteOpenedStreamIds.remove(muxFrame.getId()); + } + if (!blockFrame) { + ctx.fireChannelRead(msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + MuxFrame muxFrame = (MuxFrame) msg; + if (muxFrame.getFlag() == MuxFrame.Flag.RESET) { + // Track only RESET since CLOSE from local doesn't close the stream for writing from remote + remoteOpenedStreamIds.remove(muxFrame.getId()); + } + // ignoring since the write() just returns `promise` instance + FutureUtil.ignoreFuture(ctx.write(msg, promise)); + } + + public Connection getConnection() { + return connection; + } + } +} diff --git a/src/org/minima/system/network/base/NetworkConfig.java b/src/org/minima/system/network/base/NetworkConfig.java new file mode 100644 index 000000000..d580bfab5 --- /dev/null +++ b/src/org/minima/system/network/base/NetworkConfig.java @@ -0,0 +1,209 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.InetAddresses.isInetAddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +// import tech.pegasys.teku.infrastructure.io.PortAvailability; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig; +// import tech.pegasys.teku.util.config.InvalidConfigurationException; +import org.minima.system.network.base.gossip.config.GossipConfig; + +public class NetworkConfig { + private static final Logger LOG = LogManager.getLogger(NetworkConfig.class); + + private final GossipConfig gossipConfig; + private final WireLogsConfig wireLogsConfig; + + private final boolean isEnabled; + private final Optional privateKeyFile; + private final String networkInterface; + private final Optional advertisedIp; + private final int listenPort; + private final OptionalInt advertisedPort; + + private NetworkConfig( + final boolean isEnabled, + final GossipConfig gossipConfig, + final WireLogsConfig wireLogsConfig, + final Optional privateKeyFile, + final String networkInterface, + final Optional advertisedIp, + final int listenPort, + final OptionalInt advertisedPort) { + + this.privateKeyFile = privateKeyFile; + this.networkInterface = networkInterface; + + this.advertisedIp = advertisedIp.filter(ip -> !ip.isBlank()); + this.isEnabled = isEnabled; + if (this.advertisedIp.map(ip -> !isInetAddress(ip)).orElse(false)) { + throw new InvalidConfigurationException( + String.format( + "Advertised ip (%s) is set incorrectly.", this.advertisedIp.orElse("EMPTY"))); + } + + this.listenPort = listenPort; + this.advertisedPort = advertisedPort; + this.gossipConfig = gossipConfig; + this.wireLogsConfig = wireLogsConfig; + } + + public static Builder builder() { + return new Builder(); + } + + public void validateListenPortAvailable() { + if (listenPort != 0 && !PortAvailability.isPortAvailable(listenPort)) { + throw new InvalidConfigurationException( + String.format( + "P2P Port %d (TCP/UDP) is already in use. " + + "Check for other processes using this port.", + listenPort)); + } + } + + public boolean isEnabled() { + return isEnabled; + } + + public Optional getPrivateKeyFile() { + return privateKeyFile; + } + + public String getNetworkInterface() { + return networkInterface; + } + + public String getAdvertisedIp() { + return resolveAnyLocalAddress(advertisedIp.orElse(networkInterface)); + } + + public boolean hasUserExplicitlySetAdvertisedIp() { + return advertisedIp.isPresent(); + } + + public int getListenPort() { + return listenPort; + } + + public int getAdvertisedPort() { + return advertisedPort.orElse(listenPort); + } + + public GossipConfig getGossipConfig() { + return gossipConfig; + } + + public WireLogsConfig getWireLogsConfig() { + return wireLogsConfig; + } + + private String resolveAnyLocalAddress(final String ipAddress) { + try { + final InetAddress advertisedAddress = InetAddress.getByName(ipAddress); + if (advertisedAddress.isAnyLocalAddress()) { + return InetAddress.getLocalHost().getHostAddress(); + } else { + return ipAddress; + } + } catch (UnknownHostException err) { + LOG.error( + "Unable to start LibP2PNetwork due to failed attempt at obtaining host address", err); + return ipAddress; + } + } + + public static class Builder { + public static final int DEFAULT_P2P_PORT = 9000; + + private final GossipConfig.Builder gossipConfigBuilder = GossipConfig.builder(); + private final WireLogsConfig.Builder wireLogsConfig = WireLogsConfig.builder(); + + private Boolean isEnabled = true; + private Optional privateKeyFile = Optional.empty(); + private String networkInterface = "0.0.0.0"; + private Optional advertisedIp = Optional.empty(); + private Integer listenPort = DEFAULT_P2P_PORT; + private OptionalInt advertisedPort = OptionalInt.empty(); + + private Builder() {} + + public NetworkConfig build() { + return new NetworkConfig( + isEnabled, + gossipConfigBuilder.build(), + wireLogsConfig.build(), + privateKeyFile, + networkInterface, + advertisedIp, + listenPort, + advertisedPort); + } + + public Builder isEnabled(final Boolean enabled) { + checkNotNull(enabled); + isEnabled = enabled; + return this; + } + + public Builder gossipConfig(final Consumer consumer) { + consumer.accept(gossipConfigBuilder); + return this; + } + + public Builder wireLogs(final Consumer consumer) { + consumer.accept(wireLogsConfig); + return this; + } + + public Builder privateKeyFile(final String privateKeyFile) { + checkNotNull(privateKeyFile); + this.privateKeyFile = Optional.of(privateKeyFile).filter(f -> !f.isBlank()); + return this; + } + + public Builder networkInterface(final String networkInterface) { + checkNotNull(networkInterface); + this.networkInterface = networkInterface; + return this; + } + + public Builder advertisedIp(final Optional advertisedIp) { + checkNotNull(advertisedIp); + this.advertisedIp = advertisedIp; + return this; + } + + public Builder listenPort(final Integer listenPort) { + checkNotNull(listenPort); + this.listenPort = listenPort; + return this; + } + + public Builder advertisedPort(final OptionalInt advertisedPort) { + checkNotNull(advertisedPort); + this.advertisedPort = advertisedPort; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/NoOpDiscoveryService.java b/src/org/minima/system/network/base/NoOpDiscoveryService.java new file mode 100644 index 000000000..e53779867 --- /dev/null +++ b/src/org/minima/system/network/base/NoOpDiscoveryService.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Optional; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryService; +import org.minima.system.network.base.peer.DiscoveryPeer; + +public class NoOpDiscoveryService implements DiscoveryService { + + @Override + public SafeFuture start() { + return SafeFuture.COMPLETE; + } + + @Override + public SafeFuture stop() { + return SafeFuture.COMPLETE; + } + + @Override + public Stream streamKnownPeers() { + return Stream.empty(); + } + + @Override + public SafeFuture searchForPeers() { + return SafeFuture.COMPLETE; + } + + @Override + public Optional getEnr() { + return Optional.empty(); + } + + @Override + public Optional getDiscoveryAddress() { + return Optional.empty(); + } + + @Override + public void updateCustomENRField(String fieldName, Bytes value) {} +} diff --git a/src/org/minima/system/network/base/NodeRecordConverter.java b/src/org/minima/system/network/base/NodeRecordConverter.java new file mode 100644 index 000000000..2a55f619e --- /dev/null +++ b/src/org/minima/system/network/base/NodeRecordConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static org.minima.system.network.base.DiscoveryNetwork.ATTESTATION_SUBNET_ENR_FIELD; +import static org.minima.system.network.base.DiscoveryNetwork.ETH2_ENR_FIELD; + +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.ethereum.beacon.discovery.schema.EnrField; +import org.ethereum.beacon.discovery.schema.NodeRecord; +import org.minima.system.network.base.peer.DiscoveryPeer; +//import org.minima.system.network.base.EnrForkId; +import org.minima.system.network.base.ssz.SszBitvector; + +public class NodeRecordConverter { + private static final Logger LOG = LogManager.getLogger(NodeRecordConverter.class); + + static Optional convertToDiscoveryPeer(final NodeRecord nodeRecord) { + return nodeRecord + .getTcpAddress() + .map(address -> socketAddressToDiscoveryPeer(nodeRecord, address)); + } + + private static DiscoveryPeer socketAddressToDiscoveryPeer( + final NodeRecord nodeRecord, final InetSocketAddress address) { + + final Optional enrForkId = + parseField(nodeRecord, ETH2_ENR_FIELD, EnrForkId.SSZ_SCHEMA::sszDeserialize); + + final SszBitvector persistentSubnets = + parseField( + nodeRecord, + ATTESTATION_SUBNET_ENR_FIELD, + DiscV5Service.SUBNET_SUBSCRIPTIONS_SCHEMA::fromBytes) + .orElse(DiscV5Service.SUBNET_SUBSCRIPTIONS_SCHEMA.getDefault()); + + + Bytes nodeId = nodeRecord.getNodeId(); + String enr = nodeRecord.asEnr(); + return new DiscoveryPeer( + ((Bytes) nodeRecord.get(EnrField.PKEY_SECP256K1)), address, enrForkId, persistentSubnets, nodeId, enr); + } + + private static Optional parseField( + final NodeRecord nodeRecord, final String fieldName, final Function parse) { + try { + return Optional.ofNullable((Bytes) nodeRecord.get(fieldName)).map(parse); + } catch (final Exception e) { + LOG.debug("Failed to parse ENR field {}", fieldName, e); + return Optional.empty(); + } + } +} diff --git a/src/org/minima/system/network/base/P2PMinimaDiscovery.java b/src/org/minima/system/network/base/P2PMinimaDiscovery.java new file mode 100644 index 000000000..76bdb9d26 --- /dev/null +++ b/src/org/minima/system/network/base/P2PMinimaDiscovery.java @@ -0,0 +1,7 @@ +package org.minima.system.network.base; + +public class P2PMinimaDiscovery extends P2PMinimaDiscoveryBinding { + public P2PMinimaDiscovery() { + super(new P2PMinimaDiscoveryProtocol()); + } +} diff --git a/src/org/minima/system/network/base/P2PMinimaDiscoveryBinding.java b/src/org/minima/system/network/base/P2PMinimaDiscoveryBinding.java new file mode 100644 index 000000000..7b64f1883 --- /dev/null +++ b/src/org/minima/system/network/base/P2PMinimaDiscoveryBinding.java @@ -0,0 +1,9 @@ +package org.minima.system.network.base; + +import io.libp2p.core.multistream.StrictProtocolBinding; + +public class P2PMinimaDiscoveryBinding extends StrictProtocolBinding { + public P2PMinimaDiscoveryBinding(P2PMinimaDiscoveryProtocol mdp) { + super("/p2p/minima/1.0.0", mdp); + } +} \ No newline at end of file diff --git a/src/org/minima/system/network/base/P2PMinimaDiscoveryProtocol.java b/src/org/minima/system/network/base/P2PMinimaDiscoveryProtocol.java new file mode 100644 index 000000000..7ad3da60e --- /dev/null +++ b/src/org/minima/system/network/base/P2PMinimaDiscoveryProtocol.java @@ -0,0 +1,118 @@ +package org.minima.system.network.base; + + +import io.libp2p.core.PeerId; +import io.libp2p.core.Stream; +//import io.libp2p.etc.types.toByteBuf; +import io.libp2p.protocol.ProtocolHandler; +import io.libp2p.protocol.ProtocolMessageHandler; +import io.netty.buffer.ByteBuf; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.CompletableFuture; + +interface P2PMinimaDiscoveryProtocolController { + //fun send(message: String) + public void send(String message); +} + +// class Ping : PingBinding(PingProtocol()) + +// open class PingBinding(ping: PingProtocol) : +// StrictProtocolBinding("/ipfs/ping/1.0.0", ping) + +public class P2PMinimaDiscoveryProtocol extends ProtocolHandler { + + public P2PMinimaDiscoveryProtocol() { + super(Long.MAX_VALUE, Long.MAX_VALUE); + //TODO Auto-generated constructor stub + } + + private CompletableFuture onStart(Stream stream) { + // TODO: add a handler and do something + CompletableFuture ready = new CompletableFuture(); + // val handler = MDPHandler(chatCallback, ready) + //stream.pushHandler(handler) + //return ready.thenApply { handler } + return ready; + } + + protected CompletableFuture onStartInitiator(Stream stream) { + return onStart(stream); + } + + protected CompletableFuture onStartResponder(Stream stream) { + return onStart(stream); + } + + class MDPHandler implements ProtocolMessageHandler, P2PMinimaDiscoveryProtocolController { + + private Stream stream; + // private mdpCallback; + + public MDPHandler() { + //todo: add OnChatMessage and ready equivalents in constructor and save as private fields + } + + @Override + public void onActivated(Stream stream) { + this.stream = stream; + //ready.complete(null); + } + + @Override + public void send(String message) { + byte[] data = message.getBytes(Charset.defaultCharset()); + stream.writeAndFlush(data); // does this need to be in a ByteBuffer + } + + @Override + public void fireMessage(Stream arg0, Object arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void onClosed(Stream arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onException(Throwable arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onMessage(Stream stream, ByteBuf msg) { + String msgStr = msg.toString(Charset.defaultCharset()); + System.out.println("Received message: " + msgStr); + //mdpCallback(stream.remotePeerId(), msgStr); + } + } + +// open inner class Chatter( +// private val chatCallback: OnChatMessage, +// val ready: CompletableFuture +// ) : ProtocolMessageHandler, ChatController { +// lateinit var stream: Stream + +// override fun onActivated(stream: Stream) { +// this.stream = stream +// ready.complete(null) +// } + +// override fun onMessage(stream: Stream, msg: ByteBuf) { +// val msgStr = msg.toString(Charset.defaultCharset()) +// chatCallback(stream.remotePeerId(), msgStr) +// } + +// override fun send(message: String) { +// val data = message.toByteArray(Charset.defaultCharset()) +// stream.writeAndFlush(data.toByteBuf()) +// } +// } + +} diff --git a/src/org/minima/system/network/base/P2PNetwork.java b/src/org/minima/system/network/base/P2PNetwork.java new file mode 100644 index 000000000..7c70519ba --- /dev/null +++ b/src/org/minima/system/network/base/P2PNetwork.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.Optional; +import java.util.stream.Stream; +// import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.gossip.GossipNetwork; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; + +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.gossip.GossipNetwork; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.NodeId; +import org.minima.system.network.base.peer.Peer; +import org.minima.system.network.base.peer.PeerAddress; +import org.minima.system.network.base.peer.PeerConnectedSubscriber; + +public interface P2PNetwork extends GossipNetwork { + + enum State { + IDLE, + RUNNING, + STOPPED + } + + /** + * Connects to a Peer using a user supplied address. The address format is specific to the network + * implementation. If a connection already exists for this peer, the future completes with the + * existing peer. + * + *

The {@link PeerAddress} must have been created using the {@link #createPeerAddress(String)} + * method of this same implementation. + * + * @param peer Peer to connect to. + * @return A future which completes when the connection is established, containing the newly + * connected peer. + */ + SafeFuture connect(PeerAddress peer); + + /** + * Parses a peer address in any of this network's supported formats. + * + * @param peerAddress the address to parse + * @return a {@link PeerAddress} which is supported by {@link #connect(PeerAddress)} for + * initiating connections + */ + PeerAddress createPeerAddress(String peerAddress); + + /** + * Converts a {@link DiscoveryPeer} to a {@link PeerAddress} which can be used with this network's + * {@link #connect(PeerAddress)} method. + * + * @param discoveryPeer the discovery peer to convert + * @return a {@link PeerAddress} which is supported by {@link #connect(PeerAddress)} for + * initiating connections + */ + PeerAddress createPeerAddress(DiscoveryPeer discoveryPeer); + + long subscribeConnect(PeerConnectedSubscriber subscriber); + + void unsubscribeConnect(long subscriptionId); + + boolean isConnected(PeerAddress peerAddress); + + Bytes getPrivateKey(); + + Optional getPeer(NodeId id); + + Stream streamPeers(); + + NodeId parseNodeId(final String nodeId); + + int getPeerCount(); + + String getNodeAddress(); + + NodeId getNodeId(); + + int getListenPort(); + + /** + * Get the Ethereum Node Record (ENR) for the local node, if one exists. + * + * @return the local ENR. + */ + Optional getEnr(); + + Optional getDiscoveryAddress(); + + /** + * Starts the P2P network layer. + * + * @return + */ + SafeFuture start(); + + /** Stops the P2P network layer. */ + SafeFuture stop(); +} diff --git a/src/org/minima/system/network/base/P2PStart.java b/src/org/minima/system/network/base/P2PStart.java new file mode 100644 index 000000000..0f8e2737e --- /dev/null +++ b/src/org/minima/system/network/base/P2PStart.java @@ -0,0 +1,546 @@ +package org.minima.system.network.base; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.netty.buffer.Unpooled.*; + +// Import log4j classes. +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.minima.system.network.NetworkHandler; +import org.minima.system.network.base.DiscoveryNetworkFactory.DiscoveryNetworkBuilder; +import org.minima.system.network.base.LibP2PNetwork.PrivateKeyProvider; +import org.minima.system.network.base.libp2p.PrivateKeyGenerator; +//import org.minima.system.network.base.NodeRecordConverter; +import org.minima.system.network.base.peer.DiscoveryPeer; +import org.minima.system.network.base.peer.LibP2PNodeId; +import org.minima.system.network.base.peer.NodeId; +import org.minima.system.network.base.peer.Peer; +import org.minima.utils.messages.Message; +import org.minima.utils.messages.MessageProcessor; +import org.minima.utils.messages.TimerMessage; + +//import org.ethereum.beacon.discovery.schema.NodeRecord; +import org.ethereum.beacon.discovery.schema.NodeRecord; +import org.ethereum.beacon.discovery.schema.NodeRecordFactory; + +import io.libp2p.core.PeerId; +import io.libp2p.core.crypto.PrivKey; +import io.libp2p.core.crypto.PubKey; +import io.libp2p.core.multiformats.Multihash; +import io.libp2p.crypto.keys.Secp256k1Kt; +import io.libp2p.etc.encode.Base58; +import io.netty.buffer.ByteBuf; +import io.libp2p.core.crypto.KEY_TYPE; +import io.libp2p.core.crypto.KeyKt; +//import io.libp2p.core.crypto.marshalPublicKey; + +public class P2PStart extends MessageProcessor { + + private static final Logger logger = LogManager.getLogger(P2PStart.class); + public static final String P2P_THREAD = "P2P"; + // these messages only control LIBP2P+DISCV5 <> Minima comms at the moment + public static final String P2P_START_SCAN = "P2P_START_SCAN"; + public static final String P2P_STOP_SCAN = "P2P_STOP_SCAN"; + public static final String P2P_SAVE_NEIGHBOURS = "P2P_SAVE_NEIGHBOURS"; + public static final int P2P_SCAN_INTERVAL = 10*1000; // sync neighbours table from p2p layer every N ms. + public static final int SAVE_NEIGHBOURS_DELAY = 10*1000; // save list every 10 seconds + public static final String COMMA_DELIMITER = ","; + private String mConfFolder; + private NetworkHandler mNetwork; + private DiscoveryNetwork network; + Set activeKnownNodes; + Hashtable allDiscoveredNodes2 = new Hashtable<>(); + private NodeId nodeId; + private PubKey pubKey; + private File mP2PBootnodesFile; + private File mRoot; + + class MinimaNodeInfo { + final public InetSocketAddress socket; + final public String nodeRecord; + final public String nodeID; + final public String discoMultiAddrTCP; + public MinimaNodeInfo(String nodeID, String nodeRecord, InetSocketAddress socket, String discoMultiAddrTCP) { + this.nodeID = nodeID; + this.nodeRecord = nodeRecord; + this.socket = socket; + this.discoMultiAddrTCP = discoMultiAddrTCP; + } + } + + // staticPeers = list of static peers in multiaddr format: /ip4/127.0.0.1/tcp/10219/p2p/16Uiu2HAmCnuHVjxoQtZzqenqjRr6hAja1XWCuC1SiqcWcWcp4iSt + // bootnodes = list of ENR: enr:-Iu4QGvbP4hn3cxao3aFyZfeGBG0Ygp-KPJsK9h7pM_0FfCGauk0P2haW7AEiLaMLEDxRngy4SjCx6GGfwlsRBf0BBwBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMCButDl63KBqEEyxV2R3nCvnHb7sEIgOACbb6yt6oxqYN0Y3CCJ-uDdWRwgifr + // ENR above in enr-viewer.com leads to: pubkey=0x030206eb4397adca06a104cb15764779c2be71dbeec10880e0026dbeb2b7aa31a9 + // ip=127.0.0.1, tcp=10219, udp=10219 + // if we protobuf the pubkey (crypto.proto) we get 08021221030206eb4397adca06a104cb15764779c2be71dbeec10880e0026dbeb2b7aa31a9 + // 08 means varint follows with value 02. which means SECP256K1 key + // 12 means String follows, starting with length. length is 0x21 or 33 dec + // now we multihash it. Because the key is small (<50) it is inlined: 002508021221030206eb4397adca06a104cb15764779c2be71dbeec10880e0026dbeb2b7aa31a9 + // 00 -> Identity, 0x25=length (protobuf pubkey SECP256K1, 0x21+0x04) + // and now we base58 encode it: 16Uiu2HAmCnuHVjxoQtZzqenqjRr6hAja1XWCuC1SiqcWcWcp4iSt + // So the hex prefix to the pubkey to build the peerid is always 002508021221 (in our use case). + public P2PStart(String zConfFolder, NetworkHandler minimaNet, String[] _staticPeers, String[] _bootnodes) { + super(P2P_THREAD); + this.mConfFolder = zConfFolder; + mNetwork = minimaNet; + Vector staticPeers = new Vector<>(); + Vector bootnodes = new Vector<>(); + if(_staticPeers == null || _staticPeers.length == 0) { + logger.info("P2P layer - no static peer provided by minima."); + } + if(_bootnodes == null || _bootnodes.length == 0) { + logger.info("P2P layer - no bootnode provided by minima."); + } + if(_staticPeers != null && _bootnodes != null && _staticPeers.length == _bootnodes.length) { + logger.info("P2P layer - received bootnode and staticpeer info from minima, adding to config"); + Collections.addAll(staticPeers, _staticPeers); + Collections.addAll(bootnodes, _bootnodes); + } + + // check config file for SECP256K1 private key + mRoot = ensureFolder(new File(mConfFolder)); + String mRootPath = mRoot.getAbsolutePath(); + + //Current used TxPOW + File mP2PDir = ensureFolder(new File(mRoot,"p2p")); + File mP2PNodePrivKeyFile = new File(mP2PDir, "NodePrivKey.pkey"); + mP2PBootnodesFile = new File(mP2PDir, "bootnodes.csv"); + + // two lines below just to get rid of temporary not initialized issue + PrivateKeyProvider provider = PrivateKeyGenerator::generate; + PrivKey privKey = provider.get(); + privKey = loadNodePrivateKey(mP2PNodePrivKeyFile); + if(privKey == null) { + privKey = generateNodePrivateKey(mP2PNodePrivKeyFile); + } + + if(privKey == null) { + System.out.println("P2P Error - priv key uninitialized!"); + logger.error("P2P Error - priv key uninitialized!"); + return; + } + + PeerId pid = PeerId.fromPubKey(privKey.publicKey()); + nodeId = new LibP2PNodeId(pid); + pubKey = privKey.publicKey(); + + //byte[] marshaledPubKey = KeyKt.marshalPublicKey(privKey.publicKey()); + // if pubkey is less than 42 bytes then peerid will be + // the multihash digest of identity (0) and the pubkey as a protobuf (keytype, data) + // logger.debug("P2P layer - peerid - raw hex: " + pid.toHex()); + // logger.debug("P2P layer - pubkey - raw hex: " + hex(privKey.publicKey().bytes())); + // logger.debug("P2P layer - protobuf pubkey: " + P2PStart.hex(marshaledPubKey)); + // logger.debug("P2P layer - protobuf pubkey length (dec): " + marshaledPubKey.length); + // logger.debug("P2P layer - protobuf pubkey length (hex)): " + Integer.toHexString(marshaledPubKey.length)); + //ByteBuf wrappedBuf = wrappedBuffer(marshaledPubKey); + // logger.debug("P2P layer - wrapped buff protobuf pubkey (hex)): " + hex(wrappedBuf.array())); // Integer.toHexString(marshaledPubKey.length)); + // logger.debug("P2P layer - wrapped buff protobuf pubkey capacity (dec): " + wrappedBuf.capacity()); + // The line below does not work. It should just add 0x0025 in front of the protobuf (identity+protobuf pubkey length). + //Multihash mhash = Multihash.digest(new Multihash.Descriptor(Multihash.Digest.Identity, null), wrappedBuf, (marshaledPubKey.length)*8); + //System.out.println("P2P layer - multihash Str value: " + mhash.toString()); + // last 8 bytes are zeros for some reason + // as we are fixed size we only copy the meaningful bytes before Base58 encoding. + //byte[] mhashbytes = mhash.getBytes().copy(0, 39).array(); + //logger.debug("P2P layer - multihash hex bytes value: " + hex(mhashbytes)); + //logger.debug("P2P layer - multihash base58 value: " + Base58.INSTANCE.encode(mhashbytes)); // same as nodeId.toBase58() + //logger.debug("P2P layer - peerid - base58 encoded: " + pid.toHex()); + //logger.debug("P2P layer - nodeid (peerid base58): " + nodeId.toString()); // same as .toBase58() + logger.debug("P2P layer - nodeid base58: " + nodeId.toBase58()); + + // bootnodes CSV + List> records = loadP2PNeighbours(mP2PBootnodesFile); + if (records != null) { + for (List record : records) { + staticPeers.add(record.get(1)); + bootnodes.add(record.get(2)); + } + } + + DiscoveryNetworkFactory factory = new DiscoveryNetworkFactory(); + try { + if(staticPeers != null && staticPeers.size() > 0 && bootnodes != null && bootnodes.size() > 0) { + System.out.println("Building p2p layer using provided params: staticpeer=" + staticPeers.get(0) + " and bootnode=" + bootnodes.get(0)); + DiscoveryNetworkBuilder builder = factory.builder(); + builder = builder.setPrivKey(privKey); + for(int i = 0; i < staticPeers.size(); i++) { + builder = builder.staticPeer(staticPeers.get(i)).bootnode(bootnodes.get(i)); + } + network = builder.buildAndStart(mNetwork.getBasePort() + 5); + } else if(staticPeers == null || staticPeers.size() == 0) { + logger.info("P2P: starting in standalone mode"); + System.out.println("P2P: starting in standalone mode"); + network = factory.builder().setPrivKey(privKey).buildAndStart(mNetwork.getBasePort() + 5); + } + } catch (Exception e) { + logger.error("P2P failed to start through DiscoveyrNetworkFactory."); + e.printStackTrace(); + } + + if(network != null) { + // P2P initialization complete + Optional discAddr = network.getDiscoveryAddress(); + logger.warn("LOGGER nodeid: " + network.getNodeId() + " , nodeAddress: " + network.getNodeAddress() + + " , discovery address: " + discAddr.get()); + System.out.println("P2P nodeid: " + network.getNodeId() + " , nodeAddress: " + network.getNodeAddress() + + " , discovery address: " + discAddr.get()); + System.out.println("Starting discovery loop info"); + activeKnownNodes = new HashSet<>(); + PostTimerMessage(new TimerMessage(P2P_SCAN_INTERVAL, P2P_START_SCAN)); // could also be a TimerMessage + // PostMessage(P2P_SAVE_NEIGHBOURS); // for now we save neighbours at the end of each scan, this timer is not used + // PostTimerMessage(new TimerMessage(SAVE_NEIGHBOURS_DELAY, P2P_SAVE_NEIGHBOURS)); + } else { + // initialization failed - what do we do? + logger.error("Failed to start P2P network."); + } + } + + public static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte aByte : bytes) { + result.append(String.format("%02x", aByte)); + // upper case + // result.append(String.format("%02X", aByte)); + } + return result.toString(); + } + + public String convertToCSV(String pubKey, MinimaNodeInfo i) { + StringBuilder builder = new StringBuilder(); + builder.append(pubKey); + builder.append(","); + builder.append(i.discoMultiAddrTCP); + builder.append(","); + builder.append(i.nodeRecord); + return builder.toString(); + } + + // from https://www.baeldung.com/java-csv + public String convertToCSV(String[] data) { + return Stream.of(data) + .map(this::escapeSpecialCharacters) + .collect(Collectors.joining(",")); + } + + public String escapeSpecialCharacters(String data) { + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } + + public NodeId getNodeId() { + return nodeId; + } + + public PubKey getPubKey() { + return pubKey; + } + + public String getENR() { + return network.getENR(); + } + + public Optional getDiscoveryAddress() { + return network.getDiscoveryAddress(); + } + + public int getP2PPeerCount() { + // return network.get + return network.getP2PPeerCount(); + } + + public Stream streamPeers() { + return network.streamPeers(); + } + + public Stream streamDiscoveryPeers() { + return network.streamKnownDiscoveryPeers(); + } + + + @Override + protected void processMessage(Message zMessage) throws Exception { + logger.warn("P2PStart received message: " + zMessage.toString()); + + if(zMessage.isMessageType(P2P_START_SCAN)) { + p2pAddNewNodes(); + // timer must be re-sent each time + PostTimerMessage(new TimerMessage(P2P_SCAN_INTERVAL, P2P_START_SCAN)); + } else if(zMessage.isMessageType(P2P_SAVE_NEIGHBOURS)) { + // saveP2PNeighbours(); + } else if(zMessage.isMessageType(P2P_STOP_SCAN)) { + // TODO: stop scanning + // also stop P2P layer? + } + } + + private PrivKey loadNodePrivateKey(File mP2PNodePrivKeyFile) { + if (!mP2PNodePrivKeyFile.exists()) { + return null; + } + PrivKey privKey = null; + // try loading file from private key + // if error, bailout? + FileInputStream inputStream; + try { + inputStream = new FileInputStream(mP2PNodePrivKeyFile); + byte[] keyBuffer; + keyBuffer = inputStream.readAllBytes(); + privKey = KeyKt.unmarshalPrivateKey(keyBuffer); + PubKey pubKey = privKey.publicKey(); + System.out.println("Loaded private key from local dir: " + hex(privKey.bytes())); + System.out.println("Computed public key: " + hex(pubKey.bytes())); + } catch (FileNotFoundException e) { + System.out.println("Failed to read private key from disk - " + e.getMessage()); + e.printStackTrace(); + } catch (IOException e) { + System.out.println("Failed to read private key from disk - " + e.getMessage()); + e.printStackTrace(); + } + return privKey; + } + + private PrivKey generateNodePrivateKey(File mP2PNodePrivKeyFile) { + // generate a SECP256K1 private key + // PrivateKeyProvider keyProvider = PrivateKeyGenerator::generate; + PrivKey privKey = KeyKt.generateKeyPair(KEY_TYPE.SECP256K1).component1(); + // privKey = keyProvider.get(); + // save priv key to file + try { + FileOutputStream outputStream = new FileOutputStream(mP2PNodePrivKeyFile); + outputStream.write(KeyKt.marshalPrivateKey(privKey)); + //System.out.println("Generated new node private key and saved to local dir: " + hex(privKey.bytes())); + System.out.println("Computed node public key: " + hex(pubKey.bytes())); + outputStream.close(); + } catch (Exception e) { + System.out.println("Failed to save node private key to disk - " + e.getMessage()); + logger.error("Failed to save node private key to disk - " + e.getMessage()); + } + return privKey; + } + + private List> loadP2PNeighbours(File mP2PBootnodesFile) { + if(!mP2PBootnodesFile.exists()) { + return null; + } + + // try loading bootnodes from CSV file + List> records = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(new FileReader(mP2PBootnodesFile))) { + String line; + logger.debug("starting to read p2p bootnodes CSV file: " + mP2PBootnodesFile.getAbsolutePath()); + while ((line = br.readLine()) != null) { + if (line.length() > 0) { + String[] values = line.split(P2PStart.COMMA_DELIMITER); + logger.debug("P2PStart:csv load: read and CSV split one line: " + line); + if (values.length == 2) { + // expect public key, p2p multiaddr and ENR + // TODO: add fields validation + records.add(Arrays.asList(values)); + //System.out.println("Loaded bootnode from CSV: " + records.get(records.size() - 1).toString()); + logger.info("Loaded bootnode from CSV: " + records.get(records.size() - 1).toString()); + } else { + logger.error("Failed to parse line from CSV: " + line); + System.out.println("P2P - error parsing line from CSV: " + line); + } + } else { + logger.debug("P2PStart: skipping empty line in CSV"); + } + } + } catch (FileNotFoundException e) { + + } catch (IOException e) { + + } + return records; + } + + private void saveP2PNeighbours() { + logger.debug("saveP2PNeighbours: trying to save neighbours list"); + if(allDiscoveredNodes2.size() == 0) { + logger.debug("savep2pNeighbours: empty nodes list, nothing to save, exiting."); + return; + } + try { + //File csvOutputFile = new File(CSV_FILE_NAME); + File csvOutputFile = mP2PBootnodesFile; + FileWriter csvWriter = new FileWriter(csvOutputFile); + try (PrintWriter pw = new PrintWriter(csvWriter, true)) { + allDiscoveredNodes2.forEach(new BiConsumer() { + @Override + public void accept(String pubKey, MinimaNodeInfo i) { + logger.debug("saving node i: pubkey=" + pubKey + " enr=" + i.nodeRecord + " line=" + convertToCSV(pubKey, i)); + pw.println(convertToCSV(pubKey, i)); + } + }); + } catch(Exception e) { + logger.warn("Could not write lines to neighbours list file! " + "msg=" + e.getMessage()); + e.printStackTrace(); + } + //csvWriter.flush(); + } catch(IOException e) { + logger.warn("Could not flush and close neighbours list file! " + "msg=" + e.getMessage()); + e.printStackTrace(); + } + logger.debug("saveP2PNeighbours: end"); + } + + private void p2pAddNewNodes() { + logger.debug("p2pAddNewNodes: start"); + if (network != null) { + Set activeKnownNodes = new HashSet<>(); + + // // we dont really care about this list... + // network.streamPeers().filter(peer -> peer.getId() != null).forEach(peer -> { + // logger.debug("peer: id=" + peer.getId()); // peer address == peer id and " isConnected=" true + // }); + + ArrayList mClients = mNetwork.getNetClients(); + + Set knownNodeIDs = new HashSet<>(); + + for (MinimaClient mClient : mClients) { + logger.debug(" mclient nodeid=" + mClient.getNodeID() + ", nodeRecord=" + mClient.getNodeRecord()); + knownNodeIDs.add(mClient.getNodeID()); + } + + Set newActiveNodes = new HashSet<>(); + Set unconnectedNewNodes = new HashSet<>(); + network.streamKnownDiscoveryPeers().forEach(discoPeer -> { // disc peer node address should be + // inetsocketaddr + PeerId peerid = new PeerId(discoPeer.getNodeID().toArray()); + // nodeAddress: enr_ip:enr_port + // pubkey:enr_secp256k1 + // nodeid: derived(enr_secp256k1) + logger.debug("discovery peer: " + discoPeer.getNodeAddress() + " pubkey=" + discoPeer.getPublicKey() + + " peerid: " + peerid + " nodeid:" + discoPeer.getNodeID().toHexString() + " enr: " + + discoPeer.getNodeRecord()); + // TODO: establish link between Bytes nodeID and libp2p nodeid / peerid + // TODO: verify values for nodeid and enr and filter existing nodes vs new based + // on nodeid + // Optional tmpdiscopeer = + // NodeRecordConverter.convertToDiscoveryPeer(discoPeer.getNodeRecord()); + + // nodeRecord.getNodeId() + newActiveNodes.add(discoPeer.getNodeAddress()); + + if (!knownNodeIDs.contains(discoPeer.getNodeID().toString())) { + logger.debug("FOUND NEW NODE: nodeid:" + discoPeer.getNodeID().toString() + " " + + discoPeer.getNodeRecord().toString()); + MinimaNodeInfo aNewNodeInfo = new MinimaNodeInfo(discoPeer.getNodeID().toHexString(), + discoPeer.getNodeRecord().toString(), discoPeer.getNodeAddress(), + getDiscoMultiAddrTCPFromENR(discoPeer.getNodeRecord(), discoPeer.getPublicKey().toArray())); + unconnectedNewNodes.add(aNewNodeInfo); + allDiscoveredNodes2.put(aNewNodeInfo.nodeID, aNewNodeInfo); + } else { + logger.debug("SKIPPING an already connected node: nodeid:" + discoPeer.getNodeID().toHexString() + + " " + discoPeer.getNodeRecord().toString()); + } + }); + + Set delta = new HashSet(newActiveNodes); + delta.removeAll(activeKnownNodes); // now contains only new sockets + + for (MinimaNodeInfo i : unconnectedNewNodes) { + logger.info("New peer address: " + i.socket.toString().substring(1)); + // TODO: replace ENR with nodeID, but P2PStart.nodeID is not the correct value + // (16... and not the bytes) + if (i.nodeRecord.compareTo(network.getENR()) == 0) { + logger.warn("IGNORING node ENR in list of new peers."); + } else if (i.nodeRecord == null || i.nodeID == null) { + logger.warn("IGNORING empty nodeRecord or nodeID."); + } else { + logger.info("CONNECTING to new ENR " + i.nodeRecord); + System.out.println("Starting MinimaClient: " + i.socket.toString().substring(1) + ":9001"); + String nodeRecord = i.nodeRecord, nodeID = i.nodeID; + MinimaClient mclient = new MinimaClient(i.socket.getAddress().toString().substring(1), 9001, + mNetwork, nodeID, nodeRecord); // hardcode port for now + mNetwork.PostMessage(new Message(NetworkHandler.NETWORK_NEWCLIENT).addObject("client", mclient)); + } + } + + // update known nodes + activeKnownNodes = newActiveNodes; + + // save list + saveP2PNeighbours(); + // try { + // Thread.sleep(5000); + // PostMessage(P2P_START_SCAN); + // } catch(Exception e) { + + // } + } + logger.debug("p2pAddNewNodes: end"); + } + + public String getDiscoMultiAddrTCPFromENR(String ENR, byte[] marshalledPubKey) { + NodeRecord nodeRecord = NodeRecordFactory.DEFAULT.fromEnr(ENR); + // extract multiaddress from nodeRecord + nodeRecord.getTcpAddress(); + //nodeRecord. + StringBuilder strBuilder = new StringBuilder(); + strBuilder.append("/ip4/"); + strBuilder.append(nodeRecord.getTcpAddress().get().getAddress().getHostAddress()); + strBuilder.append("/tcp/"); + strBuilder.append(nodeRecord.getTcpAddress().get().getPort()); + strBuilder.append("/p2p/"); + // construct nodeid from public key + //discoPeer.getPublicKey() +// new PubKey(KeyType.Secp256k1); + //PubKey hostPubKey = PubKey.fromString(discoPeer.getPublicKey()); + //discoPeer.getPublicKey(); +// PubKey pubKey = Secp256k1Kt.unmarshalSecp256k1PublicKey(08021221 + discoPeer.getPublicKey().toArray()); + + PubKey pubKey = Secp256k1Kt.unmarshalSecp256k1PublicKey(marshalledPubKey); + PeerId pid = PeerId.fromPubKey(pubKey); + nodeId = new LibP2PNodeId(pid); + logger.debug("PubKey: " + hex(pubKey.bytes())); + logger.debug("pid: " + pid.toHex()); + logger.debug("Built nodeid of discovered peer: " + nodeId.toBase58()); + strBuilder.append(nodeId.toBase58()); + logger.debug("Discovery TCP address: " + strBuilder.toString()); + logger.debug("discover peer (enr fields): " + nodeRecord.toString()); + return strBuilder.toString(); + } + + //TODO: refactor below code to use above object and constructor instead - if possible + public static void main(String[] args) throws ExecutionException, InterruptedException { + System.out.println("Hello world!"); + } + + private static File ensureFolder(File zFolder) { + if(!zFolder.exists()) { + zFolder.mkdirs(); + } + + return zFolder; + } + +} diff --git a/src/org/minima/system/network/base/PortAvailability.java b/src/org/minima/system/network/base/PortAvailability.java new file mode 100644 index 000000000..e915c5c69 --- /dev/null +++ b/src/org/minima/system/network/base/PortAvailability.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PortAvailability { + private static final Logger LOG = LogManager.getLogger(); + + public static boolean isPortAvailableForTcp(final int port) { + if (!isPortValid(port)) { + return false; + } + try (final ServerSocket serverSocket = new ServerSocket()) { + serverSocket.setReuseAddress(true); + serverSocket.bind(new InetSocketAddress(port)); + return true; + } catch (IOException ex) { + LOG.trace(String.format("failed to open port %d for TCP", port), ex); + } + return false; + } + + public static boolean isPortAvailableForUdp(final int port) { + if (!isPortValid(port)) { + return false; + } + try (final DatagramSocket datagramSocket = new DatagramSocket(null)) { + datagramSocket.setReuseAddress(true); + datagramSocket.bind(new InetSocketAddress(port)); + return true; + } catch (IOException ex) { + LOG.trace(String.format("failed to open port %d for UDP", port), ex); + } + return false; + } + + public static boolean isPortValid(final int port) { + return (port >= 0 && port <= 65535); + } + + public static boolean isPortAvailable(final int port) { + return isPortAvailableForTcp(port) && isPortAvailableForUdp(port); + } +} diff --git a/src/org/minima/system/network/base/SafeFuture.java b/src/org/minima/system/network/base/SafeFuture.java new file mode 100644 index 000000000..b09172fc7 --- /dev/null +++ b/src/org/minima/system/network/base/SafeFuture.java @@ -0,0 +1,637 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class SafeFuture extends CompletableFuture { + + public static SafeFuture COMPLETE = SafeFuture.completedFuture(null); + + public static void reportExceptions(final CompletionStage future) { + future.exceptionally( + error -> { + final Thread currentThread = Thread.currentThread(); + currentThread.getUncaughtExceptionHandler().uncaughtException(currentThread, error); + return null; + }); + } + + public static > Consumer reportExceptions( + final Function action) { + return value -> reportExceptions(action.apply(value)); + } + + public static SafeFuture completedFuture(U value) { + SafeFuture future = new SafeFuture<>(); + future.complete(value); + return future; + } + + public static SafeFuture failedFuture(Throwable ex) { + SafeFuture future = new SafeFuture<>(); + future.completeExceptionally(ex); + return future; + } + + public static SafeFuture of(final CompletionStage stage) { + if (stage instanceof SafeFuture) { + return (SafeFuture) stage; + } + final SafeFuture safeFuture = new SafeFuture<>(); + propagateResult(stage, safeFuture); + return safeFuture; + } + + public static SafeFuture of(final ExceptionThrowingFutureSupplier futureSupplier) { + try { + return SafeFuture.of(futureSupplier.get()); + } catch (Throwable e) { + return SafeFuture.failedFuture(e); + } + } + + public static SafeFuture of(final ExceptionThrowingSupplier supplier) { + try { + return SafeFuture.completedFuture(supplier.get()); + } catch (final Throwable e) { + return SafeFuture.failedFuture(e); + } + } + + /** + * Creates a completed {@link SafeFuture} instance if none of the supplied interruptors are + * completed, else creates an exceptionally completed {@link SafeFuture} instance + * + * @see #orInterrupt(Interruptor...) + */ + public static SafeFuture notInterrupted(Interruptor... interruptors) { + SafeFuture delayedFuture = new SafeFuture<>(); + SafeFuture ret = delayedFuture.orInterrupt(interruptors); + delayedFuture.complete(null); + return ret; + } + + /** + * Creates an {@link Interruptor} instance from the interrupting future and exception supplier for + * the case if interruption is triggered. + * + *

The key feature of {@link Interruptor} and {@link #orInterrupt(Interruptor...)} method is + * that {@code interruptFuture} doesn't hold the reference to dependent futures after they + * complete. It's desired to consider this for long living interrupting futures to avoid memory + * leaks + * + * @param interruptFuture the future which triggers interruption when completes (normally or + * exceptionally) + * @param exceptionSupplier creates a desired exception if interruption is triggered + * @see #notInterrupted(Interruptor...) + * @see #orInterrupt(Interruptor...) + */ + public static Interruptor createInterruptor( + CompletableFuture interruptFuture, Supplier exceptionSupplier) { + return new Interruptor(interruptFuture, exceptionSupplier); + } + + /** + * Repeatedly run the loop until it returns false or completes exceptionally + * + * @param loopBody A supplier for generating futures to be run in succession + * @return A future that will complete when looping terminates + */ + public static SafeFuture asyncDoWhile(ExceptionThrowingFutureSupplier loopBody) { + // Loop while futures complete immediately in order to avoid stack overflow due to recursion + SafeFuture loopFuture = SafeFuture.of(loopBody); + while (loopFuture.isCompletedNormally()) { + if (!loopFuture.join()) { + // Break if the result is false + break; + } + loopFuture = SafeFuture.of(loopBody); + } + + return loopFuture.thenCompose(res -> res ? asyncDoWhile(loopBody) : SafeFuture.COMPLETE); + } + + @SuppressWarnings("FutureReturnValueIgnored") + static void propagateResult(final CompletionStage stage, final SafeFuture safeFuture) { + stage.whenComplete( + (result, error) -> { + if (error != null) { + safeFuture.completeExceptionally(error); + } else { + safeFuture.complete(result); + } + }); + } + + public static SafeFuture fromRunnable(final ExceptionThrowingRunnable action) { + try { + action.run(); + return SafeFuture.COMPLETE; + } catch (Throwable t) { + return SafeFuture.failedFuture(t); + } + } + + public static SafeFuture allOf(final SafeFuture... futures) { + return of(CompletableFuture.allOf(futures)) + .catchAndRethrow(completionException -> addSuppressedErrors(completionException, futures)); + } + + /** + * Adds the {@link Throwable} from each future as a suppressed exception to completionException + * unless it is already set as the cause. + * + *

This ensures that when futures are combined with {@link #allOf(SafeFuture[])} that all + * failures are reported, not just the first one. + * + * @param completionException the exception reported by {@link + * CompletableFuture#allOf(CompletableFuture[])} + * @param futures the futures passed to allOf + */ + @SuppressWarnings("FutureReturnValueIgnored") + public static void addSuppressedErrors( + final Throwable completionException, final SafeFuture[] futures) { + Stream.of(futures) + .forEach( + future -> + future.exceptionally( + error -> { + if (completionException.getCause() != error) { + completionException.addSuppressed(error); + } + return null; + })); + } + + /** + * Returns a new SafeFuture that is completed when all of the given SafeFutures complete + * successfully or completes exceptionally immediately when any of the SafeFutures complete + * exceptionally. The results, if any, of the given SafeFutures are not reflected in the returned + * SafeFuture, but may be obtained by inspecting them individually. If no SafeFutures are + * provided, returns a SafeFuture completed with the value {@code null}. + * + *

Among the applications of this method is to await completion of a set of independent + * SafeFutures before continuing a program, as in: {@code SafeFuture.allOf(c1, c2, c3).join();}. + * + * @param futures the SafeFutures + * @return a new SafeFuture that is completed when all of the given SafeFutures complete + * @throws NullPointerException if the array or any of its elements are {@code null} + */ + public static SafeFuture allOfFailFast(final SafeFuture... futures) { + final SafeFuture complete = new SafeFuture<>(); + Stream.of(futures).forEach(future -> future.finish(() -> {}, complete::completeExceptionally)); + allOf(futures).propagateTo(complete); + return complete; + } + + public static SafeFuture anyOf(final SafeFuture... futures) { + return of(CompletableFuture.anyOf(futures)); + } + + public SafeFuture toVoid() { + return thenAccept(__ -> {}); + } + + public boolean isCompletedNormally() { + return isDone() && !isCompletedExceptionally() && !isCancelled(); + } + + @Override + public SafeFuture newIncompleteFuture() { + return new SafeFuture<>(); + } + + public void reportExceptions() { + reportExceptions(this); + } + + public void finish(final Runnable onSuccess, final Consumer onError) { + finish(result -> onSuccess.run(), onError); + } + + public void propagateTo(final SafeFuture target) { + propagateResult(this, target); + } + + public void propagateToAsync(final SafeFuture target, final AsyncRunner asyncRunner) { + finish( + result -> asyncRunner.runAsync(() -> target.complete(result)).reportExceptions(), + error -> + asyncRunner.runAsync(() -> target.completeExceptionally(error)).reportExceptions()); + } + + /** + * Completes the {@code target} exceptionally if and only if this future is completed + * exceptionally + */ + public void propagateExceptionTo(final SafeFuture target) { + finish(() -> {}, target::completeExceptionally); + } + + /** + * Run final logic on success or error + * + * @param onFinished Task to run when future completes successfully or exceptionally + */ + public void always(final Runnable onFinished) { + finish(res -> onFinished.run(), err -> onFinished.run()); + } + + public SafeFuture alwaysRun(final Runnable action) { + return exceptionallyCompose( + error -> { + action.run(); + return failedFuture(error); + }) + .thenPeek(value -> action.run()); + } + + public void finish(final Consumer onSuccess, final Consumer onError) { + handle( + (result, error) -> { + if (error != null) { + onError.accept(error); + } else { + onSuccess.accept(result); + } + return null; + }) + .reportExceptions(); + } + + public void finish(final Consumer onError) { + handle( + (result, error) -> { + if (error != null) { + onError.accept(error); + } + return null; + }) + .reportExceptions(); + } + + public void finishAsync(final Consumer onError, final Executor executor) { + finishAsync(__ -> {}, onError, executor); + } + + public void finishAsync( + final Runnable onSuccess, final Consumer onError, final Executor executor) { + finishAsync(__ -> onSuccess.run(), onError, executor); + } + + public void finishAsync( + final Consumer onSuccess, final Consumer onError, final Executor executor) { + handleAsync( + (result, error) -> { + if (error != null) { + onError.accept(error); + } else { + onSuccess.accept(result); + } + return null; + }, + executor) + .reportExceptions(); + } + + /** + * Returns a new CompletionStage that, when the provided stage completes exceptionally, is + * executed with the provided stage's exception as the argument to the supplied function. + * Otherwise the returned stage completes successfully with the same value as the provided stage. + * + *

This is the exceptional equivalent to {@link CompletionStage#thenCompose(Function)} + * + * @param errorHandler the function returning a new CompletionStage + * @return the SafeFuture + */ + @SuppressWarnings({"FutureReturnValueIgnored", "MissingOverride"}) + public SafeFuture exceptionallyCompose( + final Function> errorHandler) { + final SafeFuture result = new SafeFuture<>(); + whenComplete( + (value, error) -> { + try { + final CompletionStage nextStep = + error != null ? errorHandler.apply(error) : completedFuture(value); + propagateResult(nextStep, result); + } catch (final Throwable t) { + result.completeExceptionally(t); + } + }); + return result; + } + + /** + * Returns a new CompletionStage that, when the this stage completes exceptionally, executes the + * provided {@code ExceptionThrowingConsumer} with the exception as the argument. The returned + * stage will be exceptionally completed with the same exception if the consumer completes without + * exceptions. If the consumer throws exception then the returned stage will be completed with + * thrown exception. + * + *

This is equivalent to a catch block that performs some action and then either rethrows the + * original exception or throws a new one + * + * @param onError the function to executor when this stage completes exceptionally. + * @return a new SafeFuture which completes with the same successful result as this stage or + * exceptionally with original exception or a new one + */ + public SafeFuture catchAndRethrow(final ExceptionThrowingConsumer onError) { + return exceptionallyCompose( + error -> { + try { + onError.accept(error); + return failedFuture(error); + } catch (Throwable t) { + return failedFuture(t); + } + }); + } + + public static SafeFuture supplyAsync(final Supplier supplier) { + return SafeFuture.of(CompletableFuture.supplyAsync(supplier)); + } + + @SuppressWarnings("unchecked") + @Override + public SafeFuture thenApply(final Function fn) { + return (SafeFuture) super.thenApply(fn); + } + + public SafeFuture thenApplyChecked(final ExceptionThrowingFunction function) { + return thenCompose( + value -> { + try { + final U result = function.apply(value); + return SafeFuture.completedFuture(result); + } catch (final Throwable e) { + return SafeFuture.failedFuture(e); + } + }); + } + + /** Shortcut to process the value when complete and return the same future */ + public SafeFuture thenPeek(Consumer fn) { + return thenApply( + v -> { + fn.accept(v); + return v; + }); + } + + @Override + public SafeFuture thenRun(final Runnable action) { + return (SafeFuture) super.thenRun(action); + } + + @Override + public SafeFuture thenRunAsync(final Runnable action, final Executor executor) { + return (SafeFuture) super.thenRunAsync(action, executor); + } + + @Override + public SafeFuture thenAccept(final Consumer action) { + return (SafeFuture) super.thenAccept(action); + } + + @Override + public SafeFuture thenAcceptAsync( + final Consumer action, final Executor executor) { + return (SafeFuture) super.thenAcceptAsync(action, executor); + } + + @SuppressWarnings("unchecked") + @Override + public SafeFuture thenCombine( + final CompletionStage other, + final BiFunction fn) { + return (SafeFuture) super.thenCombine(other, fn); + } + + @Override + public SafeFuture thenCompose(final Function> fn) { + return (SafeFuture) super.thenCompose(fn); + } + + @Override + public SafeFuture thenComposeAsync( + final Function> fn, final Executor executor) { + return (SafeFuture) super.thenComposeAsync(fn, executor); + } + + @SuppressWarnings("unchecked") + @Override + public SafeFuture thenCombineAsync( + final CompletionStage other, + final BiFunction fn, + final Executor executor) { + return (SafeFuture) super.thenCombineAsync(other, fn, executor); + } + + @Override + public SafeFuture exceptionally(final Function fn) { + return (SafeFuture) super.exceptionally(fn); + } + + @SuppressWarnings("unchecked") + @Override + public SafeFuture handle(final BiFunction fn) { + return (SafeFuture) super.handle(fn); + } + + @SuppressWarnings("unchecked") + @Override + public SafeFuture handleAsync( + final BiFunction fn, final Executor executor) { + return (SafeFuture) super.handleAsync(fn, executor); + } + + /** + * Returns a new CompletionStage that, when this stage completes either normally or exceptionally, + * is executed with this stage's result and exception as arguments to the supplied function. + * + *

When this stage is complete, the given function is invoked with the result (or {@code null} + * if none) and the exception (or {@code null} if none) returning another `CompletionStage`. When + * that stage completes, the `SafeFuture` returned by this method is completed with the same value + * or exception. + * + * @param fn the function to use to compute another CompletionStage + * @param the function's return type + * @return the new SafeFuture + */ + @SuppressWarnings({"FutureReturnValueIgnored"}) + public SafeFuture handleComposed( + final BiFunction> fn) { + final SafeFuture result = new SafeFuture<>(); + whenComplete( + (value, error) -> { + try { + propagateResult(fn.apply(value, error), result); + } catch (final Throwable t) { + result.completeExceptionally(t); + } + }); + return result; + } + + @Override + public SafeFuture whenComplete(final BiConsumer action) { + return (SafeFuture) super.whenComplete(action); + } + + public SafeFuture orTimeout(final Duration timeout) { + return orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public SafeFuture orTimeout(final long timeout, final TimeUnit unit) { + return (SafeFuture) super.orTimeout(timeout, unit); + } + + /** + * Returns the future which completes with the same result or exception The consumer is invoked if + * this future completes exceptionally + */ + public SafeFuture whenException(final Consumer action) { + return (SafeFuture) + super.whenComplete( + (r, t) -> { + if (t != null) { + action.accept(t); + } + }); + } + + /** + * Returns a void future that completes successfully with null result. The consumer is invoked if + * this future completes exceptions and the returned future only completes once the consumer + * returns. + * + *

The returned future will only complete exceptionally if the consumer throws an exception. + * + * @param action the exception handler to invoke. + * @return a void future that completes successfully unless the consumer throws an exception. + */ + public SafeFuture handleException(final Consumer action) { + return handle( + (__, error) -> { + if (error != null) { + action.accept(error); + } + return null; + }); + } + + /** + * Returns the future which completes with the same result or exception as this one. The resulting + * future becomes complete when `waitForStage` completes. If the `waitForStage` completes + * exceptionally the resulting future also completes exceptionally with the same exception + */ + public SafeFuture thenWaitFor(Function> waitForStage) { + return thenCompose(t -> waitForStage.apply(t).thenApply(__ -> t)); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + public final SafeFuture or(SafeFuture... others) { + SafeFuture[] futures = Arrays.copyOf(others, others.length + 1); + futures[others.length] = this; + return anyOf(futures).thenApply(o -> (T) o); + } + + /** + * Derives a {@link SafeFuture} which yields the same result as this {@link SafeFuture} if no + * {@link Interruptor} was triggered before this future is done. + * + *

If any of supplied {@link Interruptor}s is triggered the returned {@link SafeFuture} is + * completed exceptionally. The exception thrown depends on which specific Interruptor was + * triggered + * + *

The key feature of this method is that {@code interruptFuture} contained in Interruptor + * doesn't hold the reference to dependent futures after they complete. It's desired to consider + * this for long living interrupting futures to avoid memory leaks + * + * @param interruptors a set of interruptors which futures trigger interruption if complete + * (normally or exceptionally) + * @see #createInterruptor(CompletableFuture, Supplier) + */ + // The result of anyOf() future is ignored since it is used just to handle completion + // of any future. All possible outcomes are propagated to the returned future instance + @SuppressWarnings("FutureReturnValueIgnored") + public SafeFuture orInterrupt(Interruptor... interruptors) { + CompletableFuture[] allFuts = new CompletableFuture[interruptors.length + 1]; + allFuts[0] = this; + for (int i = 0; i < interruptors.length; i++) { + allFuts[i + 1] = interruptors[i].interruptFuture; + } + SafeFuture ret = new SafeFuture<>(); + anyOf(allFuts) + .whenComplete( + (res, err) -> { + if (this.isDone()) { + this.propagateTo(ret); + } else { + for (Interruptor interruptor : interruptors) { + if (interruptor.interruptFuture.isDone()) { + try { + interruptor.getInterruptFuture().get(); + ret.completeExceptionally(interruptor.getExceptionSupplier().get()); + } catch (Exception e) { + ret.completeExceptionally(e); + } + } + } + } + }); + return ret; + } + + /** + * Class containing an interrupting Future and exception supplier which produces exception if + * interrupting Future is triggered + * + * @see #createInterruptor(CompletableFuture, Supplier) + * @see #orInterrupt(Interruptor...) + * @see #notInterrupted(Interruptor...) + */ + public static class Interruptor { + private final CompletableFuture interruptFuture; + private final Supplier exceptionSupplier; + + private Interruptor( + CompletableFuture interruptFuture, Supplier exceptionSupplier) { + this.interruptFuture = interruptFuture; + this.exceptionSupplier = exceptionSupplier; + } + + private CompletableFuture getInterruptFuture() { + return interruptFuture; + } + + private Supplier getExceptionSupplier() { + return exceptionSupplier; + } + } +} diff --git a/src/org/minima/system/network/base/Service.java b/src/org/minima/system/network/base/Service.java new file mode 100644 index 000000000..4f3791d97 --- /dev/null +++ b/src/org/minima/system/network/base/Service.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.concurrent.atomic.AtomicReference; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; + +public abstract class Service { + enum State { + IDLE, + RUNNING, + STOPPED + } + + private final AtomicReference state = new AtomicReference<>(State.IDLE); + + public SafeFuture start() { + if (!state.compareAndSet(State.IDLE, State.RUNNING)) { + return SafeFuture.failedFuture( + new IllegalStateException("Attempt to start an already started service.")); + } + return doStart(); + } + + protected abstract SafeFuture doStart(); + + public SafeFuture stop() { + if (state.compareAndSet(State.RUNNING, State.STOPPED)) { + return doStop(); + } else { + // Return a successful future if there's nothing to do at this point + return SafeFuture.COMPLETE; + } + } + + protected abstract SafeFuture doStop(); + + public boolean isRunning() { + return state.get() == State.RUNNING; + } +} diff --git a/src/org/minima/system/network/base/StubTimeProvider.java b/src/org/minima/system/network/base/StubTimeProvider.java new file mode 100644 index 000000000..c40ba1e56 --- /dev/null +++ b/src/org/minima/system/network/base/StubTimeProvider.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.util.concurrent.TimeUnit; +//import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +import org.minima.system.network.base.ssz.UInt64; + +public class StubTimeProvider implements TimeProvider { + + private UInt64 timeInMillis; + + private StubTimeProvider(final UInt64 timeInMillis) { + this.timeInMillis = timeInMillis; + } + + public static StubTimeProvider withTimeInSeconds(final long timeInSeconds) { + return withTimeInSeconds(UInt64.valueOf(timeInSeconds)); + } + + public static StubTimeProvider withTimeInSeconds(final UInt64 timeInSeconds) { + return withTimeInMillis(timeInSeconds.times(MILLIS_PER_SECOND)); + } + + public static StubTimeProvider withTimeInMillis(final long timeInMillis) { + return withTimeInMillis(UInt64.valueOf(timeInMillis)); + } + + public static StubTimeProvider withTimeInMillis(final UInt64 timeInMillis) { + return new StubTimeProvider(timeInMillis); + } + + public void advanceTimeBySeconds(final long seconds) { + advanceTimeByMillis(TimeUnit.SECONDS.toMillis(seconds)); + } + + public void advanceTimeByMillis(final long millis) { + this.timeInMillis = timeInMillis.plus(millis); + } + + @Override + public UInt64 getTimeInMillis() { + return timeInMillis; + } +} diff --git a/src/org/minima/system/network/base/TimeProvider.java b/src/org/minima/system/network/base/TimeProvider.java new file mode 100644 index 000000000..78f16456d --- /dev/null +++ b/src/org/minima/system/network/base/TimeProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import org.minima.system.network.base.ssz.UInt64; + +//import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public interface TimeProvider { + + UInt64 MILLIS_PER_SECOND = UInt64.valueOf(1000); + + UInt64 getTimeInMillis(); + + default UInt64 getTimeInSeconds() { + return getTimeInMillis().dividedBy(MILLIS_PER_SECOND); + } +} diff --git a/src/org/minima/system/network/base/Waiter.java b/src/org/minima/system/network/base/Waiter.java new file mode 100644 index 000000000..38f6a36e1 --- /dev/null +++ b/src/org/minima/system/network/base/Waiter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.awaitility.Awaitility; +import org.awaitility.pollinterval.IterativePollInterval; + +/** + * A simpler wrapper around Awaitility that directs people towards best practices for waiting. The + * native Awaitility wrapper has a number of "gotchas" that can lead to intermittency which this + * wrapper aims to prevent. + */ +public class Waiter { + + private static final int DEFAULT_TIMEOUT_SECONDS = 30; + private static final Duration INITIAL_POLL_INTERVAL = Duration.ofMillis(200); + private static final Duration MAX_POLL_INTERVAL = Duration.ofSeconds(5); + + public static void waitFor( + final Condition assertion, final int timeoutValue, final TimeUnit timeUnit) { + Awaitility.waitAtMost(timeoutValue, timeUnit) + .ignoreExceptions() + .pollInterval( + IterativePollInterval.iterative(Waiter::nextPollInterval, INITIAL_POLL_INTERVAL)) + .untilAsserted(assertion::run); + } + + public static void waitFor(final Condition assertion) { + waitFor(assertion, DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + private static Duration nextPollInterval(final Duration duration) { + final Duration nextInterval = duration.multipliedBy(2); + return nextInterval.compareTo(MAX_POLL_INTERVAL) <= 0 ? nextInterval : MAX_POLL_INTERVAL; + } + + public static T waitFor(final Future future) + throws InterruptedException, ExecutionException, TimeoutException { + return future.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + public static T waitFor(final Future future, final Duration duration) + throws InterruptedException, ExecutionException, TimeoutException { + return future.get(duration.toSeconds(), TimeUnit.SECONDS); + } + + public interface Condition { + void run() throws Throwable; + } + + public static void ensureConditionRemainsMet( + final Condition condition, int waitTimeInMilliseconds) throws InterruptedException { + final long mustBeTrueUntil = System.currentTimeMillis() + waitTimeInMilliseconds; + while (System.currentTimeMillis() < mustBeTrueUntil) { + try { + condition.run(); + } catch (final Throwable t) { + throw new RuntimeException("Condition did not remain met", t); + } + Thread.sleep(500); + } + } + + public static void ensureConditionRemainsMet(final Condition condition) + throws InterruptedException { + ensureConditionRemainsMet(condition, 2000); + } +} diff --git a/src/org/minima/system/network/base/WireLogsConfig.java b/src/org/minima/system/network/base/WireLogsConfig.java new file mode 100644 index 000000000..dfcc67bf4 --- /dev/null +++ b/src/org/minima/system/network/base/WireLogsConfig.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class WireLogsConfig { + private final boolean logWireCipher; + private final boolean logWirePlain; + private final boolean logWireMuxFrames; + private final boolean logWireGossip; + + private WireLogsConfig( + boolean logWireCipher, + boolean logWirePlain, + boolean logWireMuxFrames, + boolean logWireGossip) { + this.logWireCipher = logWireCipher; + this.logWirePlain = logWirePlain; + this.logWireMuxFrames = logWireMuxFrames; + this.logWireGossip = logWireGossip; + } + + public static Builder builder() { + return new Builder(); + } + + public static WireLogsConfig createDefault() { + return builder().build(); + } + + public boolean isLogWireCipher() { + return logWireCipher; + } + + public boolean isLogWirePlain() { + return logWirePlain; + } + + public boolean isLogWireMuxFrames() { + return logWireMuxFrames; + } + + public boolean isLogWireGossip() { + return logWireGossip; + } + + public static class Builder { + private Boolean logWireCipher = false; + private Boolean logWirePlain = false; + private Boolean logWireMuxFrames = false; + private Boolean logWireGossip = false; + + private Builder() {} + + public WireLogsConfig build() { + return new WireLogsConfig(logWireCipher, logWirePlain, logWireMuxFrames, logWireGossip); + } + + public Builder logWireCipher(final Boolean logWireCipher) { + checkNotNull(logWireCipher); + this.logWireCipher = logWireCipher; + return this; + } + + public Builder logWirePlain(final Boolean logWirePlain) { + checkNotNull(logWirePlain); + this.logWirePlain = logWirePlain; + return this; + } + + public Builder logWireMuxFrames(final Boolean logWireMuxFrames) { + checkNotNull(logWireMuxFrames); + this.logWireMuxFrames = logWireMuxFrames; + return this; + } + + public Builder logWireGossip(final Boolean logWireGossip) { + checkNotNull(logWireGossip); + this.logWireGossip = logWireGossip; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/gossip/GossipNetwork.java b/src/org/minima/system/network/base/gossip/GossipNetwork.java new file mode 100644 index 000000000..0f8ab65d7 --- /dev/null +++ b/src/org/minima/system/network/base/gossip/GossipNetwork.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip; + +import java.util.Collection; +import java.util.Map; + +import org.apache.tuweni.bytes.Bytes; +//import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.SafeFuture; +import org.minima.system.network.base.gossip.config.GossipTopicsScoringConfig; +//import tech.pegasys.teku.networking.p2p.gossip.config.GossipTopicsScoringConfig; +//import tech.pegasys.teku.networking.p2p.peer.NodeId; +import org.minima.system.network.base.peer.NodeId; + +public interface GossipNetwork { + SafeFuture gossip(String topic, Bytes data); + + TopicChannel subscribe(String topic, TopicHandler topicHandler); + + Map> getSubscribersByTopic(); + + void updateGossipTopicScoring(final GossipTopicsScoringConfig config); +} diff --git a/src/org/minima/system/network/base/gossip/PreparedGossipMessage.java b/src/org/minima/system/network/base/gossip/PreparedGossipMessage.java new file mode 100644 index 000000000..139199dad --- /dev/null +++ b/src/org/minima/system/network/base/gossip/PreparedGossipMessage.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + + +package org.minima.system.network.base.gossip; + +//import org.apache.tuweni.bytes.Bytes; + +/** + * Semi-processed raw gossip message which can supply Gossip 'message-id' + * + * @see TopicHandler#prepareMessage(Bytes) + */ +public interface PreparedGossipMessage { + + /** + * Returns the Gossip 'message-id' If the 'message-id' calculation is resource consuming operation + * is should performed lazily by implementation class + */ + byte[] getMessageId(); +} diff --git a/src/org/minima/system/network/base/gossip/PreparedGossipMessageFactory.java b/src/org/minima/system/network/base/gossip/PreparedGossipMessageFactory.java new file mode 100644 index 000000000..354b2bd39 --- /dev/null +++ b/src/org/minima/system/network/base/gossip/PreparedGossipMessageFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip; + +//import org.apache.tuweni.bytes.Bytes; + +/** Factory for {@link PreparedGossipMessage} instances */ +public interface PreparedGossipMessageFactory { + + /** Creates a {@link PreparedGossipMessage} instance */ + PreparedGossipMessage create(String topic, byte[] payload); +} diff --git a/src/org/minima/system/network/base/gossip/TopicChannel.java b/src/org/minima/system/network/base/gossip/TopicChannel.java new file mode 100644 index 000000000..6c303d6c1 --- /dev/null +++ b/src/org/minima/system/network/base/gossip/TopicChannel.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + + +package org.minima.system.network.base.gossip; + +//import org.apache.tuweni.bytes.Bytes; + +public interface TopicChannel { + void gossip(byte[] data); + + void close(); +} diff --git a/src/org/minima/system/network/base/gossip/TopicHandler.java b/src/org/minima/system/network/base/gossip/TopicHandler.java new file mode 100644 index 000000000..08a07b8bf --- /dev/null +++ b/src/org/minima/system/network/base/gossip/TopicHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip; + +import io.libp2p.core.pubsub.ValidationResult; +import org.minima.system.network.base.SafeFuture; + +//import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; + +public interface TopicHandler { + + /** + * Preprocess 'raw' Gossip message returning the instance which may calculate Gossip 'message-id' + * and cache intermediate data for later message handling with {@link + * #handleMessage(PreparedGossipMessage)} + */ + PreparedGossipMessage prepareMessage(byte[] payload); + + /** + * Validates and handles gossip message preprocessed earlier by {@link #prepareMessage(Bytes)} + * + * @param message The preprocessed gossip message + * @return Message validation promise + */ + SafeFuture handleMessage(PreparedGossipMessage message); +} diff --git a/src/org/minima/system/network/base/gossip/config/GossipConfig.java b/src/org/minima/system/network/base/gossip/config/GossipConfig.java new file mode 100644 index 000000000..4abed8fbb --- /dev/null +++ b/src/org/minima/system/network/base/gossip/config/GossipConfig.java @@ -0,0 +1,206 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip.config; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.Duration; +import java.util.function.Consumer; + +/** + * Gossip options + * https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#the-gossip-domain-gossipsub + */ +public class GossipConfig { + public static final int DEFAULT_D = 6; + public static final int DEFAULT_D_LOW = 5; + public static final int DEFAULT_D_HIGH = 12; + public static final int DEFAULT_D_LAZY = 6; + public static final Duration DEFAULT_FANOUT_TTL = Duration.ofSeconds(60); + public static final int DEFAULT_ADVERTISE = 3; + public static final int DEFAULT_HISTORY = 6; + public static final Duration DEFAULT_HEARTBEAT_INTERVAL = Duration.ofMillis(700); + public static final Duration DEFAULT_SEEN_TTL = DEFAULT_HEARTBEAT_INTERVAL.multipliedBy(550); + + private final int d; + private final int dLow; + private final int dHigh; + private final int dLazy; + private final Duration fanoutTTL; + private final int advertise; + private final int history; + private final Duration heartbeatInterval; + private final Duration seenTTL; + private final GossipScoringConfig scoringConfig; + + private GossipConfig( + int d, + int dLow, + int dHigh, + int dLazy, + Duration fanoutTTL, + int advertise, + int history, + Duration heartbeatInterval, + Duration seenTTL, + final GossipScoringConfig scoringConfig) { + this.d = d; + this.dLow = dLow; + this.dHigh = dHigh; + this.dLazy = dLazy; + this.fanoutTTL = fanoutTTL; + this.advertise = advertise; + this.history = history; + this.heartbeatInterval = heartbeatInterval; + this.seenTTL = seenTTL; + this.scoringConfig = scoringConfig; + } + + public static Builder builder() { + return new Builder(); + } + + public static GossipConfig createDefault() { + return builder().build(); + } + + public int getD() { + return d; + } + + public int getDLow() { + return dLow; + } + + public int getDHigh() { + return dHigh; + } + + public int getDLazy() { + return dLazy; + } + + public Duration getFanoutTTL() { + return fanoutTTL; + } + + public int getAdvertise() { + return advertise; + } + + public int getHistory() { + return history; + } + + public Duration getHeartbeatInterval() { + return heartbeatInterval; + } + + public Duration getSeenTTL() { + return seenTTL; + } + + public GossipScoringConfig getScoringConfig() { + return scoringConfig; + } + + public static class Builder { + private final GossipScoringConfig.Builder scoringConfigBuilder = GossipScoringConfig.builder(); + + private Integer d = DEFAULT_D; + private Integer dLow = DEFAULT_D_LOW; + private Integer dHigh = DEFAULT_D_HIGH; + private Integer dLazy = DEFAULT_D_LAZY; + private Duration fanoutTTL = DEFAULT_FANOUT_TTL; + private Integer advertise = DEFAULT_ADVERTISE; + private Integer history = DEFAULT_HISTORY; + private Duration heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL; + private Duration seenTTL = DEFAULT_SEEN_TTL; + + private Builder() {} + + public GossipConfig build() { + return new GossipConfig( + d, + dLow, + dHigh, + dLazy, + fanoutTTL, + advertise, + history, + heartbeatInterval, + seenTTL, + scoringConfigBuilder.build()); + } + + public Builder scoring(final Consumer consumer) { + consumer.accept(scoringConfigBuilder); + return this; + } + + public Builder d(final Integer d) { + checkNotNull(d); + this.d = d; + return this; + } + + public Builder dLow(final Integer dLow) { + checkNotNull(dLow); + this.dLow = dLow; + return this; + } + + public Builder dHigh(final Integer dHigh) { + checkNotNull(dHigh); + this.dHigh = dHigh; + return this; + } + + public Builder dLazy(final Integer dLazy) { + checkNotNull(dLazy); + this.dLazy = dLazy; + return this; + } + + public Builder fanoutTTL(final Duration fanoutTTL) { + checkNotNull(fanoutTTL); + this.fanoutTTL = fanoutTTL; + return this; + } + + public Builder advertise(final Integer advertise) { + checkNotNull(advertise); + this.advertise = advertise; + return this; + } + + public Builder history(final Integer history) { + checkNotNull(history); + this.history = history; + return this; + } + + public Builder heartbeatInterval(final Duration heartbeatInterval) { + checkNotNull(heartbeatInterval); + this.heartbeatInterval = heartbeatInterval; + return this; + } + + public Builder seenTTL(final Duration seenTTL) { + checkNotNull(seenTTL); + this.seenTTL = seenTTL; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/gossip/config/GossipPeerScoringConfig.java b/src/org/minima/system/network/base/gossip/config/GossipPeerScoringConfig.java new file mode 100644 index 000000000..37bb744cb --- /dev/null +++ b/src/org/minima/system/network/base/gossip/config/GossipPeerScoringConfig.java @@ -0,0 +1,246 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip.config; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.Duration; +import java.util.Optional; +import org.minima.system.network.base.peer.NodeId; + +public class GossipPeerScoringConfig { + private final double topicScoreCap; + private final double appSpecificWeight; + private final Optional peerScorer; + private final Optional whitelistManager; + private final Optional directPeerManager; + private final double ipColocationFactorWeight; + private final int ipColocationFactorThreshold; + private final double behaviourPenaltyWeight; + private final double behaviourPenaltyDecay; + private final double behaviourPenaltyThreshold; + private final Duration decayInterval; + private final double decayToZero; + private final Duration retainScore; + + private GossipPeerScoringConfig( + final double topicScoreCap, + final Optional directPeerManager, + final Optional peerScorer, + final double appSpecificWeight, + final Optional whitelistManager, + final double ipColocationFactorWeight, + final int ipColocationFactorThreshold, + final double behaviourPenaltyWeight, + final double behaviourPenaltyDecay, + final double behaviourPenaltyThreshold, + final Duration decayInterval, + final double decayToZero, + final Duration retainScore) { + this.topicScoreCap = topicScoreCap; + this.directPeerManager = directPeerManager; + this.peerScorer = peerScorer; + this.appSpecificWeight = appSpecificWeight; + this.whitelistManager = whitelistManager; + this.ipColocationFactorWeight = ipColocationFactorWeight; + this.ipColocationFactorThreshold = ipColocationFactorThreshold; + this.behaviourPenaltyWeight = behaviourPenaltyWeight; + this.behaviourPenaltyDecay = behaviourPenaltyDecay; + this.behaviourPenaltyThreshold = behaviourPenaltyThreshold; + this.decayInterval = decayInterval; + this.decayToZero = decayToZero; + this.retainScore = retainScore; + } + + public static Builder builder() { + return new Builder(); + } + + public double getTopicScoreCap() { + return topicScoreCap; + } + + public double getAppSpecificWeight() { + return appSpecificWeight; + } + + public Optional getAppSpecificScorer() { + return peerScorer; + } + + public Optional getWhitelistManager() { + return whitelistManager; + } + + public Optional getDirectPeerManager() { + return directPeerManager; + } + + public double getIpColocationFactorWeight() { + return ipColocationFactorWeight; + } + + public int getIpColocationFactorThreshold() { + return ipColocationFactorThreshold; + } + + public double getBehaviourPenaltyWeight() { + return behaviourPenaltyWeight; + } + + public double getBehaviourPenaltyDecay() { + return behaviourPenaltyDecay; + } + + public double getBehaviourPenaltyThreshold() { + return behaviourPenaltyThreshold; + } + + public Duration getDecayInterval() { + return decayInterval; + } + + public double getDecayToZero() { + return decayToZero; + } + + public Duration getRetainScore() { + return retainScore; + } + + public static class Builder { + private Double topicScoreCap = 0.0; + private Double appSpecificWeight = 0.0; + private Optional appSpecificScorer = Optional.empty(); + private Optional whitelistManager = Optional.empty(); + private Optional directPeerManager = Optional.empty(); + private Double ipColocationFactorWeight = 0.0; + private Integer ipColocationFactorThreshold = 0; + private Double behaviourPenaltyWeight = 0.0; + private Double behaviourPenaltyDecay = 0.9; + private Double behaviourPenaltyThreshold = 1.0; + private Duration decayInterval = Duration.ofMinutes(1); + private Double decayToZero = 0.0; + private Duration retainScore = Duration.ofMinutes(10); + + private Builder() {} + + public GossipPeerScoringConfig build() { + return new GossipPeerScoringConfig( + topicScoreCap, + directPeerManager, + appSpecificScorer, + appSpecificWeight, + whitelistManager, + ipColocationFactorWeight, + ipColocationFactorThreshold, + behaviourPenaltyWeight, + behaviourPenaltyDecay, + behaviourPenaltyThreshold, + decayInterval, + decayToZero, + retainScore); + } + + public Builder topicScoreCap(final Double topicScoreCap) { + checkNotNull(topicScoreCap); + this.topicScoreCap = topicScoreCap; + return this; + } + + public Builder appSpecificWeight(final Double appSpecificWeight) { + checkNotNull(appSpecificWeight); + this.appSpecificWeight = appSpecificWeight; + return this; + } + + public Builder appSpecificScorer(final Optional appSpecificScorer) { + checkNotNull(appSpecificScorer); + this.appSpecificScorer = appSpecificScorer; + return this; + } + + public Builder whitelistManager(final Optional whitelistManager) { + checkNotNull(whitelistManager); + this.whitelistManager = whitelistManager; + return this; + } + + public Builder directPeerManager(final Optional directPeerManager) { + checkNotNull(directPeerManager); + this.directPeerManager = directPeerManager; + return this; + } + + public Builder ipColocationFactorWeight(final Double ipColocationFactorWeight) { + checkNotNull(ipColocationFactorWeight); + this.ipColocationFactorWeight = ipColocationFactorWeight; + return this; + } + + public Builder ipColocationFactorThreshold(final Integer ipColocationFactorThreshold) { + checkNotNull(ipColocationFactorThreshold); + this.ipColocationFactorThreshold = ipColocationFactorThreshold; + return this; + } + + public Builder behaviourPenaltyWeight(final Double behaviourPenaltyWeight) { + checkNotNull(behaviourPenaltyWeight); + this.behaviourPenaltyWeight = behaviourPenaltyWeight; + return this; + } + + public Builder behaviourPenaltyDecay(final Double behaviourPenaltyDecay) { + checkNotNull(behaviourPenaltyDecay); + this.behaviourPenaltyDecay = behaviourPenaltyDecay; + return this; + } + + public Builder behaviourPenaltyThreshold(final Double behaviourPenaltyThreshold) { + checkNotNull(behaviourPenaltyThreshold); + this.behaviourPenaltyThreshold = behaviourPenaltyThreshold; + return this; + } + + public Builder decayInterval(final Duration decayInterval) { + checkNotNull(decayInterval); + this.decayInterval = decayInterval; + return this; + } + + public Builder decayToZero(final Double decayToZero) { + checkNotNull(decayToZero); + this.decayToZero = decayToZero; + return this; + } + + public Builder retainScore(final Duration retainScore) { + checkNotNull(retainScore); + this.retainScore = retainScore; + return this; + } + } + + public interface PeerScorer { + double scorePeer(final NodeId peer); + } + + public interface DirectPeerManager { + boolean isDirectPeer(final NodeId peer); + } + + public interface WhitelistedIpManager { + boolean isWhitelisted(final String ipAddress); + } +} diff --git a/src/org/minima/system/network/base/gossip/config/GossipScoringConfig.java b/src/org/minima/system/network/base/gossip/config/GossipScoringConfig.java new file mode 100644 index 000000000..10ba5590b --- /dev/null +++ b/src/org/minima/system/network/base/gossip/config/GossipScoringConfig.java @@ -0,0 +1,167 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip.config; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; +import java.util.function.Consumer; + +/** Gossip scoring config. Contains peer scoring and topic scoring configs. */ +public class GossipScoringConfig { + private final GossipPeerScoringConfig peerScoringConfig; + private final GossipTopicScoringConfig defaultTopicScoringConfig; + private final Map topicScoringConfig; + private final double gossipThreshold; + private final double publishThreshold; + private final double graylistThreshold; + private final double acceptPXThreshold; + private final double opportunisticGraftThreshold; + + private GossipScoringConfig( + final GossipPeerScoringConfig peerScoringConfig, + final GossipTopicScoringConfig defaultTopicScoringConfig, + final Map topicScoringConfig, + final double gossipThreshold, + final double publishThreshold, + final double graylistThreshold, + final double acceptPXThreshold, + final double opportunisticGraftThreshold) { + this.peerScoringConfig = peerScoringConfig; + this.defaultTopicScoringConfig = defaultTopicScoringConfig; + this.topicScoringConfig = topicScoringConfig; + this.gossipThreshold = gossipThreshold; + this.publishThreshold = publishThreshold; + this.graylistThreshold = graylistThreshold; + this.acceptPXThreshold = acceptPXThreshold; + this.opportunisticGraftThreshold = opportunisticGraftThreshold; + } + + public GossipPeerScoringConfig getPeerScoringConfig() { + return peerScoringConfig; + } + + public GossipTopicScoringConfig getDefaultTopicScoringConfig() { + return defaultTopicScoringConfig; + } + + public Map getTopicScoringConfig() { + return topicScoringConfig; + } + + public double getGossipThreshold() { + return gossipThreshold; + } + + public double getPublishThreshold() { + return publishThreshold; + } + + public double getGraylistThreshold() { + return graylistThreshold; + } + + public double getAcceptPXThreshold() { + return acceptPXThreshold; + } + + public double getOpportunisticGraftThreshold() { + return opportunisticGraftThreshold; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final GossipPeerScoringConfig.Builder peerScoringConfigBuilder = + GossipPeerScoringConfig.builder(); + private final GossipTopicScoringConfig.Builder defaultTopicScoringConfigBuilder = + GossipTopicScoringConfig.builder(); + private final GossipTopicsScoringConfig.Builder topicsScoringBuilder = + GossipTopicsScoringConfig.builder(); + private Double gossipThreshold = 0.0; + private Double publishThreshold = 0.0; + private Double graylistThreshold = 0.0; + private Double acceptPXThreshold = 0.0; + private Double opportunisticGraftThreshold = 0.0; + + private Builder() {} + + public GossipScoringConfig build() { + final Map topicConfigs = + topicsScoringBuilder.build().getTopicConfigs(); + return new GossipScoringConfig( + peerScoringConfigBuilder.build(), + defaultTopicScoringConfigBuilder.build(), + topicConfigs, + gossipThreshold, + publishThreshold, + graylistThreshold, + acceptPXThreshold, + opportunisticGraftThreshold); + } + + public Builder peerScoring(final Consumer consumer) { + consumer.accept(peerScoringConfigBuilder); + return this; + } + + public Builder topicScoring( + final String topic, final Consumer consumer) { + topicsScoringBuilder.topicScoring(topic, consumer); + return this; + } + + public Builder topicScoring(final Consumer consumer) { + consumer.accept(topicsScoringBuilder); + return this; + } + + public Builder defaultTopicScoring(final Consumer consumer) { + consumer.accept(defaultTopicScoringConfigBuilder); + return this; + } + + public Builder gossipThreshold(final Double gossipThreshold) { + checkNotNull(gossipThreshold); + this.gossipThreshold = gossipThreshold; + return this; + } + + public Builder publishThreshold(final Double publishThreshold) { + checkNotNull(publishThreshold); + this.publishThreshold = publishThreshold; + return this; + } + + public Builder graylistThreshold(final Double graylistThreshold) { + checkNotNull(graylistThreshold); + this.graylistThreshold = graylistThreshold; + return this; + } + + public Builder acceptPXThreshold(final Double acceptPXThreshold) { + checkNotNull(acceptPXThreshold); + this.acceptPXThreshold = acceptPXThreshold; + return this; + } + + public Builder opportunisticGraftThreshold(final Double opportunisticGraftThreshold) { + checkNotNull(opportunisticGraftThreshold); + this.opportunisticGraftThreshold = opportunisticGraftThreshold; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/gossip/config/GossipTopicScoringConfig.java b/src/org/minima/system/network/base/gossip/config/GossipTopicScoringConfig.java new file mode 100644 index 000000000..9593f575f --- /dev/null +++ b/src/org/minima/system/network/base/gossip/config/GossipTopicScoringConfig.java @@ -0,0 +1,293 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip.config; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.Duration; + +/** Scoring config for a single topic */ +public class GossipTopicScoringConfig { + private final double topicWeight; + private final double timeInMeshWeight; + private final Duration timeInMeshQuantum; + private final double timeInMeshCap; + private final double firstMessageDeliveriesWeight; + private final double firstMessageDeliveriesDecay; + private final double firstMessageDeliveriesCap; + private final double meshMessageDeliveriesWeight; + private final double meshMessageDeliveriesDecay; + private final double meshMessageDeliveriesThreshold; + private final double meshMessageDeliveriesCap; + private final Duration meshMessageDeliveriesActivation; + private final Duration meshMessageDeliveryWindow; + private final double meshFailurePenaltyWeight; + private final double meshFailurePenaltyDecay; + private final double invalidMessageDeliveriesWeight; + private final double invalidMessageDeliveriesDecay; + + private GossipTopicScoringConfig( + final double topicWeight, + final double timeInMeshWeight, + final Duration timeInMeshQuantum, + final double timeInMeshCap, + final double firstMessageDeliveriesWeight, + final double firstMessageDeliveriesDecay, + final double firstMessageDeliveriesCap, + final double meshMessageDeliveriesWeight, + final double meshMessageDeliveriesDecay, + final double meshMessageDeliveriesThreshold, + final double meshMessageDeliveriesCap, + final Duration meshMessageDeliveriesActivation, + final Duration meshMessageDeliveryWindow, + final double meshFailurePenaltyWeight, + final double meshFailurePenaltyDecay, + final double invalidMessageDeliveriesWeight, + final double invalidMessageDeliveriesDecay) { + this.topicWeight = topicWeight; + this.timeInMeshWeight = timeInMeshWeight; + this.timeInMeshQuantum = timeInMeshQuantum; + this.timeInMeshCap = timeInMeshCap; + this.firstMessageDeliveriesWeight = firstMessageDeliveriesWeight; + this.firstMessageDeliveriesDecay = firstMessageDeliveriesDecay; + this.firstMessageDeliveriesCap = firstMessageDeliveriesCap; + this.meshMessageDeliveriesWeight = meshMessageDeliveriesWeight; + this.meshMessageDeliveriesDecay = meshMessageDeliveriesDecay; + this.meshMessageDeliveriesThreshold = meshMessageDeliveriesThreshold; + this.meshMessageDeliveriesCap = meshMessageDeliveriesCap; + this.meshMessageDeliveriesActivation = meshMessageDeliveriesActivation; + this.meshMessageDeliveryWindow = meshMessageDeliveryWindow; + this.meshFailurePenaltyWeight = meshFailurePenaltyWeight; + this.meshFailurePenaltyDecay = meshFailurePenaltyDecay; + this.invalidMessageDeliveriesWeight = invalidMessageDeliveriesWeight; + this.invalidMessageDeliveriesDecay = invalidMessageDeliveriesDecay; + } + + public static Builder builder() { + return new Builder(); + } + + public double getTopicWeight() { + return topicWeight; + } + + public double getTimeInMeshWeight() { + return timeInMeshWeight; + } + + public Duration getTimeInMeshQuantum() { + return timeInMeshQuantum; + } + + public double getTimeInMeshCap() { + return timeInMeshCap; + } + + public double getFirstMessageDeliveriesWeight() { + return firstMessageDeliveriesWeight; + } + + public double getFirstMessageDeliveriesDecay() { + return firstMessageDeliveriesDecay; + } + + public double getFirstMessageDeliveriesCap() { + return firstMessageDeliveriesCap; + } + + public double getMeshMessageDeliveriesWeight() { + return meshMessageDeliveriesWeight; + } + + public double getMeshMessageDeliveriesDecay() { + return meshMessageDeliveriesDecay; + } + + public double getMeshMessageDeliveriesThreshold() { + return meshMessageDeliveriesThreshold; + } + + public double getMeshMessageDeliveriesCap() { + return meshMessageDeliveriesCap; + } + + public Duration getMeshMessageDeliveriesActivation() { + return meshMessageDeliveriesActivation; + } + + public Duration getMeshMessageDeliveryWindow() { + return meshMessageDeliveryWindow; + } + + public double getMeshFailurePenaltyWeight() { + return meshFailurePenaltyWeight; + } + + public double getMeshFailurePenaltyDecay() { + return meshFailurePenaltyDecay; + } + + public double getInvalidMessageDeliveriesWeight() { + return invalidMessageDeliveriesWeight; + } + + public double getInvalidMessageDeliveriesDecay() { + return invalidMessageDeliveriesDecay; + } + + public static class Builder { + private Double topicWeight = 0.0; + private Double timeInMeshWeight = 0.0; + private Duration timeInMeshQuantum = Duration.ofSeconds(1); + private Double timeInMeshCap = 0.0; + private Double firstMessageDeliveriesWeight = 0.0; + private Double firstMessageDeliveriesDecay = 0.0; + private Double firstMessageDeliveriesCap = 0.0; + private Double meshMessageDeliveriesWeight = 0.0; + private Double meshMessageDeliveriesDecay = 0.0; + private Double meshMessageDeliveriesThreshold = 0.0; + private Double meshMessageDeliveriesCap = 0.0; + private Duration meshMessageDeliveriesActivation = Duration.ofMinutes(1); + private Duration meshMessageDeliveryWindow = Duration.ofMillis(10); + private Double meshFailurePenaltyWeight = 0.0; + private Double meshFailurePenaltyDecay = 0.0; + private Double invalidMessageDeliveriesWeight = 0.0; + private Double invalidMessageDeliveriesDecay = 0.0; + + private Builder() {} + + public GossipTopicScoringConfig build() { + return new GossipTopicScoringConfig( + topicWeight, + timeInMeshWeight, + timeInMeshQuantum, + timeInMeshCap, + firstMessageDeliveriesWeight, + firstMessageDeliveriesDecay, + firstMessageDeliveriesCap, + meshMessageDeliveriesWeight, + meshMessageDeliveriesDecay, + meshMessageDeliveriesThreshold, + meshMessageDeliveriesCap, + meshMessageDeliveriesActivation, + meshMessageDeliveryWindow, + meshFailurePenaltyWeight, + meshFailurePenaltyDecay, + invalidMessageDeliveriesWeight, + invalidMessageDeliveriesDecay); + } + + public Builder topicWeight(final Double topicWeight) { + checkNotNull(topicWeight); + this.topicWeight = topicWeight; + return this; + } + + public Builder timeInMeshWeight(final Double timeInMeshWeight) { + checkNotNull(timeInMeshWeight); + this.timeInMeshWeight = timeInMeshWeight; + return this; + } + + public Builder timeInMeshQuantum(final Duration timeInMeshQuantum) { + checkNotNull(timeInMeshQuantum); + this.timeInMeshQuantum = timeInMeshQuantum; + return this; + } + + public Builder timeInMeshCap(final Double timeInMeshCap) { + checkNotNull(timeInMeshCap); + this.timeInMeshCap = timeInMeshCap; + return this; + } + + public Builder firstMessageDeliveriesWeight(final Double firstMessageDeliveriesWeight) { + checkNotNull(firstMessageDeliveriesWeight); + this.firstMessageDeliveriesWeight = firstMessageDeliveriesWeight; + return this; + } + + public Builder firstMessageDeliveriesDecay(final Double firstMessageDeliveriesDecay) { + checkNotNull(firstMessageDeliveriesDecay); + this.firstMessageDeliveriesDecay = firstMessageDeliveriesDecay; + return this; + } + + public Builder firstMessageDeliveriesCap(final Double firstMessageDeliveriesCap) { + checkNotNull(firstMessageDeliveriesCap); + this.firstMessageDeliveriesCap = firstMessageDeliveriesCap; + return this; + } + + public Builder meshMessageDeliveriesWeight(final Double meshMessageDeliveriesWeight) { + checkNotNull(meshMessageDeliveriesWeight); + this.meshMessageDeliveriesWeight = meshMessageDeliveriesWeight; + return this; + } + + public Builder meshMessageDeliveriesDecay(final Double meshMessageDeliveriesDecay) { + checkNotNull(meshMessageDeliveriesDecay); + this.meshMessageDeliveriesDecay = meshMessageDeliveriesDecay; + return this; + } + + public Builder meshMessageDeliveriesThreshold(final Double meshMessageDeliveriesThreshold) { + checkNotNull(meshMessageDeliveriesThreshold); + this.meshMessageDeliveriesThreshold = meshMessageDeliveriesThreshold; + return this; + } + + public Builder meshMessageDeliveriesCap(final Double meshMessageDeliveriesCap) { + checkNotNull(meshMessageDeliveriesCap); + this.meshMessageDeliveriesCap = meshMessageDeliveriesCap; + return this; + } + + public Builder meshMessageDeliveriesActivation(final Duration meshMessageDeliveriesActivation) { + checkNotNull(meshMessageDeliveriesActivation); + this.meshMessageDeliveriesActivation = meshMessageDeliveriesActivation; + return this; + } + + public Builder meshMessageDeliveryWindow(final Duration meshMessageDeliveryWindow) { + checkNotNull(meshMessageDeliveryWindow); + this.meshMessageDeliveryWindow = meshMessageDeliveryWindow; + return this; + } + + public Builder meshFailurePenaltyWeight(final Double meshFailurePenaltyWeight) { + checkNotNull(meshFailurePenaltyWeight); + this.meshFailurePenaltyWeight = meshFailurePenaltyWeight; + return this; + } + + public Builder meshFailurePenaltyDecay(final Double meshFailurePenaltyDecay) { + checkNotNull(meshFailurePenaltyDecay); + this.meshFailurePenaltyDecay = meshFailurePenaltyDecay; + return this; + } + + public Builder invalidMessageDeliveriesWeight(final Double invalidMessageDeliveriesWeight) { + checkNotNull(invalidMessageDeliveriesWeight); + this.invalidMessageDeliveriesWeight = invalidMessageDeliveriesWeight; + return this; + } + + public Builder invalidMessageDeliveriesDecay(final Double invalidMessageDeliveriesDecay) { + checkNotNull(invalidMessageDeliveriesDecay); + this.invalidMessageDeliveriesDecay = invalidMessageDeliveriesDecay; + return this; + } + } +} diff --git a/src/org/minima/system/network/base/gossip/config/GossipTopicsScoringConfig.java b/src/org/minima/system/network/base/gossip/config/GossipTopicsScoringConfig.java new file mode 100644 index 000000000..5235a69f4 --- /dev/null +++ b/src/org/minima/system/network/base/gossip/config/GossipTopicsScoringConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.gossip.config; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** Scoring config for a collection a set of topics */ +public class GossipTopicsScoringConfig { + private final Map topicConfigs; + + private GossipTopicsScoringConfig(final Map topicConfigs) { + this.topicConfigs = topicConfigs; + } + + public boolean isEmpty() { + return topicConfigs.isEmpty(); + } + + public static Builder builder() { + return new Builder(); + } + + public Map getTopicConfigs() { + return topicConfigs; + } + + public static class Builder { + private final Map topicScoringConfigBuilders = + new HashMap<>(); + + public GossipTopicsScoringConfig build() { + final Map topicConfig = + topicScoringConfigBuilders.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().build())); + return new GossipTopicsScoringConfig(topicConfig); + } + + public Builder topicScoring( + final String topic, final Consumer consumer) { + GossipTopicScoringConfig.Builder builder = + topicScoringConfigBuilders.computeIfAbsent( + topic, __ -> GossipTopicScoringConfig.builder()); + consumer.accept(builder); + return this; + } + + public Builder clear() { + topicScoringConfigBuilders.clear(); + return this; + } + } +} diff --git a/src/org/minima/system/network/base/libp2p/PrivateKeyGenerator.java b/src/org/minima/system/network/base/libp2p/PrivateKeyGenerator.java new file mode 100644 index 000000000..25c6f216e --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/PrivateKeyGenerator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p; + +import io.libp2p.core.crypto.KEY_TYPE; +import io.libp2p.core.crypto.KeyKt; +import io.libp2p.core.crypto.PrivKey; + +public class PrivateKeyGenerator { + public static PrivKey generate() { + return KeyKt.generateKeyPair(KEY_TYPE.SECP256K1).component1(); + } +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/GossipHandler.java b/src/org/minima/system/network/base/libp2p/gossip/GossipHandler.java new file mode 100644 index 000000000..8fb06a972 --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/GossipHandler.java @@ -0,0 +1,119 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +import io.libp2p.core.pubsub.MessageApi; +import io.libp2p.core.pubsub.PubsubPublisherApi; +import io.libp2p.core.pubsub.Topic; +import io.libp2p.core.pubsub.ValidationResult; +import io.libp2p.pubsub.PubsubMessage; +import io.netty.buffer.Unpooled; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +//import org.apache.tuweni.bytes.Bytes; +//import org.hyperledger.besu.plugin.services.MetricsSystem; +//import org.hyperledger.besu.plugin.services.metrics.Counter; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; +//import tech.pegasys.teku.infrastructure.collections.LimitedSet; +//import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +//import tech.pegasys.teku.networking.p2p.gossip.TopicHandler; +import org.minima.system.network.base.LimitedSet; +import org.minima.system.network.base.SafeFuture; +import org.minima.system.network.base.gossip.TopicHandler; +import org.minima.system.network.base.metrics.Counter; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.metrics.TekuMetricCategory; + +public class GossipHandler implements Function> { + private static final Logger LOG = LogManager.getLogger(); + + private static final int GOSSIP_MAX_SIZE = 1024; + + private static final SafeFuture VALIDATION_FAILED = + SafeFuture.completedFuture(ValidationResult.Invalid); + private static final SafeFuture VALIDATION_IGNORED = + SafeFuture.completedFuture(ValidationResult.Ignore); + + private static final int MAX_SENT_MESSAGES = 2048; + + private final Topic topic; + private final PubsubPublisherApi publisher; + private final TopicHandler handler; + private final Set processedMessages = LimitedSet.create(MAX_SENT_MESSAGES); + private final Counter messageCounter; + + public GossipHandler( + final MetricsSystem metricsSystem, + final Topic topic, + final PubsubPublisherApi publisher, + final TopicHandler handler) { + this.topic = topic; + this.publisher = publisher; + this.handler = handler; + this.messageCounter = + metricsSystem + .createLabelledCounter( + TekuMetricCategory.LIBP2P, + "gossip_messages_total", + "Total number of gossip messages received (avoid libp2p deduplication)", + "topic") + .labels(topic.getTopic()); + } + + @Override + public SafeFuture apply(final MessageApi message) { + messageCounter.inc(); + final int messageSize = message.getData().readableBytes(); + if (messageSize > GOSSIP_MAX_SIZE) { + LOG.trace( + "Rejecting gossip message of length {} which exceeds maximum size of {}", + messageSize, + GOSSIP_MAX_SIZE); + return VALIDATION_FAILED; + } + byte[] arr = new byte[message.getData().readableBytes()]; + message.getData().slice().readBytes(arr); + byte[] bytes = arr; // Bytes.wrap(arr); + if (!processedMessages.add(bytes)) { + // We've already seen this message, skip processing + LOG.trace("Ignoring duplicate message for topic {}: {} bytes", topic, bytes.length); + return VALIDATION_IGNORED; + } + LOG.trace("Received message for topic {}: {} bytes", topic, bytes.length); + + PubsubMessage pubsubMessage = message.getOriginalMessage(); + if (!(pubsubMessage instanceof PreparedPubsubMessage)) { + throw new IllegalArgumentException( + "Don't know this PubsubMessage implementation: " + pubsubMessage.getClass()); + } + PreparedPubsubMessage gossipPubsubMessage = (PreparedPubsubMessage) pubsubMessage; + return handler.handleMessage(gossipPubsubMessage.getPreparedMessage()); + } + + public void gossip(byte[] bytes) { + if (!processedMessages.add(bytes)) { + // We've already gossiped this data + return; + } + + LOG.trace("Gossiping {}: {} bytes", topic, bytes.length); + SafeFuture.of(publisher.publish(Unpooled.wrappedBuffer(bytes), topic)) + .finish( + () -> LOG.trace("Successfully gossiped message on {}", topic), + err -> LOG.debug("Failed to gossip message on " + topic, err)); + } +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/GossipTopicFilter.java b/src/org/minima/system/network/base/libp2p/gossip/GossipTopicFilter.java new file mode 100644 index 000000000..10ca351d6 --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/GossipTopicFilter.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +@FunctionalInterface +public interface GossipTopicFilter { + boolean isRelevantTopic(String topic); +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/GossipWireValidator.java b/src/org/minima/system/network/base/libp2p/gossip/GossipWireValidator.java new file mode 100644 index 000000000..111bc8bee --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/GossipWireValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +import io.libp2p.pubsub.PubsubMessage; +import io.libp2p.pubsub.PubsubRouterMessageValidator; +import org.jetbrains.annotations.NotNull; +import pubsub.pb.Rpc; + +/** + * Validates Gossip messages at the level of Protobuf structures Rejects messages with prohibited + * Gossip fields: {@code from, signature, seqno} + */ +public class GossipWireValidator implements PubsubRouterMessageValidator { + + public static class InvalidGossipMessageException extends IllegalArgumentException { + public InvalidGossipMessageException(String s) { + super(s); + } + } + + @Override + public void validate(@NotNull PubsubMessage pubsubMessage) { + Rpc.Message message = pubsubMessage.getProtobufMessage(); + if (message.hasFrom()) { + throw new InvalidGossipMessageException("The message has prohibited 'from' field: "); + } + if (message.hasSignature()) { + throw new InvalidGossipMessageException("The message has prohibited 'signature' field"); + } + if (message.hasSeqno()) { + throw new InvalidGossipMessageException("The message has prohibited 'seqno' field"); + } + } +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/LibP2PGossipNetwork.java b/src/org/minima/system/network/base/libp2p/gossip/LibP2PGossipNetwork.java new file mode 100644 index 000000000..28959f3e6 --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/LibP2PGossipNetwork.java @@ -0,0 +1,244 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +import com.google.common.base.Preconditions; +import io.libp2p.core.PeerId; +import io.libp2p.core.pubsub.PubsubApi; +import io.libp2p.core.pubsub.PubsubApiKt; +import io.libp2p.core.pubsub.PubsubPublisherApi; +import io.libp2p.core.pubsub.PubsubSubscription; +import io.libp2p.core.pubsub.Topic; +import io.libp2p.core.pubsub.ValidationResult; +import io.libp2p.pubsub.FastIdSeenCache; +import io.libp2p.pubsub.MaxCountTopicSubscriptionFilter; +import io.libp2p.pubsub.PubsubProtocol; +import io.libp2p.pubsub.PubsubRouterMessageValidator; +import io.libp2p.pubsub.SeenCache; +import io.libp2p.pubsub.TTLSeenCache; +import io.libp2p.pubsub.TopicSubscriptionFilter; +import io.libp2p.pubsub.gossip.Gossip; +import io.libp2p.pubsub.gossip.GossipParams; +import io.libp2p.pubsub.gossip.GossipRouter; +import io.libp2p.pubsub.gossip.GossipScoreParams; +import io.libp2p.pubsub.gossip.GossipTopicScoreParams; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import kotlin.jvm.functions.Function0; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.jetbrains.annotations.NotNull; +//import org.minima.system.network.base.LibP2PNodeId; +import org.minima.system.network.base.LibP2PParamsFactory; +import org.minima.system.network.base.SafeFuture; +// import org.apache.tuweni.bytes.Bytes; +// import org.apache.tuweni.crypto.Hash; +// import org.hyperledger.besu.plugin.services.MetricsSystem; +// import org.jetbrains.annotations.NotNull; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.gossip.GossipNetwork; +// import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage; +// import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; +// import tech.pegasys.teku.networking.p2p.gossip.TopicChannel; +// import tech.pegasys.teku.networking.p2p.gossip.TopicHandler; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig; +// import tech.pegasys.teku.networking.p2p.gossip.config.GossipTopicsScoringConfig; +// import tech.pegasys.teku.networking.p2p.libp2p.LibP2PNodeId; +// import tech.pegasys.teku.networking.p2p.libp2p.config.LibP2PParamsFactory; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +import org.minima.system.network.base.gossip.GossipNetwork; +import org.minima.system.network.base.gossip.PreparedGossipMessage; +import org.minima.system.network.base.gossip.PreparedGossipMessageFactory; +import org.minima.system.network.base.gossip.TopicChannel; +import org.minima.system.network.base.gossip.TopicHandler; +import org.minima.system.network.base.gossip.config.GossipConfig; +import org.minima.system.network.base.gossip.config.GossipTopicsScoringConfig; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.peer.LibP2PNodeId; +import org.minima.system.network.base.peer.NodeId; +import org.minima.utils.Crypto; + +public class LibP2PGossipNetwork implements GossipNetwork { + + private static final Logger LOG = LogManager.getLogger(); + + private static final PubsubRouterMessageValidator STRICT_FIELDS_VALIDATOR = + new GossipWireValidator(); + private static final Function0 NULL_SEQNO_GENERATOR = () -> null; + + private final MetricsSystem metricsSystem; + private final Gossip gossip; + private final PubsubPublisherApi publisher; + private final TopicHandlers topicHandlers; + + public static LibP2PGossipNetwork create( + MetricsSystem metricsSystem, + GossipConfig gossipConfig, + PreparedGossipMessageFactory defaultMessageFactory, + GossipTopicFilter gossipTopicFilter, + boolean logWireGossip) { + + TopicHandlers topicHandlers = new TopicHandlers(); + Gossip gossip = + createGossip( + gossipConfig, logWireGossip, defaultMessageFactory, gossipTopicFilter, topicHandlers); + PubsubPublisherApi publisher = gossip.createPublisher(null, NULL_SEQNO_GENERATOR); + + return new LibP2PGossipNetwork(metricsSystem, gossip, publisher, topicHandlers); + } + + private static Gossip createGossip( + GossipConfig gossipConfig, + boolean gossipLogsEnabled, + PreparedGossipMessageFactory defaultMessageFactory, + GossipTopicFilter gossipTopicFilter, + TopicHandlers topicHandlers) { + final GossipParams gossipParams = LibP2PParamsFactory.createGossipParams(gossipConfig); + final GossipScoreParams scoreParams = + LibP2PParamsFactory.createGossipScoreParams(gossipConfig.getScoringConfig()); + + final TopicSubscriptionFilter subscriptionFilter = + new MaxCountTopicSubscriptionFilter(100, 200, gossipTopicFilter::isRelevantTopic); + GossipRouter router = + new GossipRouter( + gossipParams, scoreParams, PubsubProtocol.Gossip_V_1_1, subscriptionFilter) { + + final SeenCache> seenCache = + new TTLSeenCache<>( + new FastIdSeenCache<>( + msg -> + (new Crypto()).hashSHA2(msg.getProtobufMessage().getData().toByteArray())), + // Bytes.wrap( + // Hash.sha2_256(msg.getProtobufMessage().getData().toByteArray())) + // ), + gossipParams.getSeenTTL(), + getCurTimeMillis()); + + @NotNull + @Override + protected SeenCache> getSeenMessages() { + return seenCache; + } + }; + + router.setMessageFactory( + msg -> { + Preconditions.checkArgument( + msg.getTopicIDsCount() == 1, + "Unexpected number of topics for a single message: " + msg.getTopicIDsCount()); + String topic = msg.getTopicIDs(0); + byte[] payload = msg.getData().toByteArray(); + + PreparedGossipMessage preparedMessage = + topicHandlers + .getHandlerForTopic(topic) + .map(handler -> handler.prepareMessage(payload)) + .orElse(defaultMessageFactory.create(topic, payload)); + + return new PreparedPubsubMessage(msg, preparedMessage); + }); + router.setMessageValidator(STRICT_FIELDS_VALIDATOR); + + ChannelHandler debugHandler = + gossipLogsEnabled ? new LoggingHandler("wire.gossip", LogLevel.DEBUG) : null; + PubsubApi pubsubApi = PubsubApiKt.createPubsubApi(router); + + return new Gossip(router, pubsubApi, debugHandler); + } + + public LibP2PGossipNetwork( + MetricsSystem metricsSystem, + Gossip gossip, + PubsubPublisherApi publisher, + TopicHandlers topicHandlers) { + this.metricsSystem = metricsSystem; + this.gossip = gossip; + this.publisher = publisher; + this.topicHandlers = topicHandlers; + } + + @Override + public SafeFuture gossip(final String topic, final Bytes data) { + return SafeFuture.of( + publisher.publish(Unpooled.wrappedBuffer(data.toArrayUnsafe()), new Topic(topic))); + } + + @Override + public TopicChannel subscribe(final String topic, final TopicHandler topicHandler) { + LOG.trace("Subscribe to topic: {}", topic); + topicHandlers.add(topic, topicHandler); + final Topic libP2PTopic = new Topic(topic); + final GossipHandler gossipHandler = + new GossipHandler(metricsSystem, libP2PTopic, publisher, topicHandler); + PubsubSubscription subscription = gossip.subscribe(gossipHandler, libP2PTopic); + return new LibP2PTopicChannel(gossipHandler, subscription); + } + + @Override + public Map> getSubscribersByTopic() { + Map> peerTopics = gossip.getPeerTopics().join(); + final Map> result = new HashMap<>(); + for (Map.Entry> peerTopic : peerTopics.entrySet()) { + final LibP2PNodeId nodeId = new LibP2PNodeId(peerTopic.getKey()); + peerTopic + .getValue() + .forEach( + topic -> result.computeIfAbsent(topic.getTopic(), __ -> new HashSet<>()).add(nodeId)); + } + return result; + } + + @Override + public void updateGossipTopicScoring(final GossipTopicsScoringConfig config) { + if (config.isEmpty()) { + return; + } + + final Map params = + config.getTopicConfigs().entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + e -> LibP2PParamsFactory.createTopicScoreParams(e.getValue()))); + gossip.updateTopicScoreParams(params); + } + + public Gossip getGossip() { + return gossip; + } + + private static class TopicHandlers { + + private final Map topicToHandlerMap = new ConcurrentHashMap<>(); + + public void add(String topic, TopicHandler handler) { + topicToHandlerMap.put(topic, handler); + } + + public Optional getHandlerForTopic(String topic) { + return Optional.ofNullable(topicToHandlerMap.get(topic)); + } + } +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/LibP2PTopicChannel.java b/src/org/minima/system/network/base/libp2p/gossip/LibP2PTopicChannel.java new file mode 100644 index 000000000..308c77b05 --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/LibP2PTopicChannel.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +import io.libp2p.core.pubsub.PubsubSubscription; +import java.util.concurrent.atomic.AtomicBoolean; +//import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.networking.p2p.gossip.TopicChannel; +import org.minima.system.network.base.gossip.TopicChannel; + +public class LibP2PTopicChannel implements TopicChannel { + private final GossipHandler topicHandler; + private final PubsubSubscription subscription; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public LibP2PTopicChannel( + final GossipHandler topicHandler, final PubsubSubscription subscription) { + this.topicHandler = topicHandler; + this.subscription = subscription; + } + + @Override + public void gossip(final byte[] data) { + if (closed.get()) { + return; + } + topicHandler.gossip(data); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + subscription.unsubscribe(); + } + } +} diff --git a/src/org/minima/system/network/base/libp2p/gossip/PreparedPubsubMessage.java b/src/org/minima/system/network/base/libp2p/gossip/PreparedPubsubMessage.java new file mode 100644 index 000000000..6041c222a --- /dev/null +++ b/src/org/minima/system/network/base/libp2p/gossip/PreparedPubsubMessage.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.libp2p.gossip; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import io.libp2p.core.pubsub.MessageApi; +import io.libp2p.etc.types.WBytes; +import io.libp2p.pubsub.AbstractPubsubMessage; +import io.libp2p.pubsub.PubsubMessage; +import io.libp2p.pubsub.gossip.GossipRouter; +import org.jetbrains.annotations.NotNull; +import pubsub.pb.Rpc.Message; +import org.minima.system.network.base.gossip.PreparedGossipMessage; + +/** + * The bridge class between outer Libp2p {@link PubsubMessage} and inner {@link + * PreparedGossipMessage} + * + *

The {@link PreparedGossipMessage} instance created during {@link + * GossipRouter#getMessageFactory()} invocation can later be accessed when the gossip message is + * handled: {@link MessageApi#getOriginalMessage()} + */ +public class PreparedPubsubMessage extends AbstractPubsubMessage { + + private final Message protobufMessage; + private final PreparedGossipMessage preparedMessage; + private final Supplier cachedMessageId; + + public PreparedPubsubMessage(Message protobufMessage, PreparedGossipMessage preparedMessage) { + this.protobufMessage = protobufMessage; + this.preparedMessage = preparedMessage; + cachedMessageId = + Suppliers.memoize(() -> new WBytes(preparedMessage.getMessageId())); + } + + @NotNull + @Override + public WBytes getMessageId() { + return cachedMessageId.get(); + } + + @NotNull + @Override + public Message getProtobufMessage() { + return protobufMessage; + } + + public PreparedGossipMessage getPreparedMessage() { + return preparedMessage; + } +} diff --git a/src/org/minima/system/network/base/metrics/Counter.java b/src/org/minima/system/network/base/metrics/Counter.java new file mode 100644 index 000000000..cf016beda --- /dev/null +++ b/src/org/minima/system/network/base/metrics/Counter.java @@ -0,0 +1,35 @@ + +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.minima.system.network.base.metrics; + +/** + * A counter is a metric to track counts of events or running totals etc. The value of the counter + * can only increase. + */ +public interface Counter { + + /** Increment the counter by 1. */ + void inc(); + + /** + * Increment the counter by a specified amount. + * + * @param amount The amount to increment the counter by. Must be greater than or equal to 0. + */ + void inc(long amount); +} + diff --git a/src/org/minima/system/network/base/metrics/LabelledMetric.java b/src/org/minima/system/network/base/metrics/LabelledMetric.java new file mode 100644 index 000000000..6dde9b139 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/LabelledMetric.java @@ -0,0 +1,33 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +/** + * A metric with labels associated. Values for the associated labels can be provided to access the + * underlying metric. + * + * @param The type of metric the labels are applied to. + */ +public interface LabelledMetric { + + /** + * Returns a metric tagged with the specified label values. + * + * @param labels An array of label values in the same order as the labels when creating this + * metric. The number of values provided must match the number of labels. + * @return A metric tagged with the specified labels. + */ + T labels(String... labels); +} diff --git a/src/org/minima/system/network/base/metrics/MetricCategory.java b/src/org/minima/system/network/base/metrics/MetricCategory.java new file mode 100644 index 000000000..977cba2bb --- /dev/null +++ b/src/org/minima/system/network/base/metrics/MetricCategory.java @@ -0,0 +1,45 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +import java.util.Optional; + +/** + * A MetricCategory is used to group related metrics. Every metric belongs to one and only one + * MetricCategory. + * + *

Categories must be registered with the {@link MetricCategoryRegistry} during plugin + * initialisation. + */ +public interface MetricCategory { + + /** + * Gets the name of this MetricCategory. + * + * @return The name of this MetricCategory. + */ + String getName(); + + /** + * Gets the application-specific MetricCategory prefix. An empty Optional may be returned if this + * category is not application specific. + * + *

The prefix, if present, is prepended to the category name when creating a single combined + * name for metrics. + * + * @return An optional application prefix. + */ + Optional getApplicationPrefix(); +} diff --git a/src/org/minima/system/network/base/metrics/MetricsSystem.java b/src/org/minima/system/network/base/metrics/MetricsSystem.java new file mode 100644 index 000000000..4537c747b --- /dev/null +++ b/src/org/minima/system/network/base/metrics/MetricsSystem.java @@ -0,0 +1,122 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +//package org.hyperledger.besu.plugin.services; +package org.minima.system.network.base.metrics; + +// import org.hyperledger.besu.plugin.services.metrics.Counter; +// import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +// import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +// import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + +/** An interface for creating various Metrics components. */ +public interface MetricsSystem { + + /** + * Creates a Counter. + * + * @param category The {@link MetricCategory} this counter is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @return The created Counter instance. + */ + default Counter createCounter( + final MetricCategory category, final String name, final String help) { + return createLabelledCounter(category, name, help, new String[0]).labels(); + } + + /** + * Creates a Counter with assigned labels. + * + * @param category The {@link MetricCategory} this counter is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @param labelNames An array of labels to assign to the Counter. + * @return The created LabelledMetric instance. + */ + LabelledMetric createLabelledCounter( + MetricCategory category, String name, String help, String... labelNames); + + /** + * Creates a Timer. + * + * @param category The {@link MetricCategory} this timer is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @return The created Timer instance. + */ + default OperationTimer createTimer( + final MetricCategory category, final String name, final String help) { + return createLabelledTimer(category, name, help, new String[0]).labels(); + } + + /** + * Creates a Timer with assigned labels. + * + * @param category The {@link MetricCategory} this timer is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @param labelNames An array of labels to assign to the Timer. + * @return The created LabelledMetric instance. + */ + LabelledMetric createLabelledTimer( + MetricCategory category, String name, String help, String... labelNames); + + /** + * Creates a gauge for displaying double vales. A gauge is a metric to report the current value. + * The metric value may go up or down. + * + * @param category The {@link MetricCategory} this gauge is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @param valueSupplier A supplier for the double value to be presented. + */ + void createGauge(MetricCategory category, String name, String help, DoubleSupplier valueSupplier); + + /** + * Creates a gauge for displaying integer values. + * + * @param category The {@link MetricCategory} this gauge is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @param valueSupplier A supplier for the integer value to be presented. + */ + default void createIntegerGauge( + final MetricCategory category, + final String name, + final String help, + final IntSupplier valueSupplier) { + createGauge(category, name, help, () -> (double) valueSupplier.getAsInt()); + } + + /** + * Creates a gauge for displaying long values. + * + * @param category The {@link MetricCategory} this gauge is assigned to. + * @param name A name for this metric. + * @param help A human readable description of the metric. + * @param valueSupplier A supplier for the long value to be presented. + */ + default void createLongGauge( + final MetricCategory category, + final String name, + final String help, + final LongSupplier valueSupplier) { + createGauge(category, name, help, () -> (double) valueSupplier.getAsLong()); + } +} diff --git a/src/org/minima/system/network/base/metrics/NoOpCounter.java b/src/org/minima/system/network/base/metrics/NoOpCounter.java new file mode 100644 index 000000000..cc7f34fc7 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/NoOpCounter.java @@ -0,0 +1,24 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +class NoOpCounter implements Counter { + + @Override + public void inc() {} + + @Override + public void inc(final long amount) {} +} \ No newline at end of file diff --git a/src/org/minima/system/network/base/metrics/NoOpMetricsSystem.java b/src/org/minima/system/network/base/metrics/NoOpMetricsSystem.java new file mode 100644 index 000000000..091532710 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/NoOpMetricsSystem.java @@ -0,0 +1,126 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +//import org.hyperledger.besu.metrics.ObservableMetricsSystem; +//import org.hyperledger.besu.metrics.Observation; +//import org.hyperledger.besu.plugin.services.metrics.Counter; +//import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +//import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +//import org.hyperledger.besu.plugin.services.metrics.OperationTimer; + +import java.util.Collections; +import java.util.Set; +import java.util.function.DoubleSupplier; +import java.util.stream.Stream; + +import com.google.common.base.Preconditions; + +public class NoOpMetricsSystem implements ObservableMetricsSystem { + + public static final Counter NO_OP_COUNTER = new NoOpCounter(); + private static final OperationTimer.TimingContext NO_OP_TIMING_CONTEXT = () -> 0; + public static final OperationTimer NO_OP_OPERATION_TIMER = () -> NO_OP_TIMING_CONTEXT; + + public static final LabelledMetric NO_OP_LABELLED_1_COUNTER = + new LabelCountingNoOpMetric<>(1, NO_OP_COUNTER); + public static final LabelledMetric NO_OP_LABELLED_2_COUNTER = + new LabelCountingNoOpMetric<>(2, NO_OP_COUNTER); + public static final LabelledMetric NO_OP_LABELLED_3_COUNTER = + new LabelCountingNoOpMetric<>(3, NO_OP_COUNTER); + public static final LabelledMetric NO_OP_LABELLED_1_OPERATION_TIMER = + new LabelCountingNoOpMetric<>(1, NO_OP_OPERATION_TIMER); + + @Override + public LabelledMetric createLabelledCounter( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + return getCounterLabelledMetric(labelNames.length); + } + + public static LabelledMetric getCounterLabelledMetric(final int labelCount) { + switch (labelCount) { + case 1: + return NO_OP_LABELLED_1_COUNTER; + case 2: + return NO_OP_LABELLED_2_COUNTER; + case 3: + return NO_OP_LABELLED_3_COUNTER; + default: + return new LabelCountingNoOpMetric<>(labelCount, NO_OP_COUNTER); + } + } + + @Override + public LabelledMetric createLabelledTimer( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + return getOperationTimerLabelledMetric(labelNames.length); + } + + public static LabelledMetric getOperationTimerLabelledMetric( + final int labelCount) { + if (labelCount == 1) { + return NO_OP_LABELLED_1_OPERATION_TIMER; + } else { + return new LabelCountingNoOpMetric<>(labelCount, NO_OP_OPERATION_TIMER); + } + } + + @Override + public void createGauge( + final MetricCategory category, + final String name, + final String help, + final DoubleSupplier valueSupplier) {} + + @Override + public Stream streamObservations(final MetricCategory category) { + return Stream.empty(); + } + + @Override + public Stream streamObservations() { + return Stream.empty(); + } + + @Override + public Set getEnabledCategories() { + return Collections.emptySet(); + } + + public static class LabelCountingNoOpMetric implements LabelledMetric { + + final int labelCount; + final T fakeMetric; + + LabelCountingNoOpMetric(final int labelCount, final T fakeMetric) { + this.labelCount = labelCount; + this.fakeMetric = fakeMetric; + } + + @Override + public T labels(final String... labels) { + Preconditions.checkArgument( + labels.length == labelCount, + "The count of labels used must match the count of labels expected."); + return fakeMetric; + } + } +} \ No newline at end of file diff --git a/src/org/minima/system/network/base/metrics/ObservableMetricsSystem.java b/src/org/minima/system/network/base/metrics/ObservableMetricsSystem.java new file mode 100644 index 000000000..8f0a4366a --- /dev/null +++ b/src/org/minima/system/network/base/metrics/ObservableMetricsSystem.java @@ -0,0 +1,44 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.minima.system.network.base.metrics; + +import java.util.Set; +import java.util.stream.Stream; + +public interface ObservableMetricsSystem extends MetricsSystem { + + Stream streamObservations(MetricCategory category); + + Stream streamObservations(); + + /** + * Provides an immutable view into the metric categories enabled for metric collection. + * + * @return the set of enabled metric categories. + */ + Set getEnabledCategories(); + + /** + * Checks if a particular category of metrics is enabled. + * + * @param category the category to check + * @return true if the category is enabled, false otherwise + */ + default boolean isCategoryEnabled(final MetricCategory category) { + return getEnabledCategories().stream() + .anyMatch(metricCategory -> metricCategory.getName().equals(category.getName())); + } +} diff --git a/src/org/minima/system/network/base/metrics/Observation.java b/src/org/minima/system/network/base/metrics/Observation.java new file mode 100644 index 000000000..1d4d43279 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/Observation.java @@ -0,0 +1,84 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +import java.util.List; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class Observation { + private final MetricCategory category; + private final String metricName; + private final List labels; + private final Object value; + + public Observation( + final MetricCategory category, + final String metricName, + final Object value, + final List labels) { + this.category = category; + this.metricName = metricName; + this.value = value; + this.labels = labels; + } + + public MetricCategory getCategory() { + return category; + } + + public String getMetricName() { + return metricName; + } + + public List getLabels() { + return labels; + } + + public Object getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Observation that = (Observation) o; + return category == that.category + && Objects.equals(metricName, that.metricName) + && Objects.equals(labels, that.labels) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(category, metricName, labels, value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("category", category) + .add("metricName", metricName) + .add("labels", labels) + .add("value", value) + .toString(); + } +} \ No newline at end of file diff --git a/src/org/minima/system/network/base/metrics/OperationTimer.java b/src/org/minima/system/network/base/metrics/OperationTimer.java new file mode 100644 index 000000000..ae8111166 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/OperationTimer.java @@ -0,0 +1,45 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.minima.system.network.base.metrics; + +import java.io.Closeable; + +/** A timer metric that records duration of operations for metrics purposes. */ +public interface OperationTimer { + + /** + * Starts the timer. + * + * @return The produced TimingContext, which must be stopped or closed when the operation being + * timed has completed. + */ + TimingContext startTimer(); + + /** An interface for stopping the timer and returning elapsed time. */ + interface TimingContext extends Closeable { + + /** + * Stops the timer and returns the elapsed time. + * + * @return Elapsed time in seconds. + */ + double stopTimer(); + + @Override + default void close() { + stopTimer(); + } + } +} diff --git a/src/org/minima/system/network/base/metrics/TekuMetricCategory.java b/src/org/minima/system/network/base/metrics/TekuMetricCategory.java new file mode 100644 index 000000000..1b6f6db91 --- /dev/null +++ b/src/org/minima/system/network/base/metrics/TekuMetricCategory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.metrics; + +import java.util.Optional; + +public enum TekuMetricCategory implements MetricCategory { + BEACON("beacon"), + EVENTBUS("eventbus"), + EXECUTOR("executor"), + LIBP2P("libp2p"), + NETWORK("network"), + STORAGE("storage"), + STORAGE_HOT_DB("storage_hot"), + STORAGE_FINALIZED_DB("storage_finalized"), + REMOTE_VALIDATOR("remote_validator"), + VALIDATOR("validator"), + VALIDATOR_PERFORMANCE("validator_performance"); + + private final String name; + + TekuMetricCategory(final String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Optional getApplicationPrefix() { + return Optional.empty(); + } +} diff --git a/src/org/minima/system/network/base/peer/DisconnectReason.java b/src/org/minima/system/network/base/peer/DisconnectReason.java new file mode 100644 index 000000000..a5a46d32d --- /dev/null +++ b/src/org/minima/system/network/base/peer/DisconnectReason.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Optional; +import java.util.stream.Stream; +//import tech.pegasys.teku.infrastructure.unsigned.UInt64; +//import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.GoodbyeMessage; + +// from GoodbyeMessage.java +// public static final UInt64 REASON_CLIENT_SHUT_DOWN = UInt64.valueOf(1); +// public static final UInt64 REASON_IRRELEVANT_NETWORK = UInt64.valueOf(2); +// public static final UInt64 REASON_FAULT_ERROR = UInt64.valueOf(3); +// public static final UInt64 MIN_CUSTOM_REASON_CODE = UInt64.valueOf(128); + +// // Custom reasons +// public static final UInt64 REASON_UNABLE_TO_VERIFY_NETWORK = UInt64.valueOf(128); +// public static final UInt64 REASON_TOO_MANY_PEERS = UInt64.valueOf(129); +// public static final UInt64 REASON_RATE_LIMITING = UInt64.valueOf(130) +public enum DisconnectReason { + IRRELEVANT_NETWORK(2, true), + UNABLE_TO_VERIFY_NETWORK(128, true), + TOO_MANY_PEERS(129, false), + REMOTE_FAULT(3, false), + UNRESPONSIVE(3, false), + SHUTTING_DOWN(1, false), + RATE_LIMITING(130, false); + + private final long reasonCode; + private final boolean isPermanent; + + DisconnectReason(final long reasonCode, final boolean isPermanent) { + this.reasonCode = reasonCode; + this.isPermanent = isPermanent; + } + + public static Optional fromReasonCode(final long reasonCode) { + return Stream.of(values()) + .filter(reason -> reason.getReasonCode() == reasonCode) + .findAny(); + } + + public long getReasonCode() { + return reasonCode; + } + + public boolean isPermanent() { + return isPermanent; + } +} diff --git a/src/org/minima/system/network/base/peer/DisconnectRequestHandler.java b/src/org/minima/system/network/base/peer/DisconnectRequestHandler.java new file mode 100644 index 000000000..40581f073 --- /dev/null +++ b/src/org/minima/system/network/base/peer/DisconnectRequestHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import org.minima.system.network.base.SafeFuture; + +//import tech.pegasys.teku.infrastructure.async.SafeFuture; + +public interface DisconnectRequestHandler { + + SafeFuture requestDisconnect(DisconnectReason reason); +} diff --git a/src/org/minima/system/network/base/peer/DiscoveryPeer.java b/src/org/minima/system/network/base/peer/DiscoveryPeer.java new file mode 100644 index 000000000..e5acecffd --- /dev/null +++ b/src/org/minima/system/network/base/peer/DiscoveryPeer.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.EnrForkId; +import org.minima.system.network.base.ssz.SszBitvector; + +import java.net.InetSocketAddress; +import java.util.Optional; +// import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.EnrForkId; +// import tech.pegasys.teku.ssz.collections.SszBitvector; + +public class DiscoveryPeer { + private final Bytes publicKey; + private final InetSocketAddress nodeAddress; + private final Optional enrForkId; + private final SszBitvector persistentSubnets; + private String nodeRecord; + private Bytes nodeId; + + public DiscoveryPeer( + final Bytes publicKey, + final InetSocketAddress nodeAddress, + final Optional enrForkId, + final SszBitvector persistentSubnets) { + this.publicKey = publicKey; + this.nodeAddress = nodeAddress; + this.enrForkId = enrForkId; + this.persistentSubnets = persistentSubnets; + } + + public DiscoveryPeer( + final Bytes publicKey, + final InetSocketAddress nodeAddress, + final Optional enrForkId, + final SszBitvector persistentSubnets, + Bytes nodeId, String nodeRecord) { + this(publicKey, nodeAddress, enrForkId, persistentSubnets); + this.nodeId = nodeId; + this.nodeRecord = nodeRecord; + } + + public String getNodeRecord() { + return nodeRecord; + } + + public Bytes getNodeID() { + return nodeId; + } + + public Bytes getPublicKey() { + return publicKey; + } + + public InetSocketAddress getNodeAddress() { + return nodeAddress; + } + + public Optional getEnrForkId() { + return enrForkId; + } + + public SszBitvector getPersistentSubnets() { + return persistentSubnets; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DiscoveryPeer)) return false; + DiscoveryPeer that = (DiscoveryPeer) o; + return Objects.equal(getPublicKey(), that.getPublicKey()) + && Objects.equal(getNodeAddress(), that.getNodeAddress()) + && Objects.equal(getEnrForkId(), that.getEnrForkId()) + && Objects.equal(getPersistentSubnets(), that.getPersistentSubnets()); + } + + @Override + public int hashCode() { + return Objects.hashCode( + getPublicKey(), getNodeAddress(), getEnrForkId(), getPersistentSubnets()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("publicKey", publicKey) + .add("nodeAddress", nodeAddress) + .add("enrForkId", enrForkId) + .add("persistentSubnets", persistentSubnets) + .toString(); + } +} diff --git a/src/org/minima/system/network/base/peer/ExceptionUtil.java b/src/org/minima/system/network/base/peer/ExceptionUtil.java new file mode 100644 index 000000000..f0430e6dd --- /dev/null +++ b/src/org/minima/system/network/base/peer/ExceptionUtil.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Optional; +import java.util.function.Consumer; +import org.apache.commons.lang3.exception.ExceptionUtils; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; +import org.minima.system.network.base.SafeFuture; + +public class ExceptionUtil { + + @SuppressWarnings("unchecked") + public static Optional getCause( + final Throwable err, Class targetType) { + return ExceptionUtils.getThrowableList(err).stream() + .filter(targetType::isInstance) + .map(e -> (T) e) + .findFirst(); + } + + public static Runnable exceptionHandlingRunnable( + final Runnable runnable, final SafeFuture target) { + return () -> { + try { + runnable.run(); + } catch (final Throwable t) { + target.completeExceptionally(t); + } + }; + } + + public static Consumer exceptionHandlingConsumer( + final Consumer consumer, final SafeFuture target) { + return value -> { + try { + consumer.accept(value); + } catch (final Throwable t) { + target.completeExceptionally(t); + } + }; + } +} diff --git a/src/org/minima/system/network/base/peer/LibP2PNodeId.java b/src/org/minima/system/network/base/peer/LibP2PNodeId.java new file mode 100644 index 000000000..8caab3c74 --- /dev/null +++ b/src/org/minima/system/network/base/peer/LibP2PNodeId.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import io.libp2p.core.PeerId; +import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public class LibP2PNodeId extends NodeId { + private final PeerId peerId; + + public LibP2PNodeId(final PeerId peerId) { + this.peerId = peerId; + } + + @Override + public Bytes toBytes() { + return Bytes.wrap(peerId.getBytes()); + } + + @Override + public String toBase58() { + return peerId.toBase58(); + } +} diff --git a/src/org/minima/system/network/base/peer/LibP2PPeer.java b/src/org/minima/system/network/base/peer/LibP2PPeer.java new file mode 100644 index 000000000..604cf229b --- /dev/null +++ b/src/org/minima/system/network/base/peer/LibP2PPeer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import io.libp2p.core.Connection; +import io.libp2p.core.PeerId; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; + +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.libp2p.rpc.RpcHandler; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.DisconnectReason; +// import tech.pegasys.teku.networking.p2p.peer.DisconnectRequestHandler; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.peer.PeerDisconnectedSubscriber; +// import tech.pegasys.teku.networking.p2p.reputation.ReputationAdjustment; +// import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; +// import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; +// import tech.pegasys.teku.networking.p2p.rpc.RpcRequestHandler; +// import tech.pegasys.teku.networking.p2p.rpc.RpcStream; +import org.minima.system.network.base.SafeFuture; + +import org.minima.system.network.base.peer.RpcMethod; +import org.minima.system.network.base.peer.RpcHandler; +import org.minima.system.network.base.peer.RpcRequestHandler; +import org.minima.system.network.base.peer.RpcStream; + +public class LibP2PPeer implements Peer { + private static final Logger LOG = LogManager.getLogger(LibP2PPeer.class); + + private final Map rpcHandlers; + private final ReputationManager reputationManager; + private final Connection connection; + private final AtomicBoolean connected = new AtomicBoolean(true); + private final MultiaddrPeerAddress peerAddress; + + private volatile Optional disconnectReason = Optional.empty(); + private volatile boolean disconnectLocallyInitiated = false; + private volatile DisconnectRequestHandler disconnectRequestHandler = + reason -> { + disconnectImmediately(Optional.of(reason), true); + return SafeFuture.COMPLETE; + }; + + public LibP2PPeer( + final Connection connection, + final Map rpcHandlers, + final ReputationManager reputationManager) { + this.connection = connection; + this.rpcHandlers = rpcHandlers; + this.reputationManager = reputationManager; + + final PeerId peerId = connection.secureSession().getRemoteId(); + final NodeId nodeId = new LibP2PNodeId(peerId); + peerAddress = new MultiaddrPeerAddress(nodeId, connection.remoteAddress()); + SafeFuture.of(connection.closeFuture()) + .finish( + this::handleConnectionClosed, + error -> + LOG.trace( + "Peer {} connection close future completed exceptionally", peerId, error)); + } + + @Override + public PeerAddress getAddress() { + return peerAddress; + } + + @Override + public boolean isConnected() { + return connected.get(); + } + + @Override + public void disconnectImmediately( + final Optional reason, final boolean locallyInitiated) { + connected.set(false); + disconnectReason = reason; + disconnectLocallyInitiated = locallyInitiated; + SafeFuture.of(connection.close()) + .finish( + () -> LOG.trace("Disconnected from {}", getId()), + error -> LOG.warn("Failed to disconnect from peer {}", getId(), error)); + } + + @Override + public SafeFuture disconnectCleanly(final DisconnectReason reason) { + connected.set(false); + disconnectReason = Optional.of(reason); + disconnectLocallyInitiated = true; + return disconnectRequestHandler + .requestDisconnect(reason) + .handle( + (__, error) -> { + if (error != null) { + LOG.debug("Failed to disconnect from " + getId() + " cleanly.", error); + } + disconnectImmediately(Optional.of(reason), true); + return null; + }); + } + + @Override + public void setDisconnectRequestHandler(final DisconnectRequestHandler handler) { + this.disconnectRequestHandler = handler; + } + + @Override + public void subscribeDisconnect(final PeerDisconnectedSubscriber subscriber) { + SafeFuture.of(connection.closeFuture()) + .always(() -> subscriber.onDisconnected(disconnectReason, disconnectLocallyInitiated)); + } + + @Override + public SafeFuture sendRequest( + RpcMethod rpcMethod, final Bytes initialPayload, final RpcRequestHandler handler) { + RpcHandler rpcHandler = rpcHandlers.get(rpcMethod); + if (rpcHandler == null) { + throw new IllegalArgumentException("Unknown rpc method invoked: " + rpcMethod.getId()); + } + return rpcHandler.sendRequest(connection, initialPayload, handler); + } + + @Override + public boolean connectionInitiatedLocally() { + return connection.isInitiator(); + } + + @Override + public boolean connectionInitiatedRemotely() { + return !connectionInitiatedLocally(); + } + + private void handleConnectionClosed() { + LOG.debug("Disconnected from peer {}", getId()); + connected.set(false); + } + + @Override + public void adjustReputation(final ReputationAdjustment adjustment) { + final boolean shouldDisconnect = reputationManager.adjustReputation(getAddress(), adjustment); + if (shouldDisconnect) { + disconnectCleanly(DisconnectReason.REMOTE_FAULT).reportExceptions(); + } + } +} diff --git a/src/org/minima/system/network/base/peer/LibP2PRpcStream.java b/src/org/minima/system/network/base/peer/LibP2PRpcStream.java new file mode 100644 index 000000000..3e239fd8d --- /dev/null +++ b/src/org/minima/system/network/base/peer/LibP2PRpcStream.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import com.google.common.base.MoreObjects; +import io.libp2p.core.P2PChannel; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.rpc.RpcStream; +// import tech.pegasys.teku.networking.p2p.rpc.StreamClosedException; +import org.minima.system.network.base.SafeFuture; + +public class LibP2PRpcStream implements RpcStream { + + private final P2PChannel p2pChannel; + private final ChannelHandlerContext ctx; + private final AtomicBoolean writeStreamClosed = new AtomicBoolean(false); + private final NodeId nodeId; + + public LibP2PRpcStream( + final NodeId nodeId, final P2PChannel p2pChannel, final ChannelHandlerContext ctx) { + this.nodeId = nodeId; + this.p2pChannel = p2pChannel; + this.ctx = ctx; + } + + @Override + public SafeFuture writeBytes(final Bytes bytes) throws StreamClosedException { + if (writeStreamClosed.get()) { + throw new StreamClosedException(); + } + final ByteBuf reqByteBuf = ctx.alloc().buffer(); + reqByteBuf.writeBytes(bytes.toArrayUnsafe()); + + return toSafeFuture(ctx.writeAndFlush(reqByteBuf)); + } + + @Override + public SafeFuture closeAbruptly() { + writeStreamClosed.set(true); + return SafeFuture.of(p2pChannel.close()).thenApply((res) -> null); + } + + @Override + public SafeFuture closeWriteStream() { + writeStreamClosed.set(true); + return toSafeFuture(ctx.channel().disconnect()); + } + + private SafeFuture toSafeFuture(ChannelFuture channelFuture) { + final SafeFuture future = new SafeFuture<>(); + channelFuture.addListener( + (f) -> { + if (f.isSuccess()) { + future.complete(null); + } else { + future.completeExceptionally(f.cause()); + } + }); + return future; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("nodeId", nodeId) + .add("channel id", ctx.channel().id()) + .add("writeStreamClosed", writeStreamClosed) + .toString(); + } +} diff --git a/src/org/minima/system/network/base/peer/MultiaddrPeerAddress.java b/src/org/minima/system/network/base/peer/MultiaddrPeerAddress.java new file mode 100644 index 000000000..0f45589cb --- /dev/null +++ b/src/org/minima/system/network/base/peer/MultiaddrPeerAddress.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import io.libp2p.core.PeerId; +import io.libp2p.core.multiformats.Multiaddr; +import io.libp2p.core.multiformats.Protocol; +import java.util.Objects; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public class MultiaddrPeerAddress extends PeerAddress { + + private final Multiaddr multiaddr; + + MultiaddrPeerAddress(final NodeId nodeId, final Multiaddr multiaddr) { + super(nodeId); + this.multiaddr = multiaddr; + } + + @Override + public String toExternalForm() { + return multiaddr.toString(); + } + + public static MultiaddrPeerAddress fromAddress(final String address) { + final Multiaddr multiaddr = Multiaddr.fromString(address); + return fromMultiaddr(multiaddr); + } + + public static MultiaddrPeerAddress fromDiscoveryPeer(final DiscoveryPeer discoveryPeer) { + final Multiaddr multiaddr = MultiaddrUtil.fromDiscoveryPeer(discoveryPeer); + return fromMultiaddr(multiaddr); + } + + private static MultiaddrPeerAddress fromMultiaddr(final Multiaddr multiaddr) { + final String p2pComponent = multiaddr.getStringComponent(Protocol.P2P); + if (p2pComponent == null) { + throw new IllegalArgumentException("No peer ID present in multiaddr: " + multiaddr); + } + final LibP2PNodeId nodeId = new LibP2PNodeId(PeerId.fromBase58(p2pComponent)); + return new MultiaddrPeerAddress(nodeId, multiaddr); + } + + public Multiaddr getMultiaddr() { + return multiaddr; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final MultiaddrPeerAddress that = (MultiaddrPeerAddress) o; + return Objects.equals(multiaddr, that.multiaddr); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), multiaddr); + } +} diff --git a/src/org/minima/system/network/base/peer/MultiaddrUtil.java b/src/org/minima/system/network/base/peer/MultiaddrUtil.java new file mode 100644 index 000000000..a39358e74 --- /dev/null +++ b/src/org/minima/system/network/base/peer/MultiaddrUtil.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import static io.libp2p.crypto.keys.Secp256k1Kt.unmarshalSecp256k1PublicKey; + +import io.libp2p.core.PeerId; +import io.libp2p.core.crypto.PubKey; +import io.libp2p.core.multiformats.Multiaddr; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public class MultiaddrUtil { + + public static Multiaddr fromDiscoveryPeer(final DiscoveryPeer peer) { + return fromInetSocketAddress(peer.getNodeAddress(), getNodeId(peer)); + } + + public static Multiaddr fromDiscoveryPeerAsUdp(final DiscoveryPeer peer) { + return addPeerId(fromInetSocketAddress(peer.getNodeAddress(), "udp"), getNodeId(peer)); + } + + public static Multiaddr fromInetSocketAddress(final InetSocketAddress address) { + return fromInetSocketAddress(address, "tcp"); + } + + static Multiaddr fromInetSocketAddress(final InetSocketAddress address, final String protocol) { + final String addrString = + String.format( + "/%s/%s/%s/%d", + protocol(address.getAddress()), + address.getAddress().getHostAddress(), + protocol, + address.getPort()); + return Multiaddr.fromString(addrString); + } + + public static Multiaddr fromInetSocketAddress( + final InetSocketAddress address, final NodeId nodeId) { + return addPeerId(fromInetSocketAddress(address, "tcp"), nodeId); + } + + private static Multiaddr addPeerId(final Multiaddr addr, final NodeId nodeId) { + return new Multiaddr(addr, Multiaddr.fromString("/p2p/" + nodeId.toBase58())); + } + + private static LibP2PNodeId getNodeId(final DiscoveryPeer peer) { + final PubKey pubKey = unmarshalSecp256k1PublicKey(peer.getPublicKey().toArray()); + return new LibP2PNodeId(PeerId.fromPubKey(pubKey)); + } + + private static String protocol(final InetAddress address) { + return address instanceof Inet6Address ? "ip6" : "ip4"; + } +} diff --git a/src/org/minima/system/network/base/peer/NodeId.java b/src/org/minima/system/network/base/peer/NodeId.java new file mode 100644 index 000000000..ed78c6695 --- /dev/null +++ b/src/org/minima/system/network/base/peer/NodeId.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import org.apache.tuweni.bytes.Bytes; + +public abstract class NodeId { + + public abstract Bytes toBytes(); + + public abstract String toBase58(); + + @Override + public final int hashCode() { + return toBytes().hashCode(); + } + + @Override + public final boolean equals(final Object obj) { + if (!(obj instanceof NodeId)) { + return false; + } + return toBytes().equals(((NodeId) obj).toBytes()); + } + + @Override + public final String toString() { + return toBase58(); + } +} diff --git a/src/org/minima/system/network/base/peer/Peer.java b/src/org/minima/system/network/base/peer/Peer.java new file mode 100644 index 000000000..ef8ce313a --- /dev/null +++ b/src/org/minima/system/network/base/peer/Peer.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Objects; +import java.util.Optional; + +//import org.minima.system.network.base.DisconnectReason; +//import org.minima.system.network.base.peer.PeerDisconnectedSubscriber; +import org.minima.system.network.base.peer.RpcMethod; +import org.minima.system.network.base.peer.RpcRequestHandler; +import org.minima.system.network.base.peer.RpcStream; +import org.minima.system.network.base.SafeFuture; + +//import tech.pegasys.teku.networking.p2p.peer.DisconnectRequestHandler; + +import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; +//import tech.pegasys.teku.networking.p2p.network.PeerAddress; +//import tech.pegasys.teku.networking.p2p.reputation.ReputationAdjustment; +//import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; +//import tech.pegasys.teku.networking.p2p.rpc.RpcRequestHandler; +//import tech.pegasys.teku.networking.p2p.rpc.RpcStream; + +public interface Peer { + + default NodeId getId() { + return getAddress().getId(); + } + + PeerAddress getAddress(); + + boolean isConnected(); + + void disconnectImmediately(Optional reason, boolean locallyInitiated); + + SafeFuture disconnectCleanly(DisconnectReason reason); + + void setDisconnectRequestHandler(DisconnectRequestHandler handler); + + void subscribeDisconnect(PeerDisconnectedSubscriber subscriber); + + SafeFuture sendRequest( + RpcMethod rpcMethod, Bytes initialPayload, RpcRequestHandler handler); + + boolean connectionInitiatedLocally(); + + boolean connectionInitiatedRemotely(); + + default boolean idMatches(final Peer other) { + return other != null && Objects.equals(getId(), other.getId()); + } + + void adjustReputation(final ReputationAdjustment adjustment); +} diff --git a/src/org/minima/system/network/base/peer/PeerAddress.java b/src/org/minima/system/network/base/peer/PeerAddress.java new file mode 100644 index 000000000..7b155143a --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerAddress.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Objects; +import java.util.Optional; +//import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public class PeerAddress { + private final NodeId id; + + public PeerAddress(final NodeId id) { + this.id = id; + } + + public String toExternalForm() { + return toString(); + } + + public NodeId getId() { + return id; + } + + @SuppressWarnings("unchecked") + public Optional as(final Class clazz) { + if (clazz.isInstance(this)) { + return Optional.of((T) this); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return id.toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PeerAddress that = (PeerAddress) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/org/minima/system/network/base/peer/PeerAlreadyConnectedException.java b/src/org/minima/system/network/base/peer/PeerAlreadyConnectedException.java new file mode 100644 index 000000000..cd43d6459 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerAlreadyConnectedException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +//import tech.pegasys.teku.networking.p2p.peer.Peer; + +/** + * Indicates that two connections to the same PeerID were incorrectly established. + * + *

LibP2P usually detects attempts to establish multiple connections at the same time, but if we + * have incoming and outgoing connections simultaneously to the same peer, sometimes it slips + * through. In that case this exception is thrown so that the new connection is terminated before + * handshakes complete and we are able to identify the situation and return the existing peer. + */ +public class PeerAlreadyConnectedException extends RuntimeException { + private final Peer peer; + + public PeerAlreadyConnectedException(final Peer peer) { + super("Already connected to peer " + peer.getId().toBase58()); + this.peer = peer; + } + + public Peer getPeer() { + return peer; + } +} diff --git a/src/org/minima/system/network/base/peer/PeerConnectedSubscriber.java b/src/org/minima/system/network/base/peer/PeerConnectedSubscriber.java new file mode 100644 index 000000000..a5c2c5b07 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerConnectedSubscriber.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +@FunctionalInterface +public interface PeerConnectedSubscriber { + + void onConnected(T peer); +} diff --git a/src/org/minima/system/network/base/peer/PeerDisconnectedException.java b/src/org/minima/system/network/base/peer/PeerDisconnectedException.java new file mode 100644 index 000000000..10b518061 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerDisconnectedException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +public class PeerDisconnectedException extends RuntimeException { + + public PeerDisconnectedException() {} + + public PeerDisconnectedException(Throwable cause) { + super(cause); + } +} diff --git a/src/org/minima/system/network/base/peer/PeerDisconnectedSubscriber.java b/src/org/minima/system/network/base/peer/PeerDisconnectedSubscriber.java new file mode 100644 index 000000000..31ca1ca49 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerDisconnectedSubscriber.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Optional; + +@FunctionalInterface +public interface PeerDisconnectedSubscriber { + + void onDisconnected(Optional reason, boolean locallyInitiated); +} diff --git a/src/org/minima/system/network/base/peer/PeerHandler.java b/src/org/minima/system/network/base/peer/PeerHandler.java new file mode 100644 index 000000000..a54c25a99 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerHandler.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +//import tech.pegasys.teku.networking.p2p.peer.Peer; + +public interface PeerHandler { + void onConnect(final Peer peer); + + void onDisconnect(final Peer peer); +} diff --git a/src/org/minima/system/network/base/peer/PeerManager.java b/src/org/minima/system/network/base/peer/PeerManager.java new file mode 100644 index 000000000..be57cef49 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerManager.java @@ -0,0 +1,164 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +//package tech.pegasys.teku.networking.p2p.libp2p; +package org.minima.system.network.base.peer; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import io.libp2p.core.Connection; +import io.libp2p.core.ConnectionHandler; +import io.libp2p.core.Network; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +//import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.jetbrains.annotations.NotNull; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +// import tech.pegasys.teku.infrastructure.subscribers.Subscribers; +// import tech.pegasys.teku.networking.p2p.libp2p.rpc.RpcHandler; +// import tech.pegasys.teku.networking.p2p.network.PeerHandler; +// import tech.pegasys.teku.networking.p2p.peer.DisconnectReason; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.Peer; +// import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; +// import tech.pegasys.teku.networking.p2p.reputation.ReputationManager; +// import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; +import org.minima.system.network.base.SafeFuture; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.metrics.TekuMetricCategory; + +public class PeerManager implements ConnectionHandler { + + private static final Logger LOG = LogManager.getLogger(PeerManager.class); + + private final Map rpcHandlers; + + private final ConcurrentHashMap connectedPeerMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> pendingConnections = + new ConcurrentHashMap<>(); + private final ReputationManager reputationManager; + private final List peerHandlers; + + private final Subscribers> connectSubscribers = + Subscribers.create(true); + + public PeerManager( + final MetricsSystem metricsSystem, + final ReputationManager reputationManager, + final List peerHandlers, + final Map rpcHandlers) { + this.reputationManager = reputationManager; + this.peerHandlers = peerHandlers; + this.rpcHandlers = rpcHandlers; + metricsSystem.createGauge( + TekuMetricCategory.LIBP2P, "peers", "Tracks number of libp2p peers", this::getPeerCount); + } + + @Override + public void handleConnection(@NotNull final Connection connection) { + Peer peer = new LibP2PPeer(connection, rpcHandlers, reputationManager); + onConnectedPeer(peer); + } + + public long subscribeConnect(final PeerConnectedSubscriber subscriber) { + return connectSubscribers.subscribe(subscriber); + } + + public void unsubscribeConnect(final long subscriptionId) { + connectSubscribers.unsubscribe(subscriptionId); + } + + public SafeFuture connect(final MultiaddrPeerAddress peer, final Network network) { + return pendingConnections.computeIfAbsent(peer.getId(), __ -> doConnect(peer, network)); + } + + private SafeFuture doConnect(final MultiaddrPeerAddress peer, final Network network) { + LOG.debug("PeerMgr - Connecting to {}", peer); + + return SafeFuture.of(() -> network.connect(peer.getMultiaddr())) + .thenApply( + connection -> { + final LibP2PNodeId nodeId = + new LibP2PNodeId(connection.secureSession().getRemoteId()); + final Peer connectedPeer = connectedPeerMap.get(nodeId); + if (connectedPeer == null) { + if (connection.closeFuture().isDone()) { + // Connection has been immediately closed and the peer already removed + // Since the connection is closed anyway, we can create a new peer to wrap it. + return new LibP2PPeer(connection, rpcHandlers, reputationManager); + } else { + // Theoretically this should never happen because removing from the map is done + // by the close future completing, but make a loud noise just in case. + throw new IllegalStateException( + "No peer registered for established connection to " + nodeId); + } + } + reputationManager.reportInitiatedConnectionSuccessful(peer); + return connectedPeer; + }) + .exceptionallyCompose(this::handleConcurrentConnectionInitiation) + .catchAndRethrow(error -> reputationManager.reportInitiatedConnectionFailed(peer)) + .whenComplete((result, error) -> pendingConnections.remove(peer.getId())); + } + + private CompletionStage handleConcurrentConnectionInitiation(final Throwable error) { + final Throwable rootCause = Throwables.getRootCause(error); + return rootCause instanceof PeerAlreadyConnectedException + ? SafeFuture.completedFuture(((PeerAlreadyConnectedException) rootCause).getPeer()) + : SafeFuture.failedFuture(error); + } + + public Optional getPeer(NodeId id) { + return Optional.ofNullable(connectedPeerMap.get(id)); + } + + @VisibleForTesting + void onConnectedPeer(Peer peer) { + final boolean wasAdded = connectedPeerMap.putIfAbsent(peer.getId(), peer) == null; + if (wasAdded) { + LOG.debug("onConnectedPeer() {}", peer.getId()); + peerHandlers.forEach(h -> h.onConnect(peer)); + connectSubscribers.forEach(c -> c.onConnected(peer)); + peer.subscribeDisconnect( + (reason, locallyInitiated) -> onDisconnectedPeer(peer, reason, locallyInitiated)); + } else { + LOG.debug("Disconnecting duplicate connection to {}", peer::getId); + peer.disconnectImmediately(Optional.empty(), true); + throw new PeerAlreadyConnectedException(peer); + } + } + + private void onDisconnectedPeer( + final Peer peer, final Optional reason, final boolean locallyInitiated) { + if (connectedPeerMap.remove(peer.getId()) != null) { + LOG.debug("Peer disconnected: {}", peer.getId()); + reputationManager.reportDisconnection(peer.getAddress(), reason, locallyInitiated); + peerHandlers.forEach(h -> h.onDisconnect(peer)); + } + } + + public Stream streamPeers() { + return connectedPeerMap.values().stream(); + } + + public int getPeerCount() { + return connectedPeerMap.size(); + } +} diff --git a/src/org/minima/system/network/base/peer/PeerPools.java b/src/org/minima/system/network/base/peer/PeerPools.java new file mode 100644 index 000000000..5bf8f62b5 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerPools.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +//import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public class PeerPools { + + private static final PeerPool DEFAULT_POOL = PeerPool.SCORE_BASED; + private final Map knownSources = new ConcurrentHashMap<>(); + + public void addPeerToPool(final NodeId nodeId, final PeerPool peerPool) { + if (peerPool == DEFAULT_POOL) { + // No need to record the peer if it's in the default pool anyway. + forgetPeer(nodeId); + } else { + knownSources.put(nodeId, peerPool); + } + } + + public void forgetPeer(final NodeId nodeId) { + knownSources.remove(nodeId); + } + + public PeerPool getPool(final NodeId nodeId) { + return knownSources.getOrDefault(nodeId, DEFAULT_POOL); + } + + public enum PeerPool { + /** Default pool where peers are ranked based on their usefulness */ + SCORE_BASED, + /** + * Pool of peers we randomly selected which are kept connected to provide Sybil resistance + * regardless of their usefulness + */ + RANDOMLY_SELECTED, + /** Static peers which we maintain persistent connections to */ + STATIC + } +} diff --git a/src/org/minima/system/network/base/peer/PeerSelectionStrategy.java b/src/org/minima/system/network/base/peer/PeerSelectionStrategy.java new file mode 100644 index 000000000..f4d0716b6 --- /dev/null +++ b/src/org/minima/system/network/base/peer/PeerSelectionStrategy.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.List; +import java.util.function.Supplier; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.Peer; + +//import org.minima.system.network.base.DiscoveryPeer; +import org.minima.system.network.base.P2PNetwork; + +public interface PeerSelectionStrategy { + List selectPeersToConnect( + P2PNetwork network, PeerPools peerPools, Supplier> candidates); + + List selectPeersToDisconnect(P2PNetwork network, PeerPools peerPools); +} diff --git a/src/org/minima/system/network/base/peer/ReputationAdjustment.java b/src/org/minima/system/network/base/peer/ReputationAdjustment.java new file mode 100644 index 000000000..ef2e85b7c --- /dev/null +++ b/src/org/minima/system/network/base/peer/ReputationAdjustment.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +public enum ReputationAdjustment { + + // from ReputationManager.java + //static final int LARGE_CHANGE = 10; + //static final int SMALL_CHANGE = 3; + LARGE_PENALTY(-10), + SMALL_PENALTY(-3), + SMALL_REWARD(3), + LARGE_REWARD(10); + + private final int scoreChange; + + ReputationAdjustment(final int scoreChange) { + this.scoreChange = scoreChange; + } + + int getScoreChange() { + return scoreChange; + } +} diff --git a/src/org/minima/system/network/base/peer/ReputationManager.java b/src/org/minima/system/network/base/peer/ReputationManager.java new file mode 100644 index 000000000..ad97c86e3 --- /dev/null +++ b/src/org/minima/system/network/base/peer/ReputationManager.java @@ -0,0 +1,166 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +// import static tech.pegasys.teku.networking.p2p.peer.DisconnectReason.TOO_MANY_PEERS; +// import static tech.pegasys.teku.networking.p2p.peer.DisconnectReason.UNRESPONSIVE; +import static org.minima.system.network.base.peer.DisconnectReason.TOO_MANY_PEERS; +import static org.minima.system.network.base.peer.DisconnectReason.UNRESPONSIVE; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +// import org.hyperledger.besu.plugin.services.MetricsSystem; +// import tech.pegasys.teku.infrastructure.collections.cache.Cache; +// import tech.pegasys.teku.infrastructure.collections.cache.LRUCache; +// import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +// import tech.pegasys.teku.infrastructure.time.TimeProvider; +// import tech.pegasys.teku.infrastructure.unsigned.UInt64; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.DisconnectReason; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; + +import org.minima.system.network.base.LRUCache; +import org.minima.system.network.base.TimeProvider; +import org.minima.system.network.base.metrics.MetricsSystem; +import org.minima.system.network.base.metrics.TekuMetricCategory; +import org.minima.system.network.base.ssz.Cache; +import org.minima.system.network.base.ssz.UInt64; + +public class ReputationManager { + static final UInt64 FAILURE_BAN_PERIOD = UInt64.valueOf(TimeUnit.MINUTES.toSeconds(2)); + + static final int LARGE_CHANGE = 10; + static final int SMALL_CHANGE = 3; + private static final int DISCONNECT_THRESHOLD = -LARGE_CHANGE; + private static final int MAX_REPUTATION_SCORE = 2 * LARGE_CHANGE; + + private final TimeProvider timeProvider; + private final Cache peerReputations; + + public ReputationManager( + final MetricsSystem metricsSystem, final TimeProvider timeProvider, final int capacity) { + this.timeProvider = timeProvider; + this.peerReputations = new LRUCache<>(capacity); + metricsSystem.createIntegerGauge( + TekuMetricCategory.NETWORK, + "peer_reputation_cache_size", + "Total number of peer reputations tracked", + peerReputations::size); + } + + public void reportInitiatedConnectionFailed(final PeerAddress peerAddress) { + getOrCreateReputation(peerAddress) + .reportInitiatedConnectionFailed(timeProvider.getTimeInSeconds()); + } + + public boolean isConnectionInitiationAllowed(final PeerAddress peerAddress) { + return peerReputations + .getCached(peerAddress.getId()) + .map(reputation -> reputation.shouldInitiateConnection(timeProvider.getTimeInSeconds())) + .orElse(true); + } + + public void reportInitiatedConnectionSuccessful(final PeerAddress peerAddress) { + getOrCreateReputation(peerAddress).reportInitiatedConnectionSuccessful(); + } + + public void reportDisconnection( + final PeerAddress peerAddress, + final Optional reason, + final boolean locallyInitiated) { + getOrCreateReputation(peerAddress) + .reportDisconnection(timeProvider.getTimeInSeconds(), reason, locallyInitiated); + } + + /** + * Adjust the reputation score for a peer either positively or negatively. + * + * @param peerAddress the address of the peer to adjust the score for. + * @param effect the reputation change to apply. + * @return true if the peer should be disconnected, otherwise false. + */ + public boolean adjustReputation( + final PeerAddress peerAddress, final ReputationAdjustment effect) { + return getOrCreateReputation(peerAddress).adjustReputation(effect); + } + + private Reputation getOrCreateReputation(final PeerAddress peerAddress) { + return peerReputations.get(peerAddress.getId(), key -> new Reputation()); + } + + private static class Reputation { + private static final EnumSet LOCAL_TEMPORARY_DISCONNECT_REASONS = + EnumSet.of( + // We're currently at limit so don't mark peer unsuitable + TOO_MANY_PEERS, + // Peer may have been unresponsive due to a temporary network issue. In particular + // our internet access may have failed and all peers could be unresponsive. + // If we consider them all permanently unsuitable we may not be able to rejoin the + // network once our internet access is restored. + UNRESPONSIVE); + + private volatile Optional lastInitiationFailure = Optional.empty(); + private volatile boolean unsuitable = false; + private final AtomicInteger score = new AtomicInteger(0); + + public void reportInitiatedConnectionFailed(final UInt64 failureTime) { + lastInitiationFailure = Optional.of(failureTime); + } + + public boolean shouldInitiateConnection(final UInt64 currentTime) { + return !unsuitable + && lastInitiationFailure + .map( + lastFailureTime -> + lastFailureTime.plus(FAILURE_BAN_PERIOD).compareTo(currentTime) < 0) + .orElse(true); + } + + public void reportInitiatedConnectionSuccessful() { + lastInitiationFailure = Optional.empty(); + } + + public void reportDisconnection( + final UInt64 disconnectTime, + final Optional reason, + final boolean locallyInitiated) { + if (isLocallyConsideredUnsuitable(reason, locallyInitiated) + || reason.map(DisconnectReason::isPermanent).orElse(false)) { + unsuitable = true; + } else { + lastInitiationFailure = Optional.of(disconnectTime); + } + } + + private boolean isLocallyConsideredUnsuitable( + final Optional reason, final boolean locallyInitiated) { + return locallyInitiated + && reason.map(r -> !LOCAL_TEMPORARY_DISCONNECT_REASONS.contains(r)).orElse(false); + } + + public boolean adjustReputation(final ReputationAdjustment effect) { + final int newScore = + score.updateAndGet( + current -> Math.min(MAX_REPUTATION_SCORE, current + effect.getScoreChange())); + final boolean shouldDisconnect = newScore <= DISCONNECT_THRESHOLD; + if (shouldDisconnect) { + // Prevent our node from connecting out to the remote peer. + unsuitable = true; + } + return shouldDisconnect; + } + } +} diff --git a/src/org/minima/system/network/base/peer/RpcHandler.java b/src/org/minima/system/network/base/peer/RpcHandler.java new file mode 100644 index 000000000..16a460087 --- /dev/null +++ b/src/org/minima/system/network/base/peer/RpcHandler.java @@ -0,0 +1,229 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +//import static tech.pegasys.teku.infrastructure.async.FutureUtil.ignoreFuture; +import static org.minima.system.network.base.FutureUtil.ignoreFuture; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import io.libp2p.core.Connection; +import io.libp2p.core.ConnectionClosedException; +import io.libp2p.core.P2PChannel; +import io.libp2p.core.Stream; +import io.libp2p.core.StreamPromise; +import io.libp2p.core.multistream.ProtocolBinding; +import io.libp2p.core.multistream.ProtocolDescriptor; +import io.libp2p.etc.util.netty.mux.RemoteWriteClosed; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.jetbrains.annotations.NotNull; +import org.minima.system.network.base.AsyncRunner; +import org.minima.system.network.base.SafeFuture; +import org.minima.system.network.base.SafeFuture.Interruptor; +import org.minima.system.network.base.peer.RpcHandler.Controller; + +// import tech.pegasys.teku.infrastructure.async.AsyncRunner; +// import tech.pegasys.teku.infrastructure.async.SafeFuture; +// import tech.pegasys.teku.infrastructure.async.SafeFuture.Interruptor; +// import tech.pegasys.teku.infrastructure.exceptions.ExceptionUtil; +// import tech.pegasys.teku.networking.p2p.libp2p.LibP2PNodeId; +// import tech.pegasys.teku.networking.p2p.libp2p.rpc.RpcHandler.Controller; +// import tech.pegasys.teku.networking.p2p.peer.NodeId; +// import tech.pegasys.teku.networking.p2p.peer.PeerDisconnectedException; +// import tech.pegasys.teku.networking.p2p.rpc.RpcMethod; +// import tech.pegasys.teku.networking.p2p.rpc.RpcRequestHandler; +// import tech.pegasys.teku.networking.p2p.rpc.RpcStream; +// import tech.pegasys.teku.networking.p2p.rpc.StreamClosedException; +// import tech.pegasys.teku.networking.p2p.rpc.StreamTimeoutException; + +public class RpcHandler implements ProtocolBinding { + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Logger LOG = LogManager.getLogger(); + + private final RpcMethod rpcMethod; + private final AsyncRunner asyncRunner; + + public RpcHandler(final AsyncRunner asyncRunner, RpcMethod rpcMethod) { + this.asyncRunner = asyncRunner; + this.rpcMethod = rpcMethod; + } + + @SuppressWarnings("unchecked") + public SafeFuture sendRequest( + Connection connection, Bytes initialPayload, RpcRequestHandler handler) { + + Interruptor closeInterruptor = + SafeFuture.createInterruptor(connection.closeFuture(), PeerDisconnectedException::new); + Interruptor timeoutInterruptor = + SafeFuture.createInterruptor( + asyncRunner.getDelayedFuture(TIMEOUT), + () -> + new StreamTimeoutException( + "Timed out waiting to initialize stream for method " + rpcMethod.getId())); + + return SafeFuture.notInterrupted(closeInterruptor) + .thenApply(__ -> connection.muxerSession().createStream(this)) + // waiting for a stream or interrupt + .thenWaitFor(StreamPromise::getStream) + .orInterrupt(closeInterruptor, timeoutInterruptor) + .thenCompose( + streamPromise -> + // waiting for controller, writing initial payload or interrupt + SafeFuture.of(streamPromise.getController()) + .orInterrupt(closeInterruptor, timeoutInterruptor) + .thenPeek(ctr -> ctr.setRequestHandler(handler)) + .thenApply(Controller::getRpcStream) + .thenWaitFor(rpcStream -> rpcStream.writeBytes(initialPayload)) + .orInterrupt(closeInterruptor, timeoutInterruptor) + // closing the stream in case of any errors or interruption + .whenException(err -> closeStreamAbruptly(streamPromise.getStream().join()))) + .catchAndRethrow( + err -> { + if (ExceptionUtil.getCause(err, ConnectionClosedException.class).isPresent()) { + throw new PeerDisconnectedException(err); + } + }); + } + + private void closeStreamAbruptly(Stream stream) { + SafeFuture.of(stream.close()).reportExceptions(); + } + + @NotNull + @Override + public ProtocolDescriptor getProtocolDescriptor() { + return new ProtocolDescriptor(rpcMethod.getId()); + } + + @NotNull + @Override + public SafeFuture initChannel(P2PChannel channel, String s) { + final Connection connection = ((io.libp2p.core.Stream) channel).getConnection(); + final NodeId nodeId = new LibP2PNodeId(connection.secureSession().getRemoteId()); + Controller controller = new Controller(nodeId, channel); + if (!channel.isInitiator()) { + controller.setRequestHandler(rpcMethod.createIncomingRequestHandler()); + } + channel.pushHandler(controller); + return controller.activeFuture; + } + + static class Controller extends SimpleChannelInboundHandler { + private final NodeId nodeId; + private final P2PChannel p2pChannel; + private Optional rpcRequestHandler = Optional.empty(); + private RpcStream rpcStream; + private boolean readCompleted = false; + + protected final SafeFuture activeFuture = new SafeFuture<>(); + + private Controller(final NodeId nodeId, final P2PChannel p2pChannel) { + this.nodeId = nodeId; + this.p2pChannel = p2pChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + rpcStream = new LibP2PRpcStream(nodeId, p2pChannel, ctx); + activeFuture.complete(this); + } + + public RpcStream getRpcStream() { + return rpcStream; + } + + @Override + protected void channelRead0(final ChannelHandlerContext ctx, final ByteBuf msg) { + runHandler(h -> h.processData(nodeId, rpcStream, msg)); + } + + public void setRequestHandler(RpcRequestHandler rpcRequestHandler) { + if (this.rpcRequestHandler.isPresent()) { + throw new IllegalStateException("Attempt to set an already set data handler"); + } + this.rpcRequestHandler = Optional.of(rpcRequestHandler); + + activeFuture.finish( + () -> { + rpcRequestHandler.active(nodeId, rpcStream); + }, + err -> { + if (err != null) { + if (Throwables.getRootCause(err) instanceof StreamClosedException) { + LOG.debug("Stream closed while processing rpc input", err); + } else { + LOG.error("Unhandled exception while processing rpc input", err); + } + } + }); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + LOG.error("Unhandled error while processes req/response", cause); + final IllegalStateException exception = new IllegalStateException("Channel exception", cause); + activeFuture.completeExceptionally(exception); + closeAbruptly(); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + onChannelClosed(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof RemoteWriteClosed) { + onRemoteWriteClosed(); + } + } + + private void onRemoteWriteClosed() { + if (!readCompleted) { + readCompleted = true; + runHandler(h -> h.readComplete(nodeId, rpcStream)); + } + } + + private void onChannelClosed() { + try { + onRemoteWriteClosed(); + runHandler(h -> h.closed(nodeId, rpcStream)); + } finally { + rpcRequestHandler = Optional.empty(); + } + } + + private void runHandler(final Consumer action) { + rpcRequestHandler.ifPresentOrElse(action, this::closeAbruptly); + } + + @VisibleForTesting + void closeAbruptly() { + // We're listening for the result of the close future above, so we can ignore this future + ignoreFuture(p2pChannel.close()); + + // Make sure to complete activation future in case we are never activated + activeFuture.completeExceptionally(new StreamClosedException()); + } + } +} diff --git a/src/org/minima/system/network/base/peer/RpcMethod.java b/src/org/minima/system/network/base/peer/RpcMethod.java new file mode 100644 index 000000000..636405d5d --- /dev/null +++ b/src/org/minima/system/network/base/peer/RpcMethod.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +public interface RpcMethod { + + String getId(); + + RpcRequestHandler createIncomingRequestHandler(); +} diff --git a/src/org/minima/system/network/base/peer/RpcRequestHandler.java b/src/org/minima/system/network/base/peer/RpcRequestHandler.java new file mode 100644 index 000000000..c8ae85d1c --- /dev/null +++ b/src/org/minima/system/network/base/peer/RpcRequestHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import io.netty.buffer.ByteBuf; +//import tech.pegasys.teku.networking.p2p.peer.NodeId; + +public interface RpcRequestHandler { + + void active(final NodeId nodeId, final RpcStream rpcStream); + + void processData(final NodeId nodeId, final RpcStream rpcStream, final ByteBuf data); + + void readComplete(final NodeId nodeId, final RpcStream rpcStream); + + void closed(final NodeId nodeId, final RpcStream rpcStream); +} diff --git a/src/org/minima/system/network/base/peer/RpcStream.java b/src/org/minima/system/network/base/peer/RpcStream.java new file mode 100644 index 000000000..528c99787 --- /dev/null +++ b/src/org/minima/system/network/base/peer/RpcStream.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.infrastructure.async.SafeFuture; +import org.minima.system.network.base.SafeFuture; + +public interface RpcStream { + + SafeFuture writeBytes(Bytes bytes) throws StreamClosedException; + + /** + * Close the stream altogether, allowing no further reads or writes. + * + * @return A future completing when the stream is closed. + */ + SafeFuture closeAbruptly(); + + /** + * Close the write side of the stream. When both sides of the stream close their write stream, the + * entire stream will be closed. + * + * @return A future completing when the write stream is closed. + */ + SafeFuture closeWriteStream(); +} diff --git a/src/org/minima/system/network/base/peer/SimplePeerSelectionStrategy.java b/src/org/minima/system/network/base/peer/SimplePeerSelectionStrategy.java new file mode 100644 index 000000000..7e26b9041 --- /dev/null +++ b/src/org/minima/system/network/base/peer/SimplePeerSelectionStrategy.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +//import static tech.pegasys.teku.networking.p2p.connection.PeerPools.PeerPool.STATIC; +import static org.minima.system.network.base.peer.PeerPools.PeerPool.STATIC; + +import java.util.List; +import java.util.function.Supplier; +// import tech.pegasys.teku.networking.p2p.connection.PeerPools; +// import tech.pegasys.teku.networking.p2p.connection.PeerSelectionStrategy; +// import tech.pegasys.teku.networking.p2p.connection.TargetPeerRange; +// import tech.pegasys.teku.networking.p2p.discovery.DiscoveryPeer; +// import tech.pegasys.teku.networking.p2p.network.P2PNetwork; +// import tech.pegasys.teku.networking.p2p.network.PeerAddress; +// import tech.pegasys.teku.networking.p2p.peer.Peer; + +//import org.minima.system.network.base.; + +import org.minima.system.network.base.P2PNetwork; + +public class SimplePeerSelectionStrategy implements PeerSelectionStrategy { + private final TargetPeerRange targetPeerRange; + + public SimplePeerSelectionStrategy(final TargetPeerRange targetPeerRange) { + this.targetPeerRange = targetPeerRange; + } + + @Override + public List selectPeersToConnect( + final P2PNetwork network, + final PeerPools peerPools, + final Supplier> candidates) { + final int peersToAdd = targetPeerRange.getPeersToAdd(network.getPeerCount()); + if (peersToAdd == 0) { + return emptyList(); + } + return candidates.get().stream() + .map(network::createPeerAddress) + .limit(peersToAdd) + .collect(toList()); + } + + @Override + public List selectPeersToDisconnect( + final P2PNetwork network, final PeerPools peerPools) { + final int peersToDrop = targetPeerRange.getPeersToDrop(network.getPeerCount()); + return network + .streamPeers() + .filter(peer -> peerPools.getPool(peer.getId()) != STATIC) + .limit(peersToDrop) + .collect(toList()); + } +} diff --git a/src/org/minima/system/network/base/peer/StreamClosedException.java b/src/org/minima/system/network/base/peer/StreamClosedException.java new file mode 100644 index 000000000..513163593 --- /dev/null +++ b/src/org/minima/system/network/base/peer/StreamClosedException.java @@ -0,0 +1,16 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +public class StreamClosedException extends RuntimeException {} diff --git a/src/org/minima/system/network/base/peer/StreamTimeoutException.java b/src/org/minima/system/network/base/peer/StreamTimeoutException.java new file mode 100644 index 000000000..f90645409 --- /dev/null +++ b/src/org/minima/system/network/base/peer/StreamTimeoutException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +public class StreamTimeoutException extends RuntimeException { + + public StreamTimeoutException(final String message) { + super(message); + } +} diff --git a/src/org/minima/system/network/base/peer/Subscribers.java b/src/org/minima/system/network/base/peer/Subscribers.java new file mode 100644 index 000000000..87841336b --- /dev/null +++ b/src/org/minima/system/network/base/peer/Subscribers.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Tracks subscribers that should be notified when some event occurred. This class is safe to use + * from multiple threads. + * + *

Each subscriber is assigned a unique ID which can be used to unsubscribe. This approach was + * chosen over using the subscriber's object identity to eliminate a common trap believing that + * method references are equal when they refer to the same method. For example, if object identity + * were used to track subscribers it would be possible to write the incorrect code: + * + *

+ * subscribers.subscribe(this::onSomeEvent);
+ * subscribers.unsubscribe(this::onSomeEvent);
+ * 
+ * + *

Since the two separate this:onSomeEvent are not equal, the subscriber wouldn't be + * removed. This bug is avoided by assigning each subscriber a unique ID and using that to + * unsubscribe. + * + * @param the type of subscribers + */ +public class Subscribers { + private static final Logger LOG = LogManager.getLogger(); + + private final AtomicLong subscriberId = new AtomicLong(); + private final Map subscribers = new ConcurrentHashMap<>(); + + private final boolean suppressCallbackExceptions; + + private Subscribers(final boolean suppressCallbackExceptions) { + this.suppressCallbackExceptions = suppressCallbackExceptions; + } + + public static Subscribers create(final boolean suppressCallbackExceptions) { + return new Subscribers<>(suppressCallbackExceptions); + } + + /** + * Add a subscriber to the list. + * + * @param subscriber the subscriber to add + * @return the ID assigned to this subscriber + */ + public long subscribe(final T subscriber) { + final long id = subscriberId.getAndIncrement(); + subscribers.put(id, subscriber); + return id; + } + + /** + * Remove a subscriber from the list. + * + * @param subscriberId the ID of the subscriber to remove + * @return true if a subscriber with that ID was found and removed, otherwise + * false + */ + public boolean unsubscribe(final long subscriberId) { + return subscribers.remove(subscriberId) != null; + } + + /** + * Iterate through the current list of subscribers. This is typically used to deliver events e.g.: + * + *

+   * subscribers.forEach(subscriber -> subscriber.onEvent());
+   * 
+ * + * @param action the action to perform for each subscriber + */ + public void forEach(final Consumer action) { + subscribers + .values() + .forEach( + subscriber -> { + try { + action.accept(subscriber); + } catch (Throwable throwable) { + if (suppressCallbackExceptions) { + LOG.error("Error in callback: ", throwable); + } else { + throw throwable; + } + } + }); + } + + /** + * Deliver an event to each subscriber by calling eventMethod. + * + * @param eventMethod the method to call + * @param event the event object to provide as a parameter + * @param the type of the event + */ + public void deliver(final BiConsumer eventMethod, final E event) { + forEach(subscriber -> eventMethod.accept(subscriber, event)); + } + + /** + * Get the current subscriber count. + * + * @return the current number of subscribers. + */ + public int getSubscriberCount() { + return subscribers.size(); + } +} diff --git a/src/org/minima/system/network/base/peer/TargetPeerRange.java b/src/org/minima/system/network/base/peer/TargetPeerRange.java new file mode 100644 index 000000000..571c8d9b3 --- /dev/null +++ b/src/org/minima/system/network/base/peer/TargetPeerRange.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.peer; + +import com.google.common.base.Preconditions; + +public class TargetPeerRange { + private final int lowerBound; + private final int upperBound; + private final int minimumRandomlySelectedPeerCount; + + public TargetPeerRange( + final int lowerBound, final int upperBound, final int minimumRandomlySelectedPeerCount) { + Preconditions.checkArgument(lowerBound <= upperBound, "Invalid target peer count range"); + this.lowerBound = lowerBound; + this.upperBound = upperBound; + this.minimumRandomlySelectedPeerCount = minimumRandomlySelectedPeerCount; + } + + public int getPeersToAdd(final int currentPeerCount) { + return currentPeerCount < lowerBound ? upperBound - currentPeerCount : 0; + } + + public int getPeersToDrop(final int currentPeerCount) { + return currentPeerCount > upperBound ? currentPeerCount - upperBound : 0; + } + + public int getRandomlySelectedPeersToAdd(final int currentRandomlySelectedPeerCount) { + return currentRandomlySelectedPeerCount < minimumRandomlySelectedPeerCount + ? minimumRandomlySelectedPeerCount - currentRandomlySelectedPeerCount + : 0; + } + + public int getRandomlySelectedPeersToDrop( + final int currentRandomlySelectedPeerCount, final int currentTotalPeerCount) { + final int totalPeersToDrop = getPeersToDrop(currentTotalPeerCount); + if (totalPeersToDrop == 0) { + return 0; + } + return currentRandomlySelectedPeerCount > minimumRandomlySelectedPeerCount + ? Math.min( + currentRandomlySelectedPeerCount - minimumRandomlySelectedPeerCount, totalPeersToDrop) + : 0; + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszCollection.java b/src/org/minima/system/network/base/ssz/AbstractSszCollection.java new file mode 100644 index 000000000..e302be743 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszCollection.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Supplier; +// import tech.pegasys.teku.ssz.SszCollection; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszCollectionSchema; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public abstract class AbstractSszCollection + extends AbstractSszComposite implements SszCollection { + + protected AbstractSszCollection( + SszCompositeSchema schema, Supplier lazyBackingNode) { + super(schema, lazyBackingNode); + } + + protected AbstractSszCollection(SszCompositeSchema schema, TreeNode backingNode) { + super(schema, backingNode); + } + + protected AbstractSszCollection( + SszCompositeSchema schema, TreeNode backingNode, IntCache cache) { + super(schema, backingNode, cache); + } + + @SuppressWarnings("unchecked") + @Override + public SszCollectionSchema getSchema() { + return (SszCollectionSchema) super.getSchema(); + } + + @SuppressWarnings("unchecked") + @Override + protected SszElementT getImpl(int index) { + SszCollectionSchema type = + (SszCollectionSchema) this.getSchema(); + SszSchema elementType = type.getElementSchema(); + TreeNode node = + getBackingNode().get(type.getChildGeneralizedIndex(index / type.getElementsPerChunk())); + return (SszElementT) + elementType.createFromBackingNode(node, index % type.getElementsPerChunk()); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszCollectionSchema.java b/src/org/minima/system/network/base/ssz/AbstractSszCollectionSchema.java new file mode 100644 index 000000000..44913ca43 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszCollectionSchema.java @@ -0,0 +1,347 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.Integer.min; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.tuweni.bytes.Bytes; + +import org.minima.system.network.base.ssz.SszSchemaHints.SszSuperNodeHint; + +// import tech.pegasys.teku.ssz.SszCollection; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints.SszSuperNodeHint; +// import tech.pegasys.teku.ssz.schema.SszType; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.SszNodeTemplate; +// import tech.pegasys.teku.ssz.tree.SszSuperNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +/** Type of homogeneous collections (like List and Vector) */ +public abstract class AbstractSszCollectionSchema< + SszElementT extends SszData, SszCollectionT extends SszCollection> + implements SszCompositeSchema { + + private final long maxLength; + private final SszSchema elementSchema; + private final SszSchemaHints hints; + protected final Supplier elementSszSupernodeTemplate = + Suppliers.memoize(() -> SszNodeTemplate.createFromType(getElementSchema())); + private volatile TreeNode defaultTree; + + protected AbstractSszCollectionSchema( + long maxLength, SszSchema elementSchema, SszSchemaHints hints) { + checkArgument(maxLength >= 0); + this.maxLength = maxLength; + this.elementSchema = elementSchema; + this.hints = hints; + } + + protected abstract TreeNode createDefaultTree(); + + @Override + public TreeNode getDefaultTree() { + if (defaultTree == null) { + this.defaultTree = createDefaultTree(); + } + return defaultTree; + } + + @Override + public long getMaxLength() { + return maxLength; + } + + public SszSchema getElementSchema() { + return elementSchema; + } + + @Override + public SszSchema getChildSchema(int index) { + if (index >= maxLength) { + throw new IndexOutOfBoundsException("Child index > maxLength"); + } + return getElementSchema(); + } + + @Override + public int getElementsPerChunk() { + SszSchema elementSchema = getElementSchema(); + return elementSchema.isPrimitive() + ? (256 / ((SszPrimitiveSchema) elementSchema).getBitsSize()) + : 1; + } + + protected int getSszElementBitSize() { + SszSchema elementSchema = getElementSchema(); + return elementSchema.isPrimitive() + ? ((SszPrimitiveSchema) elementSchema).getBitsSize() + : elementSchema.getSszFixedPartSize() * 8; + } + + protected int getVariablePartSize(TreeNode vectorNode, int length) { + if (isFixedSize()) { + return 0; + } else { + int size = 0; + for (int i = 0; i < length; i++) { + size += getElementSchema().getSszSize(vectorNode.get(getChildGeneralizedIndex(i))); + } + return size; + } + } + + /** + * Serializes {@code elementsCount} from the content of this collection + * + * @param vectorNode for a {@link SszVectorSchemaImpl} type - the node itself, for a {@link + * SszListSchemaImpl} - the left sibling node of list size node + */ + public int sszSerializeVector(TreeNode vectorNode, SszWriter writer, int elementsCount) { + if (getElementSchema().isFixedSize()) { + return sszSerializeFixedVectorFast(vectorNode, writer, elementsCount); + } else { + return sszSerializeVariableVector(vectorNode, writer, elementsCount); + } + } + + private int sszSerializeFixedVectorFast( + TreeNode vectorNode, SszWriter writer, int elementsCount) { + if (elementsCount == 0) { + return 0; + } + int nodesCount = getChunks(elementsCount); + int[] bytesCnt = new int[1]; + TreeUtil.iterateLeavesData( + vectorNode, + getChildGeneralizedIndex(0), + getChildGeneralizedIndex(nodesCount - 1), + leafData -> { + writer.write(leafData); + bytesCnt[0] += leafData.size(); + }); + return bytesCnt[0]; + } + + private int sszSerializeVariableVector(TreeNode vectorNode, SszWriter writer, int elementsCount) { + SszSchema elementType = getElementSchema(); + int variableOffset = SSZ_LENGTH_SIZE * elementsCount; + for (int i = 0; i < elementsCount; i++) { + TreeNode childSubtree = vectorNode.get(getChildGeneralizedIndex(i)); + int childSize = elementType.getSszSize(childSubtree); + writer.write(SszType.sszLengthToBytes(variableOffset)); + variableOffset += childSize; + } + for (int i = 0; i < elementsCount; i++) { + TreeNode childSubtree = vectorNode.get(getChildGeneralizedIndex(i)); + elementType.sszSerializeTree(childSubtree, writer); + } + return variableOffset; + } + + protected DeserializedData sszDeserializeVector(SszReader reader) { + if (getElementSchema().isFixedSize()) { + Optional sszSuperNodeHint = getHints().getHint(SszSuperNodeHint.class); + if (sszSuperNodeHint.isPresent()) { + return sszDeserializeSupernode(reader, sszSuperNodeHint.get().getDepth()); + } else { + return sszDeserializeFixed(reader); + } + } else { + return sszDeserializeVariable(reader); + } + } + + private DeserializedData sszDeserializeSupernode(SszReader reader, int supernodeDepth) { + SszNodeTemplate template = elementSszSupernodeTemplate.get(); + int sszSize = reader.getAvailableBytes(); + if (sszSize % template.getSszLength() != 0) { + throw new SszDeserializeException("Ssz length is not multiple of element length"); + } + int elementsCount = sszSize / template.getSszLength(); + int chunkSize = (1 << supernodeDepth) * template.getSszLength(); + int bytesRemain = sszSize; + List sszNodes = new ArrayList<>(bytesRemain / chunkSize + 1); + while (bytesRemain > 0) { + int toRead = min(bytesRemain, chunkSize); + bytesRemain -= toRead; + Bytes bytes = reader.read(toRead); + SszSuperNode node = new SszSuperNode(supernodeDepth, template, bytes); + sszNodes.add(node); + } + TreeNode tree = + TreeUtil.createTree( + sszNodes, + new SszSuperNode(supernodeDepth, template, Bytes.EMPTY), + treeDepth() - supernodeDepth); + return new DeserializedData(tree, elementsCount); + } + + private DeserializedData sszDeserializeFixed(SszReader reader) { + int bytesSize = reader.getAvailableBytes(); + checkSsz( + bytesSize % getElementSchema().getSszFixedPartSize() == 0, + "SSZ sequence length is not multiple of fixed element size"); + int elementBitSize = getSszElementBitSize(); + if (elementBitSize >= 8) { + checkSsz( + bytesSize * 8 / elementBitSize <= getMaxLength(), + "SSZ sequence length exceeds max type length"); + } else { + // preliminary rough check + checkSsz( + (bytesSize - 1) * 8 / elementBitSize <= getMaxLength(), + "SSZ sequence length exceeds max type length"); + } + if (getElementSchema() instanceof AbstractSszPrimitiveSchema) { + int bytesRemain = bytesSize; + List childNodes = new ArrayList<>(bytesRemain / LeafNode.MAX_BYTE_SIZE + 1); + while (bytesRemain > 0) { + int toRead = min(bytesRemain, LeafNode.MAX_BYTE_SIZE); + bytesRemain -= toRead; + Bytes bytes = reader.read(toRead); + LeafNode node = LeafNode.create(bytes); + childNodes.add(node); + } + + Optional lastByte; + if (childNodes.isEmpty()) { + lastByte = Optional.empty(); + } else { + Bytes lastNodeData = childNodes.get(childNodes.size() - 1).getData(); + lastByte = Optional.of(lastNodeData.get(lastNodeData.size() - 1)); + } + return new DeserializedData( + TreeUtil.createTree(childNodes, treeDepth()), bytesSize * 8 / elementBitSize, lastByte); + } else { + int elementsCount = bytesSize / getElementSchema().getSszFixedPartSize(); + List childNodes = new ArrayList<>(); + for (int i = 0; i < elementsCount; i++) { + try (SszReader sszReader = reader.slice(getElementSchema().getSszFixedPartSize())) { + TreeNode childNode = getElementSchema().sszDeserializeTree(sszReader); + childNodes.add(childNode); + } + } + return new DeserializedData(TreeUtil.createTree(childNodes, treeDepth()), elementsCount); + } + } + + private DeserializedData sszDeserializeVariable(SszReader reader) { + final int endOffset = reader.getAvailableBytes(); + final List childNodes = new ArrayList<>(); + if (endOffset > 0) { + int firstElementOffset = SszType.sszBytesToLength(reader.read(SSZ_LENGTH_SIZE)); + checkSsz(firstElementOffset % SSZ_LENGTH_SIZE == 0, "Invalid first element offset"); + int elementsCount = firstElementOffset / SSZ_LENGTH_SIZE; + checkSsz(elementsCount <= getMaxLength(), "SSZ sequence length exceeds max type length"); + List elementOffsets = new ArrayList<>(elementsCount + 1); + elementOffsets.add(firstElementOffset); + for (int i = 1; i < elementsCount; i++) { + int offset = SszType.sszBytesToLength(reader.read(SSZ_LENGTH_SIZE)); + elementOffsets.add(offset); + } + elementOffsets.add(endOffset); + + List elementSizes = + IntStream.range(0, elementOffsets.size() - 1) + .map(i -> elementOffsets.get(i + 1) - elementOffsets.get(i)) + .boxed() + .collect(Collectors.toList()); + + if (elementSizes.stream().anyMatch(s -> s < 0)) { + throw new SszDeserializeException("Invalid SSZ: wrong child offsets"); + } + + for (int elementSize : elementSizes) { + try (SszReader sszReader = reader.slice(elementSize)) { + childNodes.add(getElementSchema().sszDeserializeTree(sszReader)); + } + } + } + return new DeserializedData(TreeUtil.createTree(childNodes, treeDepth()), childNodes.size()); + } + + protected static void checkSsz(boolean condition, String error) { + if (!condition) { + throw new SszDeserializeException(error); + } + } + + public SszSchemaHints getHints() { + return hints; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractSszCollectionSchema that = (AbstractSszCollectionSchema) o; + return maxLength == that.maxLength && elementSchema.equals(that.elementSchema); + } + + @Override + public int hashCode() { + return Objects.hash(maxLength, elementSchema); + } + + protected static class DeserializedData { + + private final TreeNode dataTree; + private final int childrenCount; + private final Optional lastSszByte; + + public DeserializedData(TreeNode dataTree, int childrenCount) { + this(dataTree, childrenCount, Optional.empty()); + } + + public DeserializedData(TreeNode dataTree, int childrenCount, Optional lastSszByte) { + this.dataTree = dataTree; + this.childrenCount = childrenCount; + this.lastSszByte = lastSszByte; + } + + public TreeNode getDataTree() { + return dataTree; + } + + public int getChildrenCount() { + return childrenCount; + } + + public Optional getLastSszByte() { + return lastSszByte; + } + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszComposite.java b/src/org/minima/system/network/base/ssz/AbstractSszComposite.java new file mode 100644 index 000000000..36d52f235 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszComposite.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Optional; +import java.util.function.Supplier; +// import tech.pegasys.teku.ssz.SszComposite; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.cache.ArrayIntCache; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** + * Base backing class for immutable composite ssz structures (lists, vectors, containers) + * + *

It caches it's child ssz instances so that if the underlying tree nodes are not changed (in + * the corresponding mutable classes) the instances are not recreated from tree nodes on later + * access. + * + *

Though internally this class has a mutable cache it may be thought of as immutable instance + * and used safely across threads + * + * @param the type of children. For heterogeneous composites (like container) this type + * would be just generic {@link SszData} + */ +public abstract class AbstractSszComposite + implements SszComposite { + + private final IntCache childrenViewCache; + private int sizeCache = -1; + private final SszCompositeSchema schema; + private final Supplier backingNode; + + /** Creates an instance from a schema and a backing node */ + protected AbstractSszComposite(SszCompositeSchema schema, Supplier lazyBackingNode) { + this(schema, lazyBackingNode, Optional.empty()); + } + + protected AbstractSszComposite(SszCompositeSchema schema, TreeNode backingNode) { + this(schema, () -> backingNode, Optional.empty()); + } + + /** + * Creates an instance from a schema and a backing node. + * + *

{@link SszData} instances cache is supplied for optimization to shortcut children creation + * from backing nodes. The cache should correspond to the supplied backing tree. + */ + protected AbstractSszComposite( + SszCompositeSchema schema, TreeNode backingNode, IntCache cache) { + this(schema, () -> backingNode, Optional.of(cache)); + } + + protected AbstractSszComposite( + SszCompositeSchema schema, + Supplier lazyBackingNode, + Optional> cache) { + this.schema = schema; + this.backingNode = lazyBackingNode; + this.childrenViewCache = cache.orElseGet(this::createCache); + } + + /** + * 'Transfers' the cache to a new Cache instance eliminating all the cached values from the + * current view cache. This is made under assumption that the ssz data instance this cache is + * transferred to would be used further with high probability and this ssz data instance would be + * either GCed or used with lower probability + */ + IntCache transferCache() { + return childrenViewCache.transfer(); + } + + /** + * Creates a new empty children cache. Could be overridden by subclasses for fine tuning of the + * initial cache size + */ + protected IntCache createCache() { + return new ArrayIntCache<>(); + } + + @Override + public final SszChildT get(int index) { + return childrenViewCache.getInt(index, this::getImplWithIndexCheck); + } + + private SszChildT getImplWithIndexCheck(int index) { + checkIndex(index); + return getImpl(index); + } + + /** Cache miss fallback child getter. This is where child is created from the backing tree node */ + protected abstract SszChildT getImpl(int index); + + @Override + public SszCompositeSchema getSchema() { + return schema; + } + + @Override + public TreeNode getBackingNode() { + return backingNode.get(); + } + + @Override + public final int size() { + if (sizeCache == -1) { + sizeCache = sizeImpl(); + } + return sizeCache; + } + + /** Size value is normally cached. This method calculates the size from backing tree */ + protected abstract int sizeImpl(); + + /** + * Checks the child index + * + * @throws IndexOutOfBoundsException if index is invalid + */ + protected abstract void checkIndex(int index); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SszComposite)) { + return false; + } + SszComposite that = (SszComposite) o; + return getSchema().equals(that.getSchema()) && hashTreeRoot().equals(that.hashTreeRoot()); + } + + @Override + public int hashCode() { + return hashTreeRoot().slice(28).toInt(); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszContainerSchema.java b/src/org/minima/system/network/base/ssz/AbstractSszContainerSchema.java new file mode 100644 index 000000000..b396d7209 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszContainerSchema.java @@ -0,0 +1,325 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +// import tech.pegasys.teku.ssz.SszContainer; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.schema.SszContainerSchema; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.schema.SszType; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszLengthBounds; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +public abstract class AbstractSszContainerSchema + implements SszContainerSchema { + + public static class NamedSchema { + private final String name; + private final SszSchema schema; + + public static NamedSchema of(String name, SszSchema schema) { + return new NamedSchema<>(name, schema); + } + + private NamedSchema(String name, SszSchema schema) { + this.name = name; + this.schema = schema; + } + + public String getName() { + return name; + } + + public SszSchema getSchema() { + return schema; + } + } + + protected static NamedSchema namedSchema( + String fieldName, SszSchema schema) { + return new NamedSchema<>(fieldName, schema); + } + + private final String containerName; + private final List childrenNames = new ArrayList<>(); + private final Map childrenNamesToFieldIndex = new HashMap<>(); + private final List> childrenSchemas; + private final TreeNode defaultTree; + private final long treeWidth; + + protected AbstractSszContainerSchema(String name, List> childrenSchemas) { + this.containerName = name; + for (int i = 0; i < childrenSchemas.size(); i++) { + final NamedSchema childSchema = childrenSchemas.get(i); + if (childrenNamesToFieldIndex.put(childSchema.getName(), i) != null) { + throw new IllegalArgumentException( + "Duplicate field name detected for field " + childSchema.getName() + " at index " + i); + } + childrenNames.add(childSchema.getName()); + } + this.childrenSchemas = + childrenSchemas.stream().map(NamedSchema::getSchema).collect(Collectors.toList()); + this.defaultTree = createDefaultTree(); + this.treeWidth = SszContainerSchema.super.treeWidth(); + } + + protected AbstractSszContainerSchema(List> childrenSchemas) { + this.containerName = ""; + for (int i = 0; i < childrenSchemas.size(); i++) { + final String name = "field-" + i; + childrenNamesToFieldIndex.put(name, i); + childrenNames.add(name); + } + this.childrenSchemas = childrenSchemas; + this.defaultTree = createDefaultTree(); + this.treeWidth = SszContainerSchema.super.treeWidth(); + } + + @Override + public TreeNode createTreeFromFieldValues(List fieldValues) { + checkArgument(fieldValues.size() == getFieldsCount(), "Wrong number of filed values"); + return TreeUtil.createTree( + fieldValues.stream().map(SszData::getBackingNode).collect(Collectors.toList())); + } + + @Override + public C getDefault() { + return createFromBackingNode(getDefaultTree()); + } + + @Override + public TreeNode getDefaultTree() { + return defaultTree; + } + + @Override + public long treeWidth() { + return treeWidth; + } + + private TreeNode createDefaultTree() { + List defaultChildren = new ArrayList<>((int) getMaxLength()); + for (int i = 0; i < getFieldsCount(); i++) { + defaultChildren.add(getChildSchema(i).getDefault().getBackingNode()); + } + return TreeUtil.createTree(defaultChildren); + } + + @Override + public SszSchema getChildSchema(int index) { + return childrenSchemas.get(index); + } + + /** + * Get the index of a field by name + * + * @param fieldName + * @return The index if it exists, otherwise -1 + */ + @Override + public int getFieldIndex(String fieldName) { + final Integer index = childrenNamesToFieldIndex.get(fieldName); + return index == null ? -1 : index; + } + + @Override + public abstract C createFromBackingNode(TreeNode node); + + @Override + public long getMaxLength() { + return childrenSchemas.size(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractSszContainerSchema that = (AbstractSszContainerSchema) o; + return childrenSchemas.equals(that.childrenSchemas); + } + + @Override + public int hashCode() { + return Objects.hash(childrenSchemas); + } + + @Override + public boolean isFixedSize() { + for (int i = 0; i < getFieldsCount(); i++) { + if (!getChildSchema(i).isFixedSize()) { + return false; + } + } + return true; + } + + @Override + public int getSszFixedPartSize() { + int size = 0; + for (int i = 0; i < getFieldsCount(); i++) { + SszSchema childType = getChildSchema(i); + size += childType.isFixedSize() ? childType.getSszFixedPartSize() : SSZ_LENGTH_SIZE; + } + return size; + } + + @Override + public int getSszVariablePartSize(TreeNode node) { + int size = 0; + for (int i = 0; i < getFieldsCount(); i++) { + SszSchema childType = getChildSchema(i); + if (!childType.isFixedSize()) { + size += childType.getSszSize(node.get(getChildGeneralizedIndex(i))); + } + } + return size; + } + + @Override + public List> getFieldSchemas() { + return childrenSchemas; + } + + @Override + public int sszSerializeTree(TreeNode node, SszWriter writer) { + int variableChildOffset = getSszFixedPartSize(); + int[] variableSizes = new int[getFieldsCount()]; + for (int i = 0; i < getFieldsCount(); i++) { + TreeNode childSubtree = node.get(getChildGeneralizedIndex(i)); + SszSchema childType = getChildSchema(i); + if (childType.isFixedSize()) { + int size = childType.sszSerializeTree(childSubtree, writer); + assert size == childType.getSszFixedPartSize(); + } else { + writer.write(SszType.sszLengthToBytes(variableChildOffset)); + int childSize = childType.getSszSize(childSubtree); + variableSizes[i] = childSize; + variableChildOffset += childSize; + } + } + for (int i = 0; i < getMaxLength(); i++) { + SszSchema childType = getChildSchema(i); + if (!childType.isFixedSize()) { + TreeNode childSubtree = node.get(getChildGeneralizedIndex(i)); + int size = childType.sszSerializeTree(childSubtree, writer); + assert size == variableSizes[i]; + } + } + return variableChildOffset; + } + + @Override + public TreeNode sszDeserializeTree(SszReader reader) { + int endOffset = reader.getAvailableBytes(); + int childCount = getFieldsCount(); + Queue fixedChildrenSubtrees = new ArrayDeque<>(childCount); + List variableChildrenOffsets = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + SszSchema childType = getChildSchema(i); + if (childType.isFixedSize()) { + try (SszReader sszReader = reader.slice(childType.getSszFixedPartSize())) { + TreeNode childNode = childType.sszDeserializeTree(sszReader); + fixedChildrenSubtrees.add(childNode); + } + } else { + int childOffset = SszType.sszBytesToLength(reader.read(SSZ_LENGTH_SIZE)); + variableChildrenOffsets.add(childOffset); + } + } + + if (variableChildrenOffsets.isEmpty()) { + if (reader.getAvailableBytes() > 0) { + throw new SszDeserializeException("Invalid SSZ: unread bytes for fixed size container"); + } + } else { + if (variableChildrenOffsets.get(0) != endOffset - reader.getAvailableBytes()) { + throw new SszDeserializeException( + "First variable element offset doesn't match the end of fixed part"); + } + } + + variableChildrenOffsets.add(endOffset); + + ArrayDeque variableChildrenSizes = + new ArrayDeque<>(variableChildrenOffsets.size() - 1); + for (int i = 0; i < variableChildrenOffsets.size() - 1; i++) { + variableChildrenSizes.add( + variableChildrenOffsets.get(i + 1) - variableChildrenOffsets.get(i)); + } + + if (variableChildrenSizes.stream().anyMatch(s -> s < 0)) { + throw new SszDeserializeException("Invalid SSZ: wrong child offsets"); + } + + List childrenSubtrees = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + SszSchema childType = getChildSchema(i); + if (childType.isFixedSize()) { + childrenSubtrees.add(fixedChildrenSubtrees.remove()); + } else { + try (SszReader sszReader = reader.slice(variableChildrenSizes.remove())) { + TreeNode childNode = childType.sszDeserializeTree(sszReader); + childrenSubtrees.add(childNode); + } + } + } + + return TreeUtil.createTree(childrenSubtrees); + } + + @Override + public SszLengthBounds getSszLengthBounds() { + return IntStream.range(0, getFieldsCount()) + .mapToObj(this::getChildSchema) + // dynamic sized children need 4-byte offset + .map(t -> t.getSszLengthBounds().addBytes((t.isFixedSize() ? 0 : SSZ_LENGTH_SIZE))) + // elements are not packed in containers + .map(SszLengthBounds::ceilToBytes) + .reduce(SszLengthBounds.ZERO, SszLengthBounds::add); + } + + @Override + public String getContainerName() { + return !containerName.isEmpty() ? containerName : getClass().getName(); + } + + @Override + public List getFieldNames() { + return childrenNames; + } + + @Override + public String toString() { + return getContainerName(); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszImmutableContainer.java b/src/org/minima/system/network/base/ssz/AbstractSszImmutableContainer.java new file mode 100644 index 000000000..d6af20152 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszImmutableContainer.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Preconditions; +import java.util.Arrays; +import java.util.Objects; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableContainer; +// import tech.pegasys.teku.ssz.cache.ArrayIntCache; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszContainerSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** Handy base class for immutable containers */ +public abstract class AbstractSszImmutableContainer extends SszContainerImpl { + + protected AbstractSszImmutableContainer( + AbstractSszContainerSchema schema) { + this(schema, schema.getDefaultTree()); + } + + protected AbstractSszImmutableContainer( + SszContainerSchema schema, TreeNode backingNode) { + super(schema, backingNode); + } + + protected AbstractSszImmutableContainer( + SszContainerSchema schema, SszData... memberValues) { + super( + schema, + schema.createTreeFromFieldValues(Arrays.asList(memberValues)), + createCache(memberValues)); + checkArgument( + memberValues.length == this.getSchema().getMaxLength(), + "Wrong number of member values: %s", + memberValues.length); + for (int i = 0; i < memberValues.length; i++) { + Preconditions.checkArgument( + memberValues[i].getSchema().equals(schema.getChildSchema(i)), + "Wrong child schema at index %s. Expected: %s, was %s", + i, + schema.getChildSchema(i), + memberValues[i].getSchema()); + } + } + + private static IntCache createCache(SszData... memberValues) { + ArrayIntCache cache = new ArrayIntCache<>(memberValues.length); + for (int i = 0; i < memberValues.length; i++) { + cache.invalidateWithNewValue(i, memberValues[i]); + } + return cache; + } + + @Override + public SszMutableContainer createWritableCopy() { + throw new UnsupportedOperationException( + "This container doesn't support mutable structure: " + getClass().getName()); + } + + @Override + public boolean isWritableSupported() { + return false; + } + + @Override + public boolean equals(Object obj) { + if (Objects.isNull(obj)) { + return false; + } + + if (this == obj) { + return true; + } + + if (!(obj instanceof AbstractSszImmutableContainer)) { + return false; + } + + AbstractSszImmutableContainer other = (AbstractSszImmutableContainer) obj; + return hashTreeRoot().equals(other.hashTreeRoot()); + } + + @Override + public int hashCode() { + return hashTreeRoot().slice(0, 4).toInt(); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszMutableCollection.java b/src/org/minima/system/network/base/ssz/AbstractSszMutableCollection.java new file mode 100644 index 000000000..5b20c471a --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszMutableCollection.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.schema.SszCollectionSchema; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUpdates; + +public abstract class AbstractSszMutableCollection< + SszElementT extends SszData, SszMutableElementT extends SszElementT> + extends AbstractSszMutableComposite { + + protected AbstractSszMutableCollection(AbstractSszComposite backingImmutableData) { + super(backingImmutableData); + } + + @Override + public SszCollectionSchema getSchema() { + return (SszCollectionSchema) super.getSchema(); + } + + @Override + protected TreeUpdates packChanges( + List> newChildValues, TreeNode original) { + SszCollectionSchema type = getSchema(); + SszSchema elementType = type.getElementSchema(); + int elementsPerChunk = type.getElementsPerChunk(); + + return newChildValues.stream() + .collect(Collectors.groupingBy(e -> e.getKey() / elementsPerChunk)) + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .map( + e -> { + int nodeIndex = e.getKey(); + List> nodeVals = e.getValue(); + long gIndex = type.getChildGeneralizedIndex(nodeIndex); + // optimization: when all packed values changed no need to retrieve original node to + // merge with + TreeNode node = + nodeVals.size() == elementsPerChunk ? LeafNode.EMPTY_LEAF : original.get(gIndex); + for (Map.Entry entry : nodeVals) { + node = + elementType.updateBackingNode( + node, entry.getKey() % elementsPerChunk, entry.getValue()); + } + return new TreeUpdates.Update(gIndex, node); + }) + .collect(TreeUpdates.collector()); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszMutableComposite.java b/src/org/minima/system/network/base/ssz/AbstractSszMutableComposite.java new file mode 100644 index 000000000..1738f04b9 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszMutableComposite.java @@ -0,0 +1,234 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +// import tech.pegasys.teku.ssz.InvalidValueSchemaException; +// import tech.pegasys.teku.ssz.SszComposite; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableComposite; +// import tech.pegasys.teku.ssz.SszMutableData; +// import tech.pegasys.teku.ssz.SszMutableRefComposite; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUpdates; + +/** + * Base backing {@link SszMutableData} class for mutable composite ssz structures (lists, vectors, + * containers) + * + *

It has corresponding backing immutable {@link SszData} and the set of changed children. When + * the {@link #commitChanges()} is called a new immutable {@link SszData} instance is created where + * changes accumulated in this instance are merged with cached backing {@link SszData} instance + * which weren't changed. + * + *

If this ssz data is get by reference from its parent composite view ({@link + * SszMutableRefComposite#getByRef(int)} then all the changes are notified to the parent view (see + * {@link SszMutableComposite#setInvalidator(Consumer)} + * + *

The mutable structures based on this class are inherently NOT thread safe + */ +public abstract class AbstractSszMutableComposite< + SszChildT extends SszData, SszMutableChildT extends SszChildT> + implements SszMutableRefComposite { + + protected AbstractSszComposite backingImmutableData; + private Consumer invalidator; + private final Map childrenChanges = new HashMap<>(); + private final Map childrenRefs = new HashMap<>(); + private final Set childrenRefsChanged = new HashSet<>(); + private Integer sizeCache; + + /** Creates a new mutable instance with backing immutable data */ + protected AbstractSszMutableComposite(AbstractSszComposite backingImmutableData) { + this.backingImmutableData = backingImmutableData; + sizeCache = backingImmutableData.size(); + } + + @Override + public void set(int index, SszChildT value) { + checkIndex(index, true); + checkNotNull(value); + if (!value.getSchema().equals(getSchema().getChildSchema(index))) { + throw new InvalidValueSchemaException( + "Expected child to have schema " + + getSchema().getChildSchema(index) + + ", but value has schema " + + value.getSchema()); + } + if (childrenRefs.containsKey(index)) { + throw new IllegalStateException( + "A child couldn't be simultaneously modified by value and accessed by ref"); + } + childrenChanges.put(index, value); + sizeCache = index >= sizeCache ? index + 1 : sizeCache; + invalidate(); + } + + @Override + public SszChildT get(int index) { + checkIndex(index, false); + SszChildT ret = childrenChanges.get(index); + if (ret != null) { + return ret; + } else if (childrenRefs.containsKey(index)) { + return childrenRefs.get(index); + } else { + return backingImmutableData.get(index); + } + } + + @Override + public SszMutableChildT getByRef(int index) { + SszMutableChildT ret = childrenRefs.get(index); + if (ret == null) { + SszChildT readView = get(index); + childrenChanges.remove(index); + @SuppressWarnings("unchecked") + SszMutableChildT w = (SszMutableChildT) readView.createWritableCopy(); + if (w instanceof SszMutableComposite) { + ((SszMutableComposite) w) + .setInvalidator( + viewWrite -> { + childrenRefsChanged.add(index); + invalidate(); + }); + } + childrenRefs.put(index, w); + ret = w; + } + return ret; + } + + @Override + public SszCompositeSchema getSchema() { + return backingImmutableData.getSchema(); + } + + @Override + @SuppressWarnings("unchecked") + public void clear() { + backingImmutableData = (AbstractSszComposite) getSchema().getDefault(); + childrenChanges.clear(); + childrenRefs.clear(); + childrenRefsChanged.clear(); + sizeCache = backingImmutableData.size(); + invalidate(); + } + + @Override + public int size() { + return sizeCache; + } + + @Override + @SuppressWarnings("unchecked") + public SszComposite commitChanges() { + if (childrenChanges.isEmpty() && childrenRefsChanged.isEmpty()) { + return backingImmutableData; + } else { + IntCache cache = backingImmutableData.transferCache(); + List> changesList = + Stream.concat( + childrenChanges.entrySet().stream(), + childrenRefsChanged.stream() + .map( + idx -> + new SimpleImmutableEntry<>( + idx, + (SszChildT) + ((SszMutableData) childrenRefs.get(idx)).commitChanges()))) + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + // pre-fill the read cache with changed values + changesList.forEach(e -> cache.invalidateWithNewValue(e.getKey(), e.getValue())); + TreeNode originalBackingTree = backingImmutableData.getBackingNode(); + TreeUpdates changes = changesToNewNodes(changesList, originalBackingTree); + TreeNode newBackingTree = originalBackingTree.updated(changes); + TreeNode finalBackingTree = doFinalTreeUpdates(newBackingTree); + return createImmutableSszComposite(finalBackingTree, cache); + } + } + + protected TreeNode doFinalTreeUpdates(TreeNode updatedTree) { + return updatedTree; + } + + /** Converts a set of changed view with their indexes to the {@link TreeUpdates} instance */ + protected TreeUpdates changesToNewNodes( + List> newChildValues, TreeNode original) { + SszCompositeSchema type = getSchema(); + int elementsPerChunk = type.getElementsPerChunk(); + if (elementsPerChunk == 1) { + return newChildValues.stream() + .map( + e -> + new TreeUpdates.Update( + type.getChildGeneralizedIndex(e.getKey()), e.getValue().getBackingNode())) + .collect(TreeUpdates.collector()); + } else { + return packChanges(newChildValues, original); + } + } + + /** + * Converts a set of changed view with their indexes to the {@link TreeUpdates} instance for views + * which support packed values (i.e. several child views per backing tree node) + */ + protected abstract TreeUpdates packChanges( + List> newChildValues, TreeNode original); + + /** + * Should be implemented by subclasses to create respectful immutable view with backing tree and + * views cache + */ + protected abstract AbstractSszComposite createImmutableSszComposite( + TreeNode backingNode, IntCache viewCache); + + @Override + public void setInvalidator(Consumer listener) { + invalidator = listener; + } + + protected void invalidate() { + if (invalidator != null) { + invalidator.accept(this); + } + } + + /** Creating nested mutable copies is not supported yet */ + @Override + public SszMutableComposite createWritableCopy() { + throw new UnsupportedOperationException( + "createWritableCopy() is now implemented for immutable SszData only"); + } + + /** + * Checks the child index for get or set + * + * @throws IndexOutOfBoundsException is index is not valid + */ + protected abstract void checkIndex(int index, boolean set); +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszPrimitive.java b/src/org/minima/system/network/base/ssz/AbstractSszPrimitive.java new file mode 100644 index 000000000..8af5e6cbb --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszPrimitive.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszPrimitiveSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public abstract class AbstractSszPrimitive> + implements SszPrimitive { + private final AbstractSszPrimitiveSchema schema; + private final C value; + + protected AbstractSszPrimitive(C value, AbstractSszPrimitiveSchema schema) { + checkNotNull(value); + this.schema = schema; + this.value = value; + } + + @Override + public C get() { + return value; + } + + @Override + public AbstractSszPrimitiveSchema getSchema() { + return schema; + } + + @Override + public TreeNode getBackingNode() { + return getSchema().createBackingNode(getThis()); + } + + @SuppressWarnings("unchecked") + protected V getThis() { + return (V) this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractSszPrimitive that = (AbstractSszPrimitive) o; + return Objects.equals(get(), that.get()); + } + + @Override + public int hashCode() { + return Objects.hash(get()); + } + + @Override + public String toString() { + return get().toString(); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszPrimitiveSchema.java b/src/org/minima/system/network/base/ssz/AbstractSszPrimitiveSchema.java new file mode 100644 index 000000000..1513e1b0e --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszPrimitiveSchema.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +//import static tech.pegasys.teku.ssz.tree.TreeUtil.bitsCeilToBytes; +import static org.minima.system.network.base.ssz.TreeUtil.bitsCeilToBytes; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszLengthBounds; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.LeafDataNode; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** + * Represents primitive view type + * + * @param Class of the basic view of this type + */ +public abstract class AbstractSszPrimitiveSchema< + DataT, SszDataT extends SszPrimitive> + implements SszPrimitiveSchema { + + private final int bitsSize; + + protected AbstractSszPrimitiveSchema(int bitsSize) { + checkArgument( + bitsSize > 0 && bitsSize <= 256 && 256 % bitsSize == 0, "Invalid bitsize: %s", bitsSize); + this.bitsSize = bitsSize; + } + + @Override + public int getBitsSize() { + return bitsSize; + } + + @Override + public SszDataT createFromBackingNode(TreeNode node) { + return createFromBackingNode(node, 0); + } + + @Override + public final SszDataT createFromBackingNode(TreeNode node, int internalIndex) { + assert node instanceof LeafDataNode; + return createFromLeafBackingNode((LeafDataNode) node, internalIndex); + } + + public abstract SszDataT createFromLeafBackingNode(LeafDataNode node, int internalIndex); + + public TreeNode createBackingNode(SszDataT newValue) { + return updateBackingNode(LeafNode.EMPTY_LEAF, 0, newValue); + } + + @Override + public abstract TreeNode updateBackingNode(TreeNode srcNode, int internalIndex, SszData newValue); + + private int getSSZBytesSize() { + return bitsCeilToBytes(getBitsSize()); + } + + @Override + public boolean isFixedSize() { + return true; + } + + @Override + public int getSszFixedPartSize() { + return getSSZBytesSize(); + } + + @Override + public int getSszVariablePartSize(TreeNode node) { + return 0; + } + + @Override + public int sszSerializeTree(TreeNode node, SszWriter writer) { + int sszBytesSize = getSSZBytesSize(); + final Bytes nodeData; + if (node instanceof LeafDataNode) { + // small perf optimization + nodeData = ((LeafDataNode) node).getData(); + } else { + nodeData = node.hashTreeRoot(); + } + writer.write(nodeData.toArrayUnsafe(), 0, sszBytesSize); + return sszBytesSize; + } + + @Override + public TreeNode sszDeserializeTree(SszReader reader) { + Bytes bytes = reader.read(getSSZBytesSize()); + if (reader.getAvailableBytes() > 0) { + throw new SszDeserializeException("Extra " + reader.getAvailableBytes() + " bytes found"); + } + return LeafNode.create(bytes); + } + + @Override + public SszLengthBounds getSszLengthBounds() { + return SszLengthBounds.ofBits(getBitsSize()); + } +} diff --git a/src/org/minima/system/network/base/ssz/AbstractSszVectorSchema.java b/src/org/minima/system/network/base/ssz/AbstractSszVectorSchema.java new file mode 100644 index 000000000..ea9141e0d --- /dev/null +++ b/src/org/minima/system/network/base/ssz/AbstractSszVectorSchema.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static java.util.Collections.emptyList; +import static org.minima.system.network.base.ssz.TreeUtil.bitsCeilToBytes; +import static org.minima.system.network.base.ssz.SszSchemaHints.SszSuperNodeHint; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszVector; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints.SszSuperNodeHint; +// import tech.pegasys.teku.ssz.schema.SszVectorSchema; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszLengthBounds; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.SszSuperNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +public abstract class AbstractSszVectorSchema< + SszElementT extends SszData, SszVectorT extends SszVector> + extends AbstractSszCollectionSchema + implements SszVectorSchema { + + private final boolean isListBacking; + + protected AbstractSszVectorSchema(SszSchema elementType, long vectorLength) { + this(elementType, vectorLength, false); + } + + protected AbstractSszVectorSchema( + SszSchema elementType, long vectorLength, boolean isListBacking) { + this(elementType, vectorLength, isListBacking, SszSchemaHints.none()); + } + + protected AbstractSszVectorSchema( + SszSchema elementSchema, + long vectorLength, + boolean isListBacking, + SszSchemaHints hints) { + super(vectorLength, elementSchema, hints); + this.isListBacking = isListBacking; + } + + @Override + public SszVectorT getDefault() { + return createFromBackingNode(getDefaultTree()); + } + + @Override + protected TreeNode createDefaultTree() { + if (isListBacking) { + Optional sszSuperNodeHint = getHints().getHint(SszSuperNodeHint.class); + if (sszSuperNodeHint.isPresent()) { + int superNodeDepth = sszSuperNodeHint.get().getDepth(); + SszSuperNode defaultSuperSszNode = + new SszSuperNode(superNodeDepth, elementSszSupernodeTemplate.get(), Bytes.EMPTY); + int binaryDepth = treeDepth() - superNodeDepth; + return TreeUtil.createTree(emptyList(), defaultSuperSszNode, binaryDepth); + } else { + return TreeUtil.createDefaultTree(maxChunks(), LeafNode.EMPTY_LEAF); + } + } else if (getElementsPerChunk() == 1) { + return TreeUtil.createDefaultTree(maxChunks(), getElementSchema().getDefaultTree()); + } else { + // packed vector + int fullZeroNodesCount = getLength() / getElementsPerChunk(); + int lastNodeElementCount = getLength() % getElementsPerChunk(); + int lastNodeSizeBytes = bitsCeilToBytes(lastNodeElementCount * getSszElementBitSize()); + Stream fullZeroNodes = + Stream.generate(() -> LeafNode.ZERO_LEAVES[32]).limit(fullZeroNodesCount); + Stream lastZeroNode = + lastNodeSizeBytes > 0 + ? Stream.of(LeafNode.ZERO_LEAVES[lastNodeSizeBytes]) + : Stream.empty(); + return TreeUtil.createTree( + Stream.concat(fullZeroNodes, lastZeroNode).collect(Collectors.toList())); + } + } + + @Override + public abstract SszVectorT createFromBackingNode(TreeNode node); + + @Override + public int getLength() { + long maxLength = getMaxLength(); + if (maxLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Vector size too large: " + maxLength); + } + return (int) maxLength; + } + + public int getChunksCount() { + long maxChunks = maxChunks(); + if (maxChunks > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Vector size too large: " + maxChunks); + } + return (int) maxChunks; + } + + @Override + public boolean isFixedSize() { + return getElementSchema().isFixedSize(); + } + + @Override + public int getSszVariablePartSize(TreeNode node) { + return getVariablePartSize(node, getLength()); + } + + @Override + public int getSszFixedPartSize() { + int bitsPerChild = isFixedSize() ? getSszElementBitSize() : SSZ_LENGTH_SIZE * 8; + return bitsCeilToBytes(getLength() * bitsPerChild); + } + + @Override + public int sszSerializeTree(TreeNode node, SszWriter writer) { + return sszSerializeVector(node, writer, getLength()); + } + + @Override + public TreeNode sszDeserializeTree(SszReader reader) { + if (getElementSchema() == SszPrimitiveSchemas.BIT_SCHEMA) { + throw new UnsupportedOperationException( + "Bitvector deserialization is only supported by SszBitvectorSchema"); + } + + DeserializedData data = sszDeserializeVector(reader); + if (data.getChildrenCount() != getLength()) { + throw new SszDeserializeException("Invalid Vector ssz"); + } + return data.getDataTree(); + } + + @Override + public SszLengthBounds getSszLengthBounds() { + return getElementSchema() + .getSszLengthBounds() + // if elements are of dynamic size the offset size should be added for every element + .addBytes(getElementSchema().isFixedSize() ? 0 : SSZ_LENGTH_SIZE) + .mul(getLength()) + .ceilToBytes(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AbstractSszVectorSchema)) { + return false; + } + AbstractSszVectorSchema that = (AbstractSszVectorSchema) o; + return getElementSchema().equals(that.getElementSchema()) + && getMaxLength() == that.getMaxLength(); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + return "Vector[" + getElementSchema() + ", " + getLength() + "]"; + } +} diff --git a/src/org/minima/system/network/base/ssz/ArrayIntCache.java b/src/org/minima/system/network/base/ssz/ArrayIntCache.java new file mode 100644 index 000000000..8bebcac19 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/ArrayIntCache.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.IntFunction; + +/** + * Thread-safe int indexed cache + * + *

CAUTION: though the class is thread-safe it contains no synchronisation for performance + * reasons When accessed concurrently the cache may result in extra cache misses and extra backing + * array copying but this should be safe. In optimistic scenarios such overhead could be neglected + * + *

Modify this class carefully to not violate thread safety! + */ +public final class ArrayIntCache implements IntCache { + private static final int DEFAULT_INITIAL_CACHE_SIZE = 16; + private volatile V[] values; + private final int initSize; + + public ArrayIntCache() { + this(16); + } + + public ArrayIntCache(int initialSize) { + this.initSize = initialSize; + this.values = createArray(initialSize); + } + + private ArrayIntCache(V[] values, int initSize) { + this.values = values; + this.initSize = initSize; + } + + @SuppressWarnings("unchecked") + private V[] createArray(int size) { + return (V[]) new Object[size]; + } + + private V[] extend(int index) { + V[] valuesLocal = this.values; + int newSize = valuesLocal.length; + if (index >= newSize) { + while (index >= newSize) { + newSize <<= 1; + } + V[] newValues = Arrays.copyOf(valuesLocal, newSize); + this.values = newValues; + return newValues; + } + return valuesLocal; + } + + @Override + public V getInt(int key, IntFunction fallback) { + V[] valuesLocal = this.values; + V val = key >= valuesLocal.length ? null : valuesLocal[key]; + if (val == null) { + val = fallback.apply(key); + extend(key)[key] = val; + } + return val; + } + + @Override + public Optional getCached(Integer key) { + V[] valuesLocal = this.values; + return key >= valuesLocal.length ? Optional.empty() : Optional.ofNullable(valuesLocal[key]); + } + + @Override + public IntCache copy() { + V[] valuesLocal = this.values; + return new ArrayIntCache<>(Arrays.copyOf(valuesLocal, valuesLocal.length), initSize); + } + + @Override + public IntCache transfer() { + IntCache copy = copy(); + this.values = createArray(DEFAULT_INITIAL_CACHE_SIZE); + return copy; + } + + @Override + public void invalidateWithNewValueInt(int key, V newValue) { + extend(key)[key] = newValue; + } + + @Override + public void invalidateInt(int key) { + V[] valuesLocal = this.values; + if (key < valuesLocal.length) { + valuesLocal[key] = null; + } + } + + @Override + public void clear() { + values = createArray(initSize); + } + + /** Returns the current number of items in the cache */ + @Override + public int size() { + return values.length; + } +} diff --git a/src/org/minima/system/network/base/ssz/BitvectorImpl.java b/src/org/minima/system/network/base/ssz/BitvectorImpl.java new file mode 100644 index 000000000..59bd26d0a --- /dev/null +++ b/src/org/minima/system/network/base/ssz/BitvectorImpl.java @@ -0,0 +1,143 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkElementIndex; +import static java.util.stream.Collectors.toList; +import static org.minima.system.network.base.ssz.TreeUtil.bitsCeilToBytes; + +import com.google.common.base.Objects; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; +import java.util.stream.IntStream; +import org.apache.tuweni.bytes.Bytes; + +class BitvectorImpl { + + public static BitvectorImpl fromBytes(Bytes bytes, int size) { + checkArgument( + bytes.size() == sszSerializationLength(size), + "Incorrect data size (%s) for Bitvector of size %s", + bytes.size(), + size); + BitSet bitset = new BitSet(size); + + for (int i = size - 1; i >= 0; i--) { + if (((bytes.get(i / 8) >>> (i % 8)) & 0x01) == 1) { + bitset.set(i); + } + } + + return new BitvectorImpl(bitset, size); + } + + public static int sszSerializationLength(final int size) { + return bitsCeilToBytes(size); + } + + private final BitSet data; + private final int size; + + private BitvectorImpl(BitSet bitSet, int size) { + this.data = bitSet; + this.size = size; + } + + public BitvectorImpl(int size) { + this.data = new BitSet(size); + this.size = size; + } + + public BitvectorImpl(int size, Iterable indicesToSet) { + this(size); + for (int i : indicesToSet) { + checkElementIndex(i, size); + data.set(i); + } + } + + public BitvectorImpl(int size, int... indicesToSet) { + this(size, Arrays.stream(indicesToSet).boxed().collect(toList())); + } + + public List getSetBitIndexes() { + return data.stream().boxed().collect(toList()); + } + + public BitvectorImpl withBit(int i) { + checkElementIndex(i, size); + BitSet newSet = (BitSet) data.clone(); + newSet.set(i); + return new BitvectorImpl(newSet, size); + } + + public int getBitCount() { + return data.cardinality(); + } + + public boolean getBit(int i) { + checkElementIndex(i, size); + return data.get(i); + } + + public int getSize() { + return size; + } + + public IntStream streamAllSetBits() { + return data.stream(); + } + + @SuppressWarnings("NarrowingCompoundAssignment") + public Bytes serialize() { + byte[] array = new byte[sszSerializationLength(size)]; + IntStream.range(0, size).forEach(i -> array[i / 8] |= ((data.get(i) ? 1 : 0) << (i % 8))); + return Bytes.wrap(array); + } + + public BitvectorImpl rightShift(int i) { + int length = this.getSize(); + BitSet newData = new BitSet(getSize()); + for (int j = 0; j < length - i; j++) { + if (this.getBit(j)) { + newData.set(j + i); + } + } + return new BitvectorImpl(newData, getSize()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BitvectorImpl)) return false; + BitvectorImpl bitvector = (BitvectorImpl) o; + return getSize() == bitvector.getSize() && Objects.equal(data, bitvector.data); + } + + @Override + public int hashCode() { + return Objects.hashCode(data, getSize()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < size; i++) { + sb.append(getBit(i) ? 1 : 0); + } + return sb.toString(); + } +} diff --git a/src/org/minima/system/network/base/ssz/BranchNode.java b/src/org/minima/system/network/base/ssz/BranchNode.java new file mode 100644 index 000000000..094554955 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/BranchNode.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.function.Function; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.jetbrains.annotations.NotNull; +import org.minima.system.network.base.ssz.GIndexUtil.NodeRelation; +import org.minima.system.network.base.ssz.TreeNodeImpl.BranchNodeImpl; +import org.minima.utils.Crypto; + +/** + * Branch node of a tree. This node type corresponds to the 'Commit' node in the spec: + * https://github.com/protolambda/eth-merkle-trees/blob/master/typing_partials.md#structure + */ +public interface BranchNode extends TreeNode { + + /** + * Creates a basic binary Branch node with left and right child + * + * @param left Non-null left child + * @param right Non-null right child + */ + static BranchNode create(TreeNode left, TreeNode right) { + checkNotNull(left); + checkNotNull(right); + return new BranchNodeImpl(left, right); + } + + /** + * Returns left child node. It can be either a default or non-default node. Note that both left + * and right child may be the same default instance + */ + @NotNull + TreeNode left(); + + /** + * Returns right child node. It can be either a default or non-default node. Note that both left + * and right child may be the same default instance + */ + @NotNull + TreeNode right(); + + /** + * Rebind 'sets' a new left/right child of this node. Rebind doesn't modify this instance but + * creates and returns a new one which contains a new assigned and old unmodified child + */ + BranchNode rebind(boolean left, TreeNode newNode); + + @Override + default Bytes32 hashTreeRoot() { + byte[] hash = (new Crypto()).hashSHA2(Bytes.concatenate(left().hashTreeRoot(), right().hashTreeRoot()).toArray()); + return Bytes32.wrap(hash); + //return Hash.sha2_256(Bytes.concatenate(left().hashTreeRoot(), right().hashTreeRoot())); + } + + @NotNull + @Override + default TreeNode get(long target) { + checkArgument(target >= 1, "Invalid index: %s", target); + if (GIndexUtil.gIdxIsSelf(target)) { + return this; + } else { + long relativeGIndex = GIndexUtil.gIdxGetRelativeGIndex(target, 1); + return GIndexUtil.gIdxGetChildIndex(target, 1) == 0 + ? left().get(relativeGIndex) + : right().get(relativeGIndex); + } + } + + @Override + default boolean iterate( + long thisGeneralizedIndex, long startGeneralizedIndex, TreeVisitor visitor) { + + if (GIndexUtil.gIdxCompare(thisGeneralizedIndex, startGeneralizedIndex) == NodeRelation.Left) { + return true; + } else { + return visitor.visit(this, thisGeneralizedIndex) + && left() + .iterate( + GIndexUtil.gIdxLeftGIndex(thisGeneralizedIndex), startGeneralizedIndex, visitor) + && right() + .iterate( + GIndexUtil.gIdxRightGIndex(thisGeneralizedIndex), startGeneralizedIndex, visitor); + } + } + + @Override + default TreeNode updated(long target, Function nodeUpdater) { + if (GIndexUtil.gIdxIsSelf(target)) { + return nodeUpdater.apply(this); + } else { + long relativeGIndex = GIndexUtil.gIdxGetRelativeGIndex(target, 1); + if (GIndexUtil.gIdxGetChildIndex(target, 1) == 0) { + TreeNode newLeftChild = left().updated(relativeGIndex, nodeUpdater); + return rebind(true, newLeftChild); + } else { + TreeNode newRightChild = right().updated(relativeGIndex, nodeUpdater); + return rebind(false, newRightChild); + } + } + } +} diff --git a/src/org/minima/system/network/base/ssz/Bytes4.java b/src/org/minima/system/network/base/ssz/Bytes4.java new file mode 100644 index 000000000..b8c824996 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/Bytes4.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; + +public class Bytes4 { + public static final int SIZE = 4; + + private Bytes bytes; + + public Bytes4(Bytes bytes) { + checkArgument(bytes.size() == 4, "Bytes4 should be 4 bytes, but was %s bytes.", bytes.size()); + this.bytes = bytes; + } + + public static Bytes4 fromHexString(String value) { + return new Bytes4(Bytes.fromHexString(value)); + } + + public String toHexString() { + return bytes.toHexString(); + } + + /** + * Left pad a {@link Bytes} value with zero bytes to create a {@link Bytes4}. + * + * @param value The bytes value pad. + * @return A {@link Bytes4} that exposes the left-padded bytes of {@code value}. + * @throws IllegalArgumentException if {@code value.size() > 4}. + */ + public static Bytes4 leftPad(Bytes value) { + checkNotNull(value); + if (value instanceof Bytes4) { + return (Bytes4) value; + } + checkArgument(value.size() <= 4, "Expected at most %s bytes but got %s", 4, value.size()); + MutableBytes result = MutableBytes.create(4); + value.copyTo(result, 4 - value.size()); + return new Bytes4(result); + } + + /** + * Right pad a {@link Bytes} value with zero bytes to create a {@link Bytes4}. + * + * @param value The bytes value pad. + * @return A {@link Bytes4} that exposes the right-padded bytes of {@code value}. + * @throws IllegalArgumentException if {@code value.size() > 4}. + */ + public static Bytes4 rightPad(Bytes value) { + checkNotNull(value); + if (value instanceof Bytes4) { + return (Bytes4) value; + } + checkArgument(value.size() <= 4, "Expected at most %s bytes but got %s", 4, value.size()); + MutableBytes result = MutableBytes.create(4); + value.copyTo(result, 0); + return new Bytes4(result); + } + + public Bytes getWrappedBytes() { + return bytes; + } + + public Bytes4 copy() { + return new Bytes4(bytes.copy()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Bytes4 bytes4 = (Bytes4) o; + return bytes.equals(bytes4.bytes); + } + + @Override + public int hashCode() { + return Objects.hash(bytes); + } + + public String toUnprefixedHexString() { + return bytes.toUnprefixedHexString(); + } + + @Override + public String toString() { + return bytes.toString(); + } +} diff --git a/src/org/minima/system/network/base/ssz/Cache.java b/src/org/minima/system/network/base/ssz/Cache.java new file mode 100644 index 000000000..894d4b2d4 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/Cache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Optional; +import java.util.function.Function; + +/*** Attention */ +// This source file comes from ./infrastructure/collections/src/main/java/tech/pegasys/teku/infrastructure/collections/cache/Cache.java +// and not ./ssz/src/main/java/tech/pegasys/teku/ssz/cache/Cache.java +// the only difference is the additional size() method which then needs to be overriden in all implementing classes. +/** + * Cache + * + * @param type of keys + * @param type of values + */ +public interface Cache { + /** + * Queries value from the cache. If it's not found there, fallback function is used to calculate + * value. After calculation result is put in cache and returned. + * + * @param key Key to query + * @param fallback Fallback function for calculation of the result in case of missed cache entry + * @return expected value result for provided key + */ + V get(K key, Function fallback); + + /** + * Optionally returns the value corresponding to the passed key is it's in the cache + */ + Optional getCached(K key); + + /** Creates independent copy of this Cache instance */ + Cache copy(); + + /** Creates independent copy of this Cache instance while possibly clearing this cache content */ + default Cache transfer() { + return copy(); + } + + /** Removes cache entry */ + void invalidate(K key); + + default void invalidateWithNewValue(K key, V newValue) { + invalidate(key); + get(key, k -> newValue); + } + + /** Clears all cached values */ + void clear(); + + /** Returns the current number of items in the cache */ + int size(); +} diff --git a/src/org/minima/system/network/base/ssz/Container3.java b/src/org/minima/system/network/base/ssz/Container3.java new file mode 100644 index 000000000..888bcb6b0 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/Container3.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.impl.AbstractSszImmutableContainer; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** Autogenerated by tech.pegasys.teku.ssz.backing.ContainersGenerator */ +public class Container3< + C extends Container3, + V0 extends SszData, + V1 extends SszData, + V2 extends SszData> + extends AbstractSszImmutableContainer { + + protected Container3(ContainerSchema3 schema) { + super(schema); + } + + protected Container3(ContainerSchema3 schema, TreeNode backingNode) { + super(schema, backingNode); + } + + protected Container3(ContainerSchema3 schema, V0 arg0, V1 arg1, V2 arg2) { + super(schema, arg0, arg1, arg2); + } + + protected V0 getField0() { + return getAny(0); + } + + protected V1 getField1() { + return getAny(1); + } + + protected V2 getField2() { + return getAny(2); + } +} diff --git a/src/org/minima/system/network/base/ssz/ContainerSchema3.java b/src/org/minima/system/network/base/ssz/ContainerSchema3.java new file mode 100644 index 000000000..3d9bb707e --- /dev/null +++ b/src/org/minima/system/network/base/ssz/ContainerSchema3.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.List; +import java.util.function.BiFunction; +// import tech.pegasys.teku.ssz.SszContainer; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** Autogenerated by tech.pegasys.teku.ssz.backing.ContainersGenerator */ +public abstract class ContainerSchema3< + C extends SszContainer, V0 extends SszData, V1 extends SszData, V2 extends SszData> + extends AbstractSszContainerSchema { + + public static + ContainerSchema3 create( + SszSchema fieldSchema0, + SszSchema fieldSchema1, + SszSchema fieldSchema2, + BiFunction, TreeNode, C> instanceCtor) { + return new ContainerSchema3<>(fieldSchema0, fieldSchema1, fieldSchema2) { + @Override + public C createFromBackingNode(TreeNode node) { + return instanceCtor.apply(this, node); + } + }; + } + + protected ContainerSchema3( + SszSchema fieldSchema0, SszSchema fieldSchema1, SszSchema fieldSchema2) { + + super(List.of(fieldSchema0, fieldSchema1, fieldSchema2)); + } + + protected ContainerSchema3( + String containerName, + NamedSchema fieldNamedSchema0, + NamedSchema fieldNamedSchema1, + NamedSchema fieldNamedSchema2) { + + super(containerName, List.of(fieldNamedSchema0, fieldNamedSchema1, fieldNamedSchema2)); + } + + @SuppressWarnings("unchecked") + public SszSchema getFieldSchema0() { + return (SszSchema) getChildSchema(0); + } + + @SuppressWarnings("unchecked") + public SszSchema getFieldSchema1() { + return (SszSchema) getChildSchema(1); + } + + @SuppressWarnings("unchecked") + public SszSchema getFieldSchema2() { + return (SszSchema) getChildSchema(2); + } +} diff --git a/src/org/minima/system/network/base/ssz/GIndexUtil.java b/src/org/minima/system/network/base/ssz/GIndexUtil.java new file mode 100644 index 000000000..33ef96e1d --- /dev/null +++ b/src/org/minima/system/network/base/ssz/GIndexUtil.java @@ -0,0 +1,311 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static java.lang.Integer.min; + +import com.google.common.annotations.VisibleForTesting; + +/** + * Util methods for binary tree generalized indexes manipulations See + * https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/ssz/merkle-proofs.md#generalized-merkle-tree-index + * for more info on generalized indexes + * + *

Here the general index is represented by long which is treated as unsigned uint64 + * Thus the only illegal generalized index value is 0 + */ +public class GIndexUtil { + + /** See {@link #gIdxCompare(long, long)} */ + public enum NodeRelation { + Left, + Right, + Successor, + Predecessor, + Same; + + /** gIdxCompare(idx1, idx2) == gIdxCompare(idx2, idx1).inverse() */ + public NodeRelation inverse() { + switch (this) { + case Left: + return Right; + case Right: + return Left; + case Predecessor: + return Successor; + case Successor: + return Predecessor; + case Same: + return Same; + default: + throw new IllegalArgumentException("Unknown: " + this); + } + } + } + + /** Maximal depth this generalized index implementation can handle */ + // with the depth 64 positive long would overflow and we don't want to handle it here + public static final int MAX_DEPTH = 63; + + /** + * The generalized index of either a root tree node or an index of a node relative to the node + * itself. Effectively this is 1L + */ + public static final long SELF_G_INDEX = 1; + + /** The generalized index of the left child. Effectively 0b10 */ + public static final long LEFT_CHILD_G_INDEX = gIdxLeftGIndex(SELF_G_INDEX); + + /** The generalized index of the right child. Effectively 0b11 */ + public static final long RIGHT_CHILD_G_INDEX = gIdxRightGIndex(SELF_G_INDEX); + + /** + * The generalized index (normally an index of non-existing node) of the leftmost possible node + * Effectively this is {@link Long#MIN_VALUE} or 0b10000...000L in binary form + */ + static final long LEFTMOST_G_INDEX = gIdxLeftmostFrom(SELF_G_INDEX); + /** + * The generalized index (normally an index of non-existing node) of the rightmost possible node + * Effectively this is -1L or 0b11111...111L in binary form + */ + static final long RIGHTMOST_G_INDEX = gIdxRightmostFrom(SELF_G_INDEX); + + /** + * Indicates that a relative generalized index refers to the node itself + * + * @see #SELF_G_INDEX + */ + public static boolean gIdxIsSelf(long generalizedIndex) { + checkGIndex(generalizedIndex); + return generalizedIndex == SELF_G_INDEX; + } + + /** + * Indicates how the node with generalized index idx1 relates to the node with + * generalized index idx2: + * + *

    + *
  • {@link NodeRelation#Left}: idx1 is to the left of idx2 + *
  • {@link NodeRelation#Right}: idx1 is to the right of idx2 + *
  • {@link NodeRelation#Successor}: idx1 is the successor of idx2 + *
  • {@link NodeRelation#Predecessor}: idx1 is the predecessor of idx2 + *
  • {@link NodeRelation#Same}: idx1 is equal to idx2 + *
+ */ + public static NodeRelation gIdxCompare(long idx1, long idx2) { + checkGIndex(idx1); + checkGIndex(idx2); + long anchor1 = Long.highestOneBit(idx1); + long anchor2 = Long.highestOneBit(idx2); + int depth1 = Long.bitCount(anchor1 - 1); + int depth2 = Long.bitCount(anchor2 - 1); + int minDepth = min(depth1, depth2); + long minDepthIdx1 = idx1 >>> (depth1 - minDepth); + long minDepthIdx2 = idx2 >>> (depth2 - minDepth); + if (minDepthIdx1 == minDepthIdx2) { + if (depth1 < depth2) { + return NodeRelation.Predecessor; + } else if (depth1 > depth2) { + return NodeRelation.Successor; + } else { + return NodeRelation.Same; + } + } else { + if (minDepthIdx1 < minDepthIdx2) { + return NodeRelation.Left; + } else { + return NodeRelation.Right; + } + } + } + + /** + * Returns the depth of the node denoted by the supplied generalized index. E.g. the depth of the + * {@link #SELF_G_INDEX} would be 0 + */ + public static int gIdxGetDepth(long generalizedIndex) { + checkGIndex(generalizedIndex); + long anchor = Long.highestOneBit(generalizedIndex); + return Long.bitCount(anchor - 1); + } + + /** + * Returns the generalized index of the left child of the node with specified generalized index + * E.g. the result when passing {@link #SELF_G_INDEX} would be 10 + */ + public static long gIdxLeftGIndex(long generalizedIndex) { + return gIdxChildGIndex(generalizedIndex, 0, 1); + } + + /** + * Returns the generalized index of the right child of the node with specified generalized index + * E.g. the result when passing {@link #SELF_G_INDEX} would be 11 + */ + public static long gIdxRightGIndex(long generalizedIndex) { + return gIdxChildGIndex(generalizedIndex, 1, 1); + } + + /** + * More generic variant of methods {@link #gIdxLeftGIndex(long)} {@link #gIdxRightGIndex(long)} + * Calculates the generalized index of a node's childIdx successor at depth + * childDepth (depth relative to the original node). Note that childIdx is not + * the generalized index but index number of child. + * + *

For example: + * + *

    + *
  • gIdxChildGIndex(SELF_G_INDEX, 0, 2) == 100 + *
  • gIdxChildGIndex(SELF_G_INDEX, 1, 2) == 101 + *
  • gIdxChildGIndex(SELF_G_INDEX, 2, 2) == 110 + *
  • gIdxChildGIndex(SELF_G_INDEX, 3, 2) == 111 + *
  • gIdxChildGIndex(SELF_G_INDEX, 4, 2) is invalid cause there are just 4 successors + * at depth 2 + *
  • gIdxChildGIndex(anyIndex, 0, 1) == gIdxLeftGIndex(anyIndex) + *
  • gIdxChildGIndex(anyIndex, 1, 1) == gIdxRightGIndex(anyIndex) + *
+ */ + public static long gIdxChildGIndex(long generalizedIndex, long childIdx, int childDepth) { + checkGIndex(generalizedIndex); + assert childDepth >= 0 && childDepth <= MAX_DEPTH; + assert childIdx >= 0 && childIdx < (1L << childDepth); + assert gIdxGetDepth(generalizedIndex) + childDepth <= MAX_DEPTH; + return (generalizedIndex << childDepth) | childIdx; + } + + /** + * Compose absolute generalized index, where childGeneralizedIndex is relative to the + * node at parentGeneralizedIndex + * + *

For example: + * + *

    + *
  • gIdxCompose(0b1111, 0b1000) == 0b1111000 + *
  • gIdxCompose(0b1000, 0b1111) == 0b1000111 + *
+ */ + public static long gIdxCompose(long parentGeneralizedIndex, long childGeneralizedIndex) { + checkGIndex(parentGeneralizedIndex); + checkGIndex(childGeneralizedIndex); + assert gIdxGetDepth(parentGeneralizedIndex) + gIdxGetDepth(childGeneralizedIndex) <= MAX_DEPTH; + + long childAnchor = Long.highestOneBit(childGeneralizedIndex); + int childDepth = Long.bitCount(childAnchor - 1); + return (parentGeneralizedIndex << childDepth) | (childGeneralizedIndex ^ childAnchor); + } + + /** + * Returns the generalized index (normally an index of non-existing node) of the leftmost possible + * successor of this node + * + *

For example: + * + *

    + *
  • gIdxLeftmostFrom(0b1100) == 0b110000000...00L + *
  • gIdxLeftmostFrom(0b1101) == 0b110100000...00L + *
+ */ + public static long gIdxLeftmostFrom(long fromGeneralizedIndex) { + checkGIndex(fromGeneralizedIndex); + long highestOneBit = Long.highestOneBit(fromGeneralizedIndex); + if (highestOneBit < 0) { + return fromGeneralizedIndex; + } else { + int nodeDepth = Long.bitCount(highestOneBit - 1); + return fromGeneralizedIndex << (MAX_DEPTH - nodeDepth); + } + } + + /** + * Returns the generalized index (normally an index of non-existing node) of the rightmost + * possible successor of this node + * + *

For example: + * + *

    + *
  • gIdxRightmostFrom(0b1100) == 0b110011111...11L + *
  • gIdxRightmostFrom(0b1101) == 0b110111111...11L + *
+ */ + public static long gIdxRightmostFrom(long fromGeneralizedIndex) { + checkGIndex(fromGeneralizedIndex); + long highestOneBit = Long.highestOneBit(fromGeneralizedIndex); + if (highestOneBit < 0) { + return fromGeneralizedIndex; + } else { + int nodeDepth = Long.bitCount(highestOneBit - 1); + int shiftN = MAX_DEPTH - nodeDepth; + return (fromGeneralizedIndex << shiftN) | ((1L << shiftN) - 1); + } + } + + /** + * Returns the index number (not a generalized index) of a node at depth childDepth + * which is a predecessor of or equal to the node at generalizedIndex + * + *

For example: + * + *

    + *
  • gIdxGetChildIndex(LEFTMOST_G_INDEX, anyDepth) == 0 + *
  • gIdxGetChildIndex(0b1100, 2) == 2 + *
  • gIdxGetChildIndex(0b1101, 2) == 2 + *
  • gIdxGetChildIndex(0b1110, 2) == 3 + *
  • gIdxGetChildIndex(0b1111, 2) == 3 + *
  • gIdxGetChildIndex(0b11, 2) call would be invalid cause node with index 0b11 + * is at depth 1 + *
+ */ + public static int gIdxGetChildIndex(long generalizedIndex, int childDepth) { + checkGIndex(generalizedIndex); + assert childDepth >= 0 && childDepth <= MAX_DEPTH; + + long anchor = Long.highestOneBit(generalizedIndex); + int indexBitCount = Long.bitCount(anchor - 1); + assert indexBitCount >= childDepth; + long generalizedIndexWithoutAnchor = generalizedIndex ^ anchor; + return (int) (generalizedIndexWithoutAnchor >>> (indexBitCount - childDepth)); + } + + /** + * Returns the generalized index of the node at generalizedIndex relative to its + * predecessor at depth childDepth For example: + * + *
    + *
  • gIdxGetRelativeGIndex(0b1100, 2) == 0b10 + *
  • gIdxGetChildIndex(0b1101, 2) == 0b11 + *
  • gIdxGetChildIndex(0b1110, 2) == 0b10 + *
  • gIdxGetChildIndex(0b1111, 3) == SELF_G_INDEX + *
  • gIdxGetChildIndex(0b11, 2) call would be invalid cause node with index 0b11 + * is at depth 1 + *
+ */ + public static long gIdxGetRelativeGIndex(long generalizedIndex, int childDepth) { + checkGIndex(generalizedIndex); + assert childDepth >= 0 && childDepth <= MAX_DEPTH; + + long anchor = Long.highestOneBit(generalizedIndex); + long pivot = anchor >>> childDepth; + assert pivot != 0; + return (generalizedIndex & (pivot - 1)) | pivot; + } + + @VisibleForTesting + static long gIdxGetParent(long generalizedIndex) { + checkGIndex(generalizedIndex); + return generalizedIndex >>> 1; + } + + private static void checkGIndex(long index) { + assert index != 0; + } +} diff --git a/src/org/minima/system/network/base/ssz/IntCache.java b/src/org/minima/system/network/base/ssz/IntCache.java new file mode 100644 index 000000000..2774f3750 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/IntCache.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Function; +import java.util.function.IntFunction; + +/** + * Optimized int keys cache. Eliminate int boxing/unboxing + * + * @param type of values + */ +public interface IntCache extends Cache { + + @SuppressWarnings("unchecked") + static IntCache noop() { + return (IntCache) NoopIntCache.INSTANCE; + } + + /** + * Queries value from the cache. If it's not found there, fallback function is used to calculate + * value. After calculation result is put in cache and returned. + * + * @param key Key to query + * @param fallback Fallback function for calculation of the result in case of missed cache entry + * @return expected value result for provided key + */ + V getInt(int key, IntFunction fallback); + + @Override + default V get(Integer key, Function fallback) { + return getInt(key, value -> fallback.apply(key)); + } + + @Override + IntCache copy(); + + @Override + default IntCache transfer() { + return copy(); + } + + /** Removes cache entry */ + void invalidateInt(int key); + + @Override + default void invalidate(Integer key) { + invalidateInt(key); + } + + default void invalidateWithNewValueInt(int key, V newValue) { + invalidateInt(key); + getInt(key, k -> newValue); + } + + @Override + default void invalidateWithNewValue(Integer key, V newValue) { + invalidateWithNewValueInt(key, newValue); + } + + /** Clears all cached values */ + @Override + void clear(); + + + /** Returns the current number of items in the cache */ + @Override + int size(); +} diff --git a/src/org/minima/system/network/base/ssz/InvalidValueSchemaException.java b/src/org/minima/system/network/base/ssz/InvalidValueSchemaException.java new file mode 100644 index 000000000..b8880dbc1 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/InvalidValueSchemaException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +public class InvalidValueSchemaException extends RuntimeException { + + public InvalidValueSchemaException(String message) { + super(message); + } +} diff --git a/src/org/minima/system/network/base/ssz/LeafDataNode.java b/src/org/minima/system/network/base/ssz/LeafDataNode.java new file mode 100644 index 000000000..66d167827 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/LeafDataNode.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; + +/** Represents a tree node which can supply its leaves data */ +public interface LeafDataNode extends TreeNode { + + /** + * Returns the merged leaf data of this node's leaf descendants (see {@link SszSuperNode} for + * example) or just a leaf data if this node represents a single leaf (see {@link + * LeafNode#getData()} + */ + Bytes getData(); +} diff --git a/src/org/minima/system/network/base/ssz/LeafNode.java b/src/org/minima/system/network/base/ssz/LeafNode.java new file mode 100644 index 000000000..feabeb15c --- /dev/null +++ b/src/org/minima/system/network/base/ssz/LeafNode.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.function.Function; +import java.util.stream.IntStream; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.jetbrains.annotations.NotNull; +import org.minima.system.network.base.ssz.GIndexUtil.NodeRelation; +//import tech.pegasys.teku.ssz.tree.TreeNodeImpl.LeafNodeImpl; +//import tech.pegasys.teku.ssz.tree.TreeUtil.ZeroLeafNode; +import org.minima.system.network.base.ssz.TreeUtil.ZeroLeafNode; +import org.minima.system.network.base.ssz.TreeNodeImpl.LeafNodeImpl; + +/** + * Leaf node of a tree which contains 'bytes32' value. This node type corresponds to the 'Root' node + * in the spec: + * https://github.com/protolambda/eth-merkle-trees/blob/master/typing_partials.md#structure + */ +public interface LeafNode extends TreeNode, LeafDataNode { + + int MAX_BYTE_SIZE = 32; + int MAX_BIT_SIZE = MAX_BYTE_SIZE * 8; + + /** + * Pre-allocated leaf nodes with the data consisting of 0, 1, 2, ..., 32 zero bytes Worth to + * mention that {@link TreeNode#hashTreeRoot()} for all these nodes return the same value {@link + * Bytes32#ZERO} + */ + LeafNode[] ZERO_LEAVES = + IntStream.rangeClosed(0, MAX_BYTE_SIZE).mapToObj(ZeroLeafNode::new).toArray(LeafNode[]::new); + + /** The {@link LeafNode} with empty data */ + LeafNode EMPTY_LEAF = ZERO_LEAVES[0]; + + /** Creates a basic Leaf node instance with the data <= 32 bytes */ + static LeafNode create(Bytes data) { + return new LeafNodeImpl(data); + } + + /** + * Returns only data bytes without zero right padding (unlike {@link #hashTreeRoot()}) E.g. if a + * {@code LeafNode} corresponds to a contained UInt64 field, then {@code getData()} returns only 8 + * bytes corresponding to the field value If a {@code Vector[Byte, 48]} is stored across two + * {@code LeafNode}s then the second node {@code getData} would return just the last 16 bytes of + * the vector (while {@link #hashTreeRoot()} would return zero padded 32 bytes) + */ + @Override + Bytes getData(); + + /** LeafNode hash tree root is the leaf data right padded to 32 bytes */ + @Override + default Bytes32 hashTreeRoot() { + return Bytes32.rightPad(getData()); + } + + /** + * @param target generalized index. Should be equal to 1 + * @return this node if 'target' == 1 + * @throws IllegalArgumentException if 'target' != 1 + */ + @NotNull + @Override + default TreeNode get(long target) { + checkArgument(target == 1, "Invalid root index: %s", target); + return this; + } + + @Override + default boolean iterate( + long thisGeneralizedIndex, long startGeneralizedIndex, TreeVisitor visitor) { + if (GIndexUtil.gIdxCompare(thisGeneralizedIndex, startGeneralizedIndex) == NodeRelation.Left) { + return true; + } else { + return visitor.visit(this, thisGeneralizedIndex); + } + } + + @Override + default TreeNode updated(long target, Function nodeUpdater) { + checkArgument(target == 1, "Invalid root index: %s", target); + return nodeUpdater.apply(this); + } +} diff --git a/src/org/minima/system/network/base/ssz/Merkleizable.java b/src/org/minima/system/network/base/ssz/Merkleizable.java new file mode 100644 index 000000000..ebe203b72 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/Merkleizable.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; + +/** + * Returns `hash_tree_root` conforming to SSZ spec: + * https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/simple-serialize.md#merkleization + */ +public interface Merkleizable { + + /** + * Returns `hash_tree_root` conforming to SSZ spec: + * https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/simple-serialize.md#merkleization + */ + Bytes32 hashTreeRoot(); +} diff --git a/src/org/minima/system/network/base/ssz/NoopIntCache.java b/src/org/minima/system/network/base/ssz/NoopIntCache.java new file mode 100644 index 000000000..b8122e86b --- /dev/null +++ b/src/org/minima/system/network/base/ssz/NoopIntCache.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Optional; +import java.util.function.IntFunction; + +public class NoopIntCache implements IntCache { + + static final IntCache INSTANCE = new NoopIntCache<>(); + + private NoopIntCache() {} + + @Override + public V getInt(int key, IntFunction fallback) { + return fallback.apply(key); + } + + @Override + public IntCache copy() { + return new NoopIntCache<>(); + } + + @Override + public void invalidateInt(int key) {} + + @Override + public void clear() {} + + @Override + public Optional getCached(Integer key) { + return Optional.empty(); + } + + + /** Returns the current number of items in the cache */ + @Override + public int size() { return INSTANCE.size(); } +} diff --git a/src/org/minima/system/network/base/ssz/SchemaUtils.java b/src/org/minima/system/network/base/ssz/SchemaUtils.java new file mode 100644 index 000000000..4f5785c74 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SchemaUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +class SchemaUtils { + + public static TreeNode createTreeFromBytes(Bytes bytes, int treeDepth) { + return TreeUtil.createTree( + split(bytes, LeafNode.MAX_BYTE_SIZE).stream() + .map(LeafNode::create) + .collect(Collectors.toList()), + treeDepth); + } + + public static List split(Bytes bytes, int chunkSize) { + List ret = new ArrayList<>(); + int off = 0; + int size = bytes.size(); + while (off < size) { + Bytes leafData = bytes.slice(off, Integer.min(chunkSize, size - off)); + ret.add(leafData); + off += chunkSize; + } + return ret; + } +} diff --git a/src/org/minima/system/network/base/ssz/SimpleOffsetSerializable.java b/src/org/minima/system/network/base/ssz/SimpleOffsetSerializable.java new file mode 100644 index 000000000..da865dcba --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SimpleOffsetSerializable.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +//import tech.pegasys.teku.ssz.sos.SszWriter; + +/** + * Represent the data which can be SSZ serialized + * + *

SSZ spec: https://github.com/protolambda/eth2.0-ssz + */ +public interface SimpleOffsetSerializable { + + /** Returns this data SSZ serialization */ + Bytes sszSerialize(); + + /** + * SSZ serializes this data to supplied {@code writer} + * + * @return number of bytes written + */ + int sszSerialize(SszWriter writer); +} diff --git a/src/org/minima/system/network/base/ssz/SimpleSszReader.java b/src/org/minima/system/network/base/ssz/SimpleSszReader.java new file mode 100644 index 000000000..fe577f89a --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SimpleSszReader.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; + +public class SimpleSszReader implements SszReader { + + private final Bytes bytes; + protected int offset = 0; + + public SimpleSszReader(Bytes bytes) { + this.bytes = bytes; + } + + @Override + public int getAvailableBytes() { + return bytes.size() - offset; + } + + @Override + public SszReader slice(int size) { + checkIfAvailable(size); + SimpleSszReader ret = new SimpleSszReader(bytes.slice(offset, size)); + offset += size; + return ret; + } + + @Override + public Bytes read(int length) { + checkIfAvailable(length); + Bytes ret = bytes.slice(offset, length); + offset += length; + return ret; + } + + private void checkIfAvailable(int size) { + if (getAvailableBytes() < size) { + throw new SszDeserializeException("Invalid SSZ: trying to read more bytes than available"); + } + } + + @Override + public void close() { + if (getAvailableBytes() > 0) { + throw new SszDeserializeException("Invalid SSZ: unread bytes remain: " + getAvailableBytes()); + } + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBit.java b/src/org/minima/system/network/base/ssz/SszBit.java new file mode 100644 index 000000000..a29ec7e6a --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBit.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.impl.AbstractSszPrimitive; +//import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; + +public class SszBit extends AbstractSszPrimitive { + + private static final SszBit TRUE_VIEW = new SszBit(true); + private static final SszBit FALSE_VIEW = new SszBit(false); + + public static SszBit of(boolean value) { + return value ? TRUE_VIEW : FALSE_VIEW; + } + + private SszBit(Boolean value) { + super(value, SszPrimitiveSchemas.BIT_SCHEMA); + } + + @Override + @SuppressWarnings("ReferenceEquality") + public String toString() { + return this == TRUE_VIEW ? "1" : "0"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBitvector.java b/src/org/minima/system/network/base/ssz/SszBitvector.java new file mode 100644 index 000000000..d2e5b0b2e --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBitvector.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.List; +import java.util.stream.IntStream; +// import tech.pegasys.teku.ssz.primitive.SszBit; +// import tech.pegasys.teku.ssz.schema.collections.SszBitvectorSchema; + +/** Specialized implementation of {@code SszVector} */ +public interface SszBitvector extends SszPrimitiveVector { + + @Override + default SszMutablePrimitiveVector createWritableCopy() { + throw new UnsupportedOperationException("SszBitlist is immutable structure"); + } + + @Override + default boolean isWritableSupported() { + return false; + } + + @Override + SszBitvectorSchema getSchema(); + + // Bitlist methods + + SszBitvector withBit(int i); + + /** Returns individual bit value */ + boolean getBit(int i); + + /** Returns the number of bits set to {@code true} in this {@code SszBitlist}. */ + int getBitCount(); + + /** Returns new vector with bits shifted to the right by {@code n} positions */ + SszBitvector rightShift(int n); + + /** Returns indexes of all bits set in this {@link SszBitvector} */ + List getAllSetBits(); + + /** Streams indexes of all bits set in this {@link SszBitvector} */ + default IntStream streamAllSetBits() { + return getAllSetBits().stream().mapToInt(i -> i); + } + + @Override + default Boolean getElement(int index) { + return getBit(index); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBitvectorImpl.java b/src/org/minima/system/network/base/ssz/SszBitvectorImpl.java new file mode 100644 index 000000000..2b1317905 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBitvectorImpl.java @@ -0,0 +1,105 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; +import java.util.stream.Collectors; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.collections.SszBitvector; +// import tech.pegasys.teku.ssz.collections.SszMutablePrimitiveVector; +// import tech.pegasys.teku.ssz.impl.SszVectorImpl; +// import tech.pegasys.teku.ssz.primitive.SszBit; +// import tech.pegasys.teku.ssz.schema.SszVectorSchema; +// import tech.pegasys.teku.ssz.schema.collections.SszBitvectorSchema; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszBitvectorImpl extends SszVectorImpl implements SszBitvector { + + public static SszBitvectorImpl ofBits(SszBitvectorSchema schema, int... bits) { + return new SszBitvectorImpl(schema, new BitvectorImpl(schema.getLength(), bits)); + } + + private final BitvectorImpl value; + + public SszBitvectorImpl(SszVectorSchema schema, TreeNode backingNode) { + super(schema, backingNode); + value = BitvectorImpl.fromBytes(sszSerialize(), size()); + } + + public SszBitvectorImpl(SszBitvectorSchema schema, BitvectorImpl value) { + super(schema, () -> schema.sszDeserializeTree(SszReader.fromBytes(value.serialize()))); + checkNotNull(value); + this.value = value; + } + + @SuppressWarnings("unchecked") + @Override + public SszBitvectorSchema getSchema() { + return (SszBitvectorSchema) super.getSchema(); + } + + @Override + protected IntCache createCache() { + // BitvectorImpl is far more effective cache than caching individual bits + return IntCache.noop(); + } + + @Override + public boolean getBit(int i) { + return value.getBit(i); + } + + @Override + public int getBitCount() { + return value.getBitCount(); + } + + @Override + public SszBitvector rightShift(int n) { + return new SszBitvectorImpl(getSchema(), value.rightShift(n)); + } + + @Override + public List getAllSetBits() { + return value.streamAllSetBits().boxed().collect(Collectors.toList()); + } + + @Override + public SszBitvector withBit(int i) { + return new SszBitvectorImpl(getSchema(), value.withBit(i)); + } + + @Override + protected int sizeImpl() { + return getSchema().getLength(); + } + + @Override + public SszMutablePrimitiveVector createWritableCopy() { + throw new UnsupportedOperationException("SszBitlist is immutable structure"); + } + + @Override + public boolean isWritableSupported() { + return false; + } + + @Override + public String toString() { + return "SszBitvector{size=" + this.size() + ", " + value.toString() + "}"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBitvectorSchema.java b/src/org/minima/system/network/base/ssz/SszBitvectorSchema.java new file mode 100644 index 000000000..4bf61831f --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBitvectorSchema.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.stream.StreamSupport; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.collections.SszBitvector; +// import tech.pegasys.teku.ssz.primitive.SszBit; +// import tech.pegasys.teku.ssz.schema.collections.impl.SszBitvectorSchemaImpl; + +public interface SszBitvectorSchema + extends SszPrimitiveVectorSchema { + + static SszBitvectorSchema create(long length) { + return new SszBitvectorSchemaImpl(length); + } + + SszBitvectorT ofBits(int... setBitIndexes); + + default SszBitvectorT fromBytes(Bytes bivectorBytes) { + return sszDeserialize(bivectorBytes); + } + + default SszBitvectorT ofBits(Iterable setBitIndexes) { + int[] indexesArray = + StreamSupport.stream(setBitIndexes.spliterator(), false).mapToInt(i -> i).toArray(); + return ofBits(indexesArray); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBitvectorSchemaImpl.java b/src/org/minima/system/network/base/ssz/SszBitvectorSchemaImpl.java new file mode 100644 index 000000000..579a480a8 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBitvectorSchemaImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.List; +import java.util.stream.IntStream; +// import tech.pegasys.teku.ssz.collections.SszBitvector; +// import tech.pegasys.teku.ssz.collections.impl.SszBitvectorImpl; +// import tech.pegasys.teku.ssz.primitive.SszBit; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.schema.collections.SszBitvectorSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszVectorSchema; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +public class SszBitvectorSchemaImpl extends AbstractSszVectorSchema + implements SszBitvectorSchema { + + public SszBitvectorSchemaImpl(long length) { + super(SszPrimitiveSchemas.BIT_SCHEMA, length); + checkArgument(length > 0, "Invalid Bitlist length"); + } + + @Override + public SszBitvector createFromBackingNode(TreeNode node) { + return new SszBitvectorImpl(this, node); + } + + @Override + public SszBitvector ofBits(int... setBitIndexes) { + return SszBitvectorImpl.ofBits(this, setBitIndexes); + } + + @Override + public SszBitvector createFromElements(List elements) { + return ofBits(IntStream.range(0, elements.size()).filter(i -> elements.get(i).get()).toArray()); + } + + @Override + public int sszSerializeTree(TreeNode node, SszWriter writer) { + return sszSerializeVector(node, writer, getLength()); + } + + @Override + public TreeNode sszDeserializeTree(SszReader reader) { + checkSsz( + reader.getAvailableBytes() == TreeUtil.bitsCeilToBytes(getLength()), + "SSZ length doesn't match Bitvector size"); + + DeserializedData data = sszDeserializeVector(reader); + if (getLength() % 8 > 0) { + // for BitVector we need to check that all 'unused' bits in the last byte are 0 + int usedBitCount = getLength() % 8; + if (data.getLastSszByte().orElseThrow() >>> usedBitCount != 0) { + throw new SszDeserializeException("Invalid Bitvector ssz: trailing bits are not 0"); + } + } + return data.getDataTree(); + } + + @Override + public String toString() { + return "Bitvector[" + getMaxLength() + "]"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByte.java b/src/org/minima/system/network/base/ssz/SszByte.java new file mode 100644 index 000000000..5e70157f5 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByte.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.impl.AbstractSszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; + +public class SszByte extends AbstractSszPrimitive { + + public static SszByte of(int value) { + return new SszByte((byte) value); + } + + public static SszByte of(byte value) { + return new SszByte(value); + } + + private SszByte(Byte value) { + super(value, SszPrimitiveSchemas.BYTE_SCHEMA); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByteArrayWriter.java b/src/org/minima/system/network/base/ssz/SszByteArrayWriter.java new file mode 100644 index 000000000..c3e7b5a50 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByteArrayWriter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; + +public class SszByteArrayWriter implements SszWriter { + private final byte[] bytes; + private int size = 0; + + public SszByteArrayWriter(int maxSize) { + bytes = new byte[maxSize]; + } + + @Override + public void write(byte[] bytes, int offset, int length) { + System.arraycopy(bytes, offset, this.bytes, this.size, length); + this.size += length; + } + + public byte[] getBytesArray() { + return bytes; + } + + public int getLength() { + return size; + } + + public Bytes toBytes() { + return Bytes.wrap(bytes, 0, size); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByteVector.java b/src/org/minima/system/network/base/ssz/SszByteVector.java new file mode 100644 index 000000000..26c6102e7 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByteVector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.primitive.SszByte; +// import tech.pegasys.teku.ssz.schema.collections.SszByteVectorSchema; + +public interface SszByteVector extends SszPrimitiveVector { + + static SszByteVector fromBytes(Bytes byteVector) { + return SszByteVectorSchema.create(byteVector.size()).fromBytes(byteVector); + } + + static Bytes32 computeHashTreeRoot(Bytes byteVector) { + return fromBytes(byteVector).hashTreeRoot(); + } + + default Bytes getBytes() { + byte[] data = new byte[size()]; + for (int i = 0; i < size(); i++) { + data[i] = getElement(i); + } + return Bytes.wrap(data); + } + + @Override + default SszMutablePrimitiveVector createWritableCopy() { + throw new UnsupportedOperationException("Not supported here"); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByteVectorImpl.java b/src/org/minima/system/network/base/ssz/SszByteVectorImpl.java new file mode 100644 index 000000000..9e482d255 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByteVectorImpl.java @@ -0,0 +1,70 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.collections.SszByteVector; +// import tech.pegasys.teku.ssz.collections.SszMutablePrimitiveVector; +// import tech.pegasys.teku.ssz.primitive.SszByte; +// import tech.pegasys.teku.ssz.schema.collections.SszByteVectorSchema; +// import tech.pegasys.teku.ssz.schema.collections.impl.SszByteVectorSchemaImpl; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszByteVectorImpl extends SszPrimitiveVectorImpl + implements SszByteVector { + + private final Bytes data; + + public SszByteVectorImpl(SszByteVectorSchema schema, Bytes bytes) { + super(schema, () -> SszByteVectorSchemaImpl.fromBytesToTree(schema, bytes)); + this.data = bytes; + } + + public SszByteVectorImpl(SszByteVectorSchema schema, TreeNode backingTree) { + super(schema, backingTree); + this.data = SszByteVectorSchemaImpl.fromTreeToBytes(schema, backingTree); + } + + @Override + public Bytes getBytes() { + return data; + } + + @Override + protected IntCache createCache() { + // caching with Bytes in this class + return IntCache.noop(); + } + + @Override + public SszByteVectorSchemaImpl getSchema() { + return (SszByteVectorSchemaImpl) super.getSchema(); + } + + @Override + public SszMutablePrimitiveVector createWritableCopy() { + throw new UnsupportedOperationException("SszBitlist is immutable structure"); + } + + @Override + public boolean isWritableSupported() { + return false; + } + + @Override + public String toString() { + return "SszByteVector{" + data + '}'; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByteVectorSchema.java b/src/org/minima/system/network/base/ssz/SszByteVectorSchema.java new file mode 100644 index 000000000..481999117 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByteVectorSchema.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.collections.SszByteVector; +// import tech.pegasys.teku.ssz.primitive.SszByte; +// import tech.pegasys.teku.ssz.schema.collections.impl.SszByteVectorSchemaImpl; + +public interface SszByteVectorSchema + extends SszPrimitiveVectorSchema { + + SszVectorT fromBytes(Bytes bytes); + + static SszByteVectorSchema create(int length) { + return new SszByteVectorSchemaImpl<>(length); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszByteVectorSchemaImpl.java b/src/org/minima/system/network/base/ssz/SszByteVectorSchemaImpl.java new file mode 100644 index 000000000..602177028 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszByteVectorSchemaImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.List; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.collections.SszByteVector; +// import tech.pegasys.teku.ssz.collections.impl.SszByteVectorImpl; +// import tech.pegasys.teku.ssz.primitive.SszByte; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.schema.collections.SszByteVectorSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszVectorSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +public class SszByteVectorSchemaImpl + extends AbstractSszVectorSchema + implements SszByteVectorSchema { + + public SszByteVectorSchemaImpl(long vectorLength) { + super(SszPrimitiveSchemas.BYTE_SCHEMA, vectorLength); + } + + @Override + @SuppressWarnings("unchecked") + public SszVectorT createFromBackingNode(TreeNode node) { + return (SszVectorT) new SszByteVectorImpl(this, node); + } + + @Override + @SuppressWarnings("unchecked") + public SszVectorT fromBytes(Bytes bytes) { + return (SszVectorT) new SszByteVectorImpl(this, bytes); + } + + public static TreeNode fromBytesToTree(SszByteVectorSchema schema, Bytes bytes) { + checkArgument(bytes.size() == schema.getLength(), "Bytes size doesn't match vector length"); + return SchemaUtils.createTreeFromBytes(bytes, schema.treeDepth()); + } + + public static Bytes fromTreeToBytes(SszByteVectorSchema schema, TreeNode tree) { + Bytes bytes = TreeUtil.concatenateLeavesData(tree); + checkArgument(bytes.size() == schema.getLength(), "Tree doesn't match vector schema"); + return bytes; + } + + @Override + public SszVectorT createFromElements(List elements) { + Bytes bytes = Bytes.of(elements.stream().mapToInt(sszByte -> 0xFF & sszByte.get()).toArray()); + return fromBytes(bytes); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes32.java b/src/org/minima/system/network/base/ssz/SszBytes32.java new file mode 100644 index 000000000..0ce481212 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes32.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.impl.AbstractSszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; + +public class SszBytes32 extends AbstractSszPrimitive { + + public static SszBytes32 of(Bytes32 val) { + return new SszBytes32(val); + } + + private SszBytes32(Bytes32 val) { + super(val, SszPrimitiveSchemas.BYTES32_SCHEMA); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes32Vector.java b/src/org/minima/system/network/base/ssz/SszBytes32Vector.java new file mode 100644 index 000000000..c7068db6f --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes32Vector.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; +//import tech.pegasys.teku.ssz.primitive.SszBytes32; + +public interface SszBytes32Vector extends SszPrimitiveVector { + + @Override + SszMutableBytes32Vector createWritableCopy(); +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes32VectorImpl.java b/src/org/minima/system/network/base/ssz/SszBytes32VectorImpl.java new file mode 100644 index 000000000..edd6609b7 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes32VectorImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Supplier; +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.collections.SszBytes32Vector; +// import tech.pegasys.teku.ssz.collections.SszMutableBytes32Vector; +// import tech.pegasys.teku.ssz.primitive.SszBytes32; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszBytes32VectorImpl extends SszPrimitiveVectorImpl + implements SszBytes32Vector { + + public SszBytes32VectorImpl(SszCompositeSchema schema, Supplier lazyBackingNode) { + super(schema, lazyBackingNode); + } + + public SszBytes32VectorImpl(SszCompositeSchema schema, TreeNode backingNode) { + super(schema, backingNode); + } + + @Override + public SszMutableBytes32Vector createWritableCopy() { + return new SszMutableBytes32VectorImpl(this); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes32VectorSchema.java b/src/org/minima/system/network/base/ssz/SszBytes32VectorSchema.java new file mode 100644 index 000000000..b68cb17c6 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes32VectorSchema.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.collections.SszBytes32Vector; +// import tech.pegasys.teku.ssz.primitive.SszBytes32; +// import tech.pegasys.teku.ssz.schema.collections.impl.SszBytes32VectorSchemaImpl; + +public interface SszBytes32VectorSchema + extends SszPrimitiveVectorSchema { + + static SszBytes32VectorSchema create(int length) { + return new SszBytes32VectorSchemaImpl<>(length); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes32VectorSchemaImpl.java b/src/org/minima/system/network/base/ssz/SszBytes32VectorSchemaImpl.java new file mode 100644 index 000000000..54f065683 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes32VectorSchemaImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.collections.SszBytes32Vector; +// import tech.pegasys.teku.ssz.collections.impl.SszBytes32VectorImpl; +// import tech.pegasys.teku.ssz.primitive.SszBytes32; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.schema.collections.SszBytes32VectorSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszVectorSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszBytes32VectorSchemaImpl + extends AbstractSszVectorSchema + implements SszBytes32VectorSchema { + + public SszBytes32VectorSchemaImpl(long vectorLength) { + super(SszPrimitiveSchemas.BYTES32_SCHEMA, vectorLength); + } + + @Override + @SuppressWarnings("unchecked") + public SszVectorT createFromBackingNode(TreeNode node) { + return (SszVectorT) new SszBytes32VectorImpl(this, node); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszBytes4.java b/src/org/minima/system/network/base/ssz/SszBytes4.java new file mode 100644 index 000000000..01bb49be3 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszBytes4.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.impl.AbstractSszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.type.Bytes4; + +public class SszBytes4 extends AbstractSszPrimitive { + + public static SszBytes4 of(Bytes4 val) { + return new SszBytes4(val); + } + + private SszBytes4(Bytes4 val) { + super(val, SszPrimitiveSchemas.BYTES4_SCHEMA); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszCollection.java b/src/org/minima/system/network/base/ssz/SszCollection.java new file mode 100644 index 000000000..fa0768e17 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszCollection.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.AbstractList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +//import tech.pegasys.teku.ssz.schema.SszCollectionSchema; + +public interface SszCollection + extends SszComposite, Iterable { + + default boolean isEmpty() { + return size() == 0; + } + + default List asList() { + return new AbstractList<>() { + @Override + public ElementT get(int index) { + return SszCollection.this.get(index); + } + + @Override + public int size() { + return SszCollection.this.size(); + } + }; + } + + @NotNull + @Override + default Iterator iterator() { + return asList().iterator(); + } + + default Stream stream() { + return asList().stream(); + } + + @Override + SszCollectionSchema getSchema(); +} diff --git a/src/org/minima/system/network/base/ssz/SszCollectionSchema.java b/src/org/minima/system/network/base/ssz/SszCollectionSchema.java new file mode 100644 index 000000000..dde2c6e31 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszCollectionSchema.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; +// import tech.pegasys.teku.ssz.SszCollection; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableComposite; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public interface SszCollectionSchema< + SszElementT extends SszData, SszCollectionT extends SszCollection> + extends SszCompositeSchema { + + SszSchema getElementSchema(); + + @SuppressWarnings("unchecked") + default SszCollectionT of(SszElementT... elements) { + return createFromElements(Arrays.asList(elements)); + } + + @SuppressWarnings("unchecked") + default SszCollectionT createFromElements(List elements) { + checkArgument(elements.size() <= getMaxLength(), "Too many elements for this collection type"); + SszMutableComposite writableCopy = getDefault().createWritableCopy(); + int idx = 0; + for (SszElementT element : elements) { + writableCopy.set(idx++, element); + } + return (SszCollectionT) writableCopy.commitChanges(); + } + + default TreeNode createTreeFromElements(List elements) { + // TODO: suboptimal + return createFromElements(elements).getBackingNode(); + } + + default Collector collector() { + return Collectors.collectingAndThen(Collectors.toList(), this::createFromElements); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszComposite.java b/src/org/minima/system/network/base/ssz/SszComposite.java new file mode 100644 index 000000000..707c954f6 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszComposite.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.schema.SszCompositeSchema; + +/** + * Represents composite immutable ssz structure which has descendant ssz structures + * + * @param the type of children {@link SszData} + */ +public interface SszComposite extends SszData { + + /** Returns number of children in this structure */ + default int size() { + return (int) getSchema().getMaxLength(); + } + + /** + * Returns the child at index + * + * @throws IndexOutOfBoundsException if index >= size() + */ + SszChildT get(int index); + + @Override + SszCompositeSchema getSchema(); + + @Override + SszMutableComposite createWritableCopy(); +} diff --git a/src/org/minima/system/network/base/ssz/SszCompositeSchema.java b/src/org/minima/system/network/base/ssz/SszCompositeSchema.java new file mode 100644 index 000000000..9d1b7b7d3 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszCompositeSchema.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszComposite; +// import tech.pegasys.teku.ssz.tree.GIndexUtil; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUtil; + +/** Abstract schema of {@link SszComposite} subclasses */ +public interface SszCompositeSchema> + extends SszSchema { + + /** + * Returns the maximum number of elements in ssz structures of this scheme. For structures with + * fixed number of children (like Containers and Vectors) their size should always be equal to + * maxLength + */ + long getMaxLength(); + + /** + * Returns the child schema at index. For homogeneous structures (like Vector, List) the returned + * schema is the same for any index For heterogeneous structures (like Container) each child has + * individual schema + * + * @throws IndexOutOfBoundsException if index >= getMaxLength + */ + SszSchema getChildSchema(int index); + + /** + * Return the number of elements that may be stored in a single tree node This value is 1 for all + * types except of packed basic lists/vectors + */ + default int getElementsPerChunk() { + return 1; + } + + /** + * Returns the maximum number of this ssz structure backed subtree 'leaf' nodes required to store + * maxLength elements + */ + default long maxChunks() { + return (getMaxLength() - 1) / getElementsPerChunk() + 1; + } + + /** + * Returns then number of chunks (i.e. leaf nodes) to store {@code elementCount} child elements + * Returns a number lower than {@code elementCode} only in case of packed basic types collection + */ + default int getChunks(int elementCount) { + return (elementCount - 1) / getElementsPerChunk() + 1; + } + + /** Returns the backed binary tree depth to store maxLength elements */ + default int treeDepth() { + return Long.bitCount(treeWidth() - 1); + } + + /** Returns the backed binary tree width to store maxLength elements */ + default long treeWidth() { + return TreeUtil.nextPowerOf2(maxChunks()); + } + + /** + * Returns binary backing tree generalized index corresponding to child element index + * + * @see TreeNode#get(long) + */ + default long getChildGeneralizedIndex(long elementIndex) { + return GIndexUtil.gIdxChildGIndex(GIndexUtil.SELF_G_INDEX, elementIndex, treeDepth()); + } + + @Override + default boolean isPrimitive() { + return false; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszContainer.java b/src/org/minima/system/network/base/ssz/SszContainer.java new file mode 100644 index 000000000..c0ce199fe --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszContainer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.schema.SszContainerSchema; + +/** + * Base class for immutable containers. Since containers are heterogeneous their generic child type + * is {@link SszData} + */ +public interface SszContainer extends SszComposite { + + @Override + SszContainerSchema getSchema(); + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + // container is heterogeneous by its nature so making unsafe cast here + // is more convenient and is not less safe + default C getAny(int index) { + return (C) get(index); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszContainerImpl.java b/src/org/minima/system/network/base/ssz/SszContainerImpl.java new file mode 100644 index 000000000..7d9142430 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszContainerImpl.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; +// import tech.pegasys.teku.ssz.SszContainer; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableContainer; +// import tech.pegasys.teku.ssz.cache.ArrayIntCache; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.schema.SszContainerSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszContainerImpl extends AbstractSszComposite implements SszContainer { + + public SszContainerImpl(SszContainerSchema type) { + this(type, type.getDefaultTree()); + } + + public SszContainerImpl(SszContainerSchema type, TreeNode backingNode) { + super(type, backingNode); + } + + public SszContainerImpl( + SszCompositeSchema type, TreeNode backingNode, IntCache cache) { + super(type, backingNode, cache); + } + + @Override + protected SszData getImpl(int index) { + SszCompositeSchema type = this.getSchema(); + TreeNode node = getBackingNode().get(type.getChildGeneralizedIndex(index)); + return type.getChildSchema(index).createFromBackingNode(node); + } + + @Override + public AbstractSszContainerSchema getSchema() { + return (AbstractSszContainerSchema) super.getSchema(); + } + + @Override + public SszMutableContainer createWritableCopy() { + return new SszMutableContainerImpl(this); + } + + @Override + protected int sizeImpl() { + return (int) this.getSchema().getMaxLength(); + } + + @Override + protected IntCache createCache() { + return new ArrayIntCache<>(size()); + } + + @Override + protected void checkIndex(int index) { + if (index >= size()) { + throw new IndexOutOfBoundsException( + "Invalid index " + index + " for container with size " + size()); + } + } + + @Override + public String toString() { + return this.getSchema().getContainerName() + + "{" + + IntStream.range(0, this.getSchema().getFieldsCount()) + .mapToObj(idx -> this.getSchema().getFieldNames().get(idx) + "=" + get(idx)) + .collect(Collectors.joining(", ")) + + "}"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszContainerSchema.java b/src/org/minima/system/network/base/ssz/SszContainerSchema.java new file mode 100644 index 000000000..b9f0160a6 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszContainerSchema.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.List; +import java.util.function.BiFunction; +// import tech.pegasys.teku.ssz.SszContainer; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema.NamedSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +import org.minima.system.network.base.ssz.AbstractSszContainerSchema.NamedSchema; + +/** + * {@link SszSchema} for an Ssz Container structure + * + * @param the type of actual container class + */ +public interface SszContainerSchema extends SszCompositeSchema { + + /** + * Creates a new {@link SszContainer} schema with specified field schemas and container instance + * constructor + */ + static SszContainerSchema create( + List> childrenSchemas, + BiFunction, TreeNode, C> instanceCtor) { + return new AbstractSszContainerSchema(childrenSchemas) { + @Override + public C createFromBackingNode(TreeNode node) { + return instanceCtor.apply(this, node); + } + }; + } + + static SszContainerSchema create( + String containerName, + List> childrenSchemas, + BiFunction, TreeNode, C> instanceCtor) { + return new AbstractSszContainerSchema(containerName, childrenSchemas) { + @Override + public C createFromBackingNode(TreeNode node) { + return instanceCtor.apply(this, node); + } + }; + } + + /** + * Get the index of a field by name + * + * @param fieldName + * @return The index if it exists, otherwise -1 + */ + int getFieldIndex(String fieldName); + + /** + * Creates the backing tree from container field values + * + * @throws IllegalArgumentException if value types doesn't match this scheme field types + */ + TreeNode createTreeFromFieldValues(List fieldValues); + + /** + * Creates an {@link SszContainer} instance from field values + * + * @throws IllegalArgumentException if value types doesn't match this scheme field types + */ + default C createFromFieldValues(List fieldValues) { + return createFromBackingNode(createTreeFromFieldValues(fieldValues)); + } + + /** Returns the number of fields in ssz containers of this type */ + default int getFieldsCount() { + return (int) getMaxLength(); + } + + /** Returns this container name */ + String getContainerName(); + + /** Return this container field names */ + List getFieldNames(); + + /** Return this container field schemas */ + List> getFieldSchemas(); +} diff --git a/src/org/minima/system/network/base/ssz/SszData.java b/src/org/minima/system/network/base/ssz/SszData.java new file mode 100644 index 000000000..822c81a8f --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszData.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** + * Base class of immutable views over Binary Backing Tree ({@link TreeNode}) Overlay views concept + * described here: + * https://github.com/protolambda/eth-merkle-trees/blob/master/typing_partials.md#views + */ +public interface SszData extends Merkleizable, SimpleOffsetSerializable { + + /** + * Creates a corresponding writeable copy of this immutable structure Any modifications made to + * the returned copy affect neither this structure nor its descendant structures + */ + SszMutableData createWritableCopy(); + + default boolean isWritableSupported() { + return true; + } + + /** Gets the schema of this structure */ + SszSchema getSchema(); + + /** Returns Backing Tree this structure is backed by */ + TreeNode getBackingNode(); + + @Override + default Bytes32 hashTreeRoot() { + return getBackingNode().hashTreeRoot(); + } + + @Override + default Bytes sszSerialize() { + return getSchema().sszSerializeTree(getBackingNode()); + } + + @Override + default int sszSerialize(SszWriter writer) { + return getSchema().sszSerializeTree(getBackingNode(), writer); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszDeserializeException.java b/src/org/minima/system/network/base/ssz/SszDeserializeException.java new file mode 100644 index 000000000..9e122bd0b --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszDeserializeException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +public class SszDeserializeException extends IllegalArgumentException { + + public SszDeserializeException(String s) { + super(s); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszLengthBounds.java b/src/org/minima/system/network/base/ssz/SszLengthBounds.java new file mode 100644 index 000000000..e39be4e9d --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszLengthBounds.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static org.minima.system.network.base.ssz.TreeUtil.bitsCeilToBytes; + +import com.google.common.base.MoreObjects; +import java.util.Objects; + +public class SszLengthBounds { + public static final SszLengthBounds ZERO = new SszLengthBounds(0, 0); + private final long min; + private final long max; + + public static SszLengthBounds ofBits(long fixedSize) { + return new SszLengthBounds(fixedSize, fixedSize); + } + + public static SszLengthBounds ofBits(long min, long max) { + return new SszLengthBounds(min, max); + } + + public static SszLengthBounds ofBytes(long fixedSize) { + return new SszLengthBounds(fixedSize * 8, fixedSize * 8); + } + + public static SszLengthBounds ofBytes(long min, long max) { + return new SszLengthBounds(min * 8, max * 8); + } + + private SszLengthBounds(final long min, final long max) { + this.min = min; + this.max = max; + } + + public long getMinBits() { + return min; + } + + public long getMaxBits() { + return max; + } + + public long getMinBytes() { + return bitsCeilToBytes(min); + } + + public long getMaxBytes() { + return bitsCeilToBytes(max); + } + + public SszLengthBounds ceilToBytes() { + return SszLengthBounds.ofBytes(getMinBytes(), getMaxBytes()); + } + + public SszLengthBounds add(final SszLengthBounds other) { + return new SszLengthBounds(this.min + other.min, this.max + other.max); + } + + public SszLengthBounds addBytes(final int moreBytes) { + return addBits(moreBytes * 8); + } + + public SszLengthBounds addBits(final int moreBits) { + return new SszLengthBounds(this.min + moreBits, this.max + moreBits); + } + + public SszLengthBounds mul(final long factor) { + return new SszLengthBounds(this.min * factor, this.max * factor); + } + + public boolean isWithinBounds(final long lengthBytes) { + return lengthBytes >= getMinBytes() && lengthBytes <= getMaxBytes(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SszLengthBounds that = (SszLengthBounds) o; + return getMinBits() == that.getMinBits() && getMaxBits() == that.getMaxBits(); + } + + @Override + public int hashCode() { + return Objects.hash(getMinBits(), getMaxBits()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("min", fromBits(getMinBits())) + .add("max", fromBits(getMaxBits())) + .toString(); + } + + private static String fromBits(long bits) { + long bytes = bits / 8; + return "" + bytes + ((bits & 7) == 0 ? "" : "(+" + (bits - bytes * 8) + " bits)"); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableBytes32Vector.java b/src/org/minima/system/network/base/ssz/SszMutableBytes32Vector.java new file mode 100644 index 000000000..e65d915ff --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableBytes32Vector.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; +//import tech.pegasys.teku.ssz.primitive.SszBytes32; + +public interface SszMutableBytes32Vector + extends SszMutablePrimitiveVector, SszBytes32Vector { + + @Override + SszBytes32Vector commitChanges(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableBytes32VectorImpl.java b/src/org/minima/system/network/base/ssz/SszMutableBytes32VectorImpl.java new file mode 100644 index 000000000..63f7b2b66 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableBytes32VectorImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes32; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.collections.SszBytes32Vector; +// import tech.pegasys.teku.ssz.collections.SszMutableBytes32Vector; +// import tech.pegasys.teku.ssz.impl.AbstractSszComposite; +// import tech.pegasys.teku.ssz.primitive.SszBytes32; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszMutableBytes32VectorImpl extends SszMutablePrimitiveVectorImpl + implements SszMutableBytes32Vector { + + public SszMutableBytes32VectorImpl(AbstractSszComposite backingImmutableData) { + super(backingImmutableData); + } + + @Override + public SszBytes32Vector commitChanges() { + return (SszBytes32Vector) super.commitChanges(); + } + + @Override + protected SszBytes32VectorImpl createImmutableSszComposite( + TreeNode backingNode, IntCache childrenCache) { + return new SszBytes32VectorImpl(getSchema(), backingNode); + } + + @Override + public SszMutableBytes32VectorImpl createWritableCopy() { + throw new UnsupportedOperationException( + "Creating a writable copy from writable instance is not supported"); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableComposite.java b/src/org/minima/system/network/base/ssz/SszMutableComposite.java new file mode 100644 index 000000000..6f7ca681c --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableComposite.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Represents composite mutable ssz structure which has descendant ssz structures + * + * @param the type of children + */ +public interface SszMutableComposite + extends SszMutableData, SszComposite { + + /** + * Sets the function which should called by the implementation on any changes in this structure or + * its descendant structures. + * + *

This is to propagate changes up in the data hierarchy from child mutable structures to + * parent mutable structures. + * + * @param listener listener to be called with this instance as a parameter + */ + void setInvalidator(Consumer listener); + + /** + * Sets the child at the index. + * + *

If {@code index == size()} and the structure is extendable (e.g. List) then this is treated + * as `append()` operation and the size is expanded. In this case `size` should be less than + * `maxSize` + * + * @throws IndexOutOfBoundsException if index > size() or if index == size() but size() == maxSize + */ + void set(int index, SszChildT value); + + /** + * Similar to {@link #set(int, SszData)} but using modifier function which may consider old value + * to calculate new value. The implementation may potentially optimize this case. + */ + default void update(int index, Function mutator) { + set(index, mutator.apply(get(index))); + } + + default void setAll(Iterable newChildren) { + clear(); + int idx = 0; + for (SszChildT newChild : newChildren) { + set(idx++, newChild); + } + } + + @Override + SszComposite commitChanges(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableContainer.java b/src/org/minima/system/network/base/ssz/SszMutableContainer.java new file mode 100644 index 000000000..be265fc16 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableContainer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** Base class for mutable containers. */ +public interface SszMutableContainer extends SszMutableComposite, SszContainer { + + @Override + SszContainer commitChanges(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableContainerImpl.java b/src/org/minima/system/network/base/ssz/SszMutableContainerImpl.java new file mode 100644 index 000000000..ca821ab7d --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableContainerImpl.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.List; +import java.util.Map; +// import tech.pegasys.teku.ssz.SszContainer; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableContainer; +// import tech.pegasys.teku.ssz.SszMutableData; +// import tech.pegasys.teku.ssz.SszMutableRefContainer; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszContainerSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.tree.TreeUpdates; + +public class SszMutableContainerImpl extends AbstractSszMutableComposite + implements SszMutableRefContainer { + + public SszMutableContainerImpl(SszContainerImpl backingImmutableView) { + super(backingImmutableView); + } + + @Override + protected SszContainerImpl createImmutableSszComposite( + TreeNode backingNode, IntCache viewCache) { + return new SszContainerImpl(getSchema(), backingNode, viewCache); + } + + @Override + public AbstractSszContainerSchema getSchema() { + return (AbstractSszContainerSchema) super.getSchema(); + } + + @Override + public SszContainer commitChanges() { + return (SszContainer) super.commitChanges(); + } + + @Override + public SszMutableContainer createWritableCopy() { + throw new UnsupportedOperationException( + "createWritableCopy() is now implemented for immutable SszData only"); + } + + @Override + protected void checkIndex(int index, boolean set) { + if (index >= size()) { + throw new IndexOutOfBoundsException( + "Invalid index " + index + " for container with size " + size()); + } + } + + @Override + protected TreeUpdates packChanges( + List> newChildValues, TreeNode original) { + throw new UnsupportedOperationException("Packed values are not supported"); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableData.java b/src/org/minima/system/network/base/ssz/SszMutableData.java new file mode 100644 index 000000000..1488f33aa --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableData.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.tree.TreeNode; + +/** + * Base class of mutable structures over Binary Backing Tree ({@link TreeNode}) Each {@link + * SszMutableData} subclass class normally inherits from the corresponding immutable class to have + * both get/set methods however {@link SszMutableData} instance shouldn't be leaked as {@link + * SszData} instance, the {@link #commitChanges()} should be used instead to get immutable structure + */ +public interface SszMutableData extends SszData { + + /** Resets this ssz structure to its default value */ + void clear(); + + /** Creates the corresponding immutable structure with all the changes */ + SszData commitChanges(); + + /** + * Returns the backing tree of this modified structure. + * + *

Note that calling this method on {@link SszMutableData} could be suboptimal from performance + * perspective as it internally needs to create an immutable {@link SszData} via {@link + * #commitChanges()} which is then discarded. It's normally better to make all modifications on + * {@link SszMutableData}, commit the changes and then call either {@link + * SszData#getBackingNode()} or {@link SszData#hashTreeRoot()} on the resulting immutable instance + */ + @Override + default TreeNode getBackingNode() { + return commitChanges().getBackingNode(); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutablePrimitiveCollection.java b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveCollection.java new file mode 100644 index 000000000..cee33b7b1 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveCollection.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszMutableComposite; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; + +public interface SszMutablePrimitiveCollection< + ElementT, SszElementT extends SszPrimitive> + extends SszPrimitiveCollection, SszMutableComposite { + + @SuppressWarnings("unchecked") + default SszPrimitiveSchema getPrimitiveElementSchema() { + return (SszPrimitiveSchema) getSchema().getElementSchema(); + } + + default void setElement(int index, ElementT primitiveValue) { + SszElementT sszData = getPrimitiveElementSchema().boxed(primitiveValue); + set(index, sszData); + } + + default void setAllElements(Iterable newChildren) { + clear(); + int idx = 0; + for (ElementT newChild : newChildren) { + setElement(idx++, newChild); + } + } + + @Override + SszPrimitiveCollection commitChanges(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVector.java b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVector.java new file mode 100644 index 000000000..64adf2a46 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszMutableVector; +// import tech.pegasys.teku.ssz.SszPrimitive; + +public interface SszMutablePrimitiveVector< + ElementT, SszElementT extends SszPrimitive> + extends SszMutablePrimitiveCollection, + SszMutableVector, + SszPrimitiveVector { + + @Override + SszPrimitiveVector commitChanges(); + + @Override + SszMutablePrimitiveVector createWritableCopy(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVectorImpl.java b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVectorImpl.java new file mode 100644 index 000000000..d66465ce5 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutablePrimitiveVectorImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.collections.SszMutablePrimitiveVector; +// import tech.pegasys.teku.ssz.collections.SszPrimitiveVector; +// import tech.pegasys.teku.ssz.impl.AbstractSszComposite; +// import tech.pegasys.teku.ssz.impl.SszMutableVectorImpl; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszMutablePrimitiveVectorImpl< + ElementT, SszElementT extends SszPrimitive> + extends SszMutableVectorImpl + implements SszMutablePrimitiveVector { + + public SszMutablePrimitiveVectorImpl(AbstractSszComposite backingImmutableData) { + super(backingImmutableData); + } + + @Override + @SuppressWarnings("unchecked") + public SszPrimitiveVector commitChanges() { + return (SszPrimitiveVector) super.commitChanges(); + } + + @Override + protected AbstractSszComposite createImmutableSszComposite( + TreeNode backingNode, IntCache childrenCache) { + return new SszPrimitiveVectorImpl<>(getSchema(), backingNode); + } + + @Override + public SszMutablePrimitiveVector createWritableCopy() { + throw new UnsupportedOperationException( + "Creating a writable copy from writable instance is not supported"); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableRefComposite.java b/src/org/minima/system/network/base/ssz/SszMutableRefComposite.java new file mode 100644 index 000000000..4dd1c98fc --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableRefComposite.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** + * Represents a mutable {@link SszComposite} which is able to return a mutable child 'by reference'. + * Any modifications made to such child are reflected in this structure and its backing tree + */ +public interface SszMutableRefComposite< + ChildReadType extends SszData, ChildWriteType extends ChildReadType> + extends SszMutableComposite { + + /** + * Returns a mutable child at index 'by reference' Any modifications made to such child are + * reflected in this structure and its backing tree + * + * @throws IndexOutOfBoundsException if index >= size() + */ + ChildWriteType getByRef(int index); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableRefContainer.java b/src/org/minima/system/network/base/ssz/SszMutableRefContainer.java new file mode 100644 index 000000000..30a6195ca --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableRefContainer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** + * Base class for mutable {@link SszContainer} which is able to return mutable children by reference + */ +public interface SszMutableRefContainer + extends SszMutableRefComposite, SszMutableContainer { + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + // container is heterogeneous by its nature so making unsafe cast here + // is more convenient and is not less safe + default W getAnyByRef(int index) { + return (W) getByRef(index); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableRefVector.java b/src/org/minima/system/network/base/ssz/SszMutableRefVector.java new file mode 100644 index 000000000..02cd89e47 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableRefVector.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** + * Represents a mutable {@link SszVector} which is able to return a mutable child 'by reference' Any + * modifications made to such child are reflected in this vector and its backing tree + * + * @param Class of immutable child views + * @param Class of the corresponding mutable child views + */ +public interface SszMutableRefVector< + SszElementT extends SszData, SszMutableElementT extends SszElementT> + extends SszMutableRefComposite, SszMutableVector { + + /** + * Returns a mutable child at index 'by reference' Any modifications made to such child are + * reflected in this structure and its backing tree + * + * @throws IndexOutOfBoundsException if index >= size() + */ + @Override + SszMutableElementT getByRef(int index); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableVector.java b/src/org/minima/system/network/base/ssz/SszMutableVector.java new file mode 100644 index 000000000..a584f3b53 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableVector.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** + * Mutable {@link SszVector} with immutable elements. This type of vector can be modified by setting + * immutable elements + * + * @param Type of elements + */ +public interface SszMutableVector + extends SszMutableComposite, SszVector { + + @Override + SszVector commitChanges(); +} diff --git a/src/org/minima/system/network/base/ssz/SszMutableVectorImpl.java b/src/org/minima/system/network/base/ssz/SszMutableVectorImpl.java new file mode 100644 index 000000000..2f952ec6b --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszMutableVectorImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableRefVector; +// import tech.pegasys.teku.ssz.SszMutableVector; +// import tech.pegasys.teku.ssz.SszVector; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszVectorSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszMutableVectorImpl< + SszElementT extends SszData, SszMutableElementT extends SszElementT> + extends AbstractSszMutableCollection + implements SszMutableRefVector { + + public SszMutableVectorImpl(AbstractSszComposite backingImmutableData) { + super(backingImmutableData); + } + + @Override + protected AbstractSszComposite createImmutableSszComposite( + TreeNode backingNode, IntCache childrenCache) { + return new SszVectorImpl<>(getSchema(), backingNode, childrenCache); + } + + @Override + @SuppressWarnings("unchecked") + public SszVectorSchema getSchema() { + return (SszVectorSchema) super.getSchema(); + } + + @Override + @SuppressWarnings("unchecked") + public SszVector commitChanges() { + return (SszVector) super.commitChanges(); + } + + @Override + protected void checkIndex(int index, boolean set) { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException( + "Invalid index " + index + " for vector with size " + size()); + } + } + + @Override + public SszMutableVector createWritableCopy() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszNodeTemplate.java b/src/org/minima/system/network/base/ssz/SszNodeTemplate.java new file mode 100644 index 000000000..e570f222e --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszNodeTemplate.java @@ -0,0 +1,208 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.LEFTMOST_G_INDEX; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.RIGHTMOST_G_INDEX; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.SELF_G_INDEX; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.gIdxIsSelf; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.gIdxLeftGIndex; +// import static tech.pegasys.teku.ssz.tree.GIndexUtil.gIdxRightGIndex; + +import static org.minima.system.network.base.ssz.GIndexUtil.LEFTMOST_G_INDEX; +import static org.minima.system.network.base.ssz.GIndexUtil.RIGHTMOST_G_INDEX; +import static org.minima.system.network.base.ssz.GIndexUtil.SELF_G_INDEX; +import static org.minima.system.network.base.ssz.GIndexUtil.gIdxIsSelf; +import static org.minima.system.network.base.ssz.GIndexUtil.gIdxLeftGIndex; +import static org.minima.system.network.base.ssz.GIndexUtil.gIdxRightGIndex; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.MutableBytes; +//import org.apache.tuweni.crypto.Hash; +//import tech.pegasys.teku.ssz.schema.SszSchema; +import org.minima.utils.Crypto; + +/** + * Represents the tree structure for a fixed size SSZ type See {@link SszSuperNode} docs for more + * details + */ +public class SszNodeTemplate { + + static final class Location { + private final int offset; + private final int length; + private final boolean leaf; + + public Location(int offset, int length, boolean leaf) { + this.offset = offset; + this.length = length; + this.leaf = leaf; + } + + public Location withAddedOffset(int addOffset) { + return new Location(getOffset() + addOffset, getLength(), isLeaf()); + } + + public int getOffset() { + return offset; + } + + public int getLength() { + return length; + } + + public boolean isLeaf() { + return leaf; + } + } + + public static SszNodeTemplate createFromType(SszSchema sszSchema) { + checkArgument(sszSchema.isFixedSize(), "Only fixed size types supported"); + + return createFromTree(sszSchema.getDefaultTree()); + } + + // This should be CANONICAL binary tree + private static SszNodeTemplate createFromTree(TreeNode defaultTree) { + Map gIdxToLoc = + binaryTraverse( + GIndexUtil.SELF_G_INDEX, + defaultTree, + new BinaryVisitor<>() { + @Override + public Map visitLeaf(long gIndex, LeafNode node) { + Map ret = new HashMap<>(); + ret.put(gIndex, new Location(0, node.getData().size(), true)); + return ret; + } + + @Override + public Map visitBranch( + long gIndex, + TreeNode node, + Map leftVisitResult, + Map rightVisitResult) { + Location leftChildLoc = leftVisitResult.get(gIdxLeftGIndex(gIndex)); + Location rightChildLoc = rightVisitResult.get(gIdxRightGIndex(gIndex)); + rightVisitResult.replaceAll( + (idx, loc) -> loc.withAddedOffset(leftChildLoc.getLength())); + leftVisitResult.putAll(rightVisitResult); + leftVisitResult.put( + gIndex, + new Location(0, leftChildLoc.getLength() + rightChildLoc.getLength(), false)); + return leftVisitResult; + } + }); + return new SszNodeTemplate(gIdxToLoc, defaultTree); + } + + private static List nodeSsz(TreeNode node) { + List sszBytes = new ArrayList<>(); + TreeUtil.iterateLeavesData(node, LEFTMOST_G_INDEX, RIGHTMOST_G_INDEX, sszBytes::add); + return sszBytes; + } + + private final Map gIdxToLoc; + private final TreeNode defaultTree; + private final Map subTemplatesCache = new ConcurrentHashMap<>(); + + public SszNodeTemplate(Map gIdxToLoc, TreeNode defaultTree) { + this.gIdxToLoc = gIdxToLoc; + this.defaultTree = defaultTree; + } + + public Location getNodeSszLocation(long generalizedIndex) { + return gIdxToLoc.get(generalizedIndex); + } + + public int getSszLength() { + return gIdxToLoc.get(SELF_G_INDEX).getLength(); + } + + public SszNodeTemplate getSubTemplate(long generalizedIndex) { + return subTemplatesCache.computeIfAbsent(generalizedIndex, this::calcSubTemplate); + } + + private SszNodeTemplate calcSubTemplate(long generalizedIndex) { + if (gIdxIsSelf(generalizedIndex)) { + return this; + } + TreeNode subTree = defaultTree.get(generalizedIndex); + return createFromTree(subTree); + } + + public void update(long generalizedIndex, TreeNode newNode, MutableBytes dest) { + update(generalizedIndex, nodeSsz(newNode), dest); + } + + private void update(long generalizedIndex, List nodeSsz, MutableBytes dest) { + Location leafPos = getNodeSszLocation(generalizedIndex); + int off = 0; + for (int i = 0; i < nodeSsz.size(); i++) { + Bytes newSszChunk = nodeSsz.get(i); + newSszChunk.copyTo(dest, leafPos.getOffset() + off); + off += newSszChunk.size(); + } + checkArgument(off == leafPos.getLength()); + } + + public Bytes32 calculateHashTreeRoot(Bytes ssz, int offset) { + return binaryTraverse( + SELF_G_INDEX, + defaultTree, + new BinaryVisitor<>() { + @Override + public Bytes32 visitLeaf(long gIndex, LeafNode node) { + Location location = gIdxToLoc.get(gIndex); + return Bytes32.rightPad(ssz.slice(offset + location.getOffset(), location.getLength())); + } + + @Override + public Bytes32 visitBranch( + long gIndex, TreeNode node, Bytes32 leftVisitResult, Bytes32 rightVisitResult) { + return Bytes32.wrap((new Crypto()).hashSHA2(Bytes.wrap(leftVisitResult, rightVisitResult).toArray())); +// return Hash.sha2_256(Bytes.wrap(leftVisitResult, rightVisitResult)); + } + }); + } + + private static T binaryTraverse(long gIndex, TreeNode node, BinaryVisitor visitor) { + if (node instanceof LeafNode) { + return visitor.visitLeaf(gIndex, (LeafNode) node); + } else if (node instanceof BranchNode) { + BranchNode branchNode = (BranchNode) node; + return visitor.visitBranch( + gIndex, + branchNode, + binaryTraverse(gIdxLeftGIndex(gIndex), branchNode.left(), visitor), + binaryTraverse(gIdxRightGIndex(gIndex), branchNode.right(), visitor)); + } else { + throw new IllegalArgumentException("Unexpected node type: " + node.getClass()); + } + } + + private interface BinaryVisitor { + + T visitLeaf(long gIndex, LeafNode node); + + T visitBranch(long gIndex, TreeNode node, T leftVisitResult, T rightVisitResult); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitive.java b/src/org/minima/system/network/base/ssz/SszPrimitive.java new file mode 100644 index 000000000..4111ac6e0 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitive.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; + +/** + * A wrapper class for SSZ primitive values. {@link SszPrimitive} classes has no mutable versions + */ +public interface SszPrimitive> + extends SszData { + + /** Returns wrapped primitive value */ + ValueType get(); + + /** {@link SszPrimitive} classes has no mutable versions */ + @Override + default SszMutableData createWritableCopy() { + throw new UnsupportedOperationException("Basic view instances are immutable"); + } + + @Override + default boolean isWritableSupported() { + return false; + } + + @Override + SszPrimitiveSchema getSchema(); +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveCollection.java b/src/org/minima/system/network/base/ssz/SszPrimitiveCollection.java new file mode 100644 index 000000000..b51d16616 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveCollection.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.AbstractList; +import java.util.List; +import java.util.stream.Stream; +// import tech.pegasys.teku.ssz.SszCollection; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszCollectionSchema; + +public interface SszPrimitiveCollection< + ElementT, SszElementT extends SszPrimitive> + extends SszCollection { + + default ElementT getElement(int index) { + return get(index).get(); + } + + @Override + SszCollectionSchema getSchema(); + + @Override + SszMutablePrimitiveCollection createWritableCopy(); + + default List asListUnboxed() { + return new AbstractList<>() { + @Override + public ElementT get(int index) { + return SszPrimitiveCollection.this.getElement(index); + } + + @Override + public int size() { + return SszPrimitiveCollection.this.size(); + } + }; + } + + default Stream streamUnboxed() { + return asListUnboxed().stream(); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveCollectionSchema.java b/src/org/minima/system/network/base/ssz/SszPrimitiveCollectionSchema.java new file mode 100644 index 000000000..75eb88d21 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveCollectionSchema.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.collections.SszPrimitiveCollection; +// import tech.pegasys.teku.ssz.schema.SszCollectionSchema; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; + +public interface SszPrimitiveCollectionSchema< + ElementT, + SszElementT extends SszPrimitive, + SszCollectionT extends SszPrimitiveCollection> + extends SszCollectionSchema { + + @SuppressWarnings("unchecked") + default SszCollectionT of(ElementT... rawElements) { + return of(Arrays.asList(rawElements)); + } + + default SszCollectionT of(List rawElements) { + SszPrimitiveSchema elementSchema = getPrimitiveElementSchema(); + return createFromElements( + rawElements.stream().map(elementSchema::boxed).collect(Collectors.toList())); + } + + @SuppressWarnings("unchecked") + default SszPrimitiveSchema getPrimitiveElementSchema() { + return (SszPrimitiveSchema) getElementSchema(); + } + + default Collector collectorUnboxed() { + return Collectors.collectingAndThen(Collectors.toList(), this::of); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveSchema.java b/src/org/minima/system/network/base/ssz/SszPrimitiveSchema.java new file mode 100644 index 000000000..b6ef1bfee --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveSchema.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.SszPrimitive; + +public interface SszPrimitiveSchema> + extends SszSchema { + + int getBitsSize(); + + SszDataT boxed(DataT rawValue); + + @Override + default boolean isPrimitive() { + return true; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveSchemas.java b/src/org/minima/system/network/base/ssz/SszPrimitiveSchemas.java new file mode 100644 index 000000000..c12bedf1c --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveSchemas.java @@ -0,0 +1,237 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.nio.ByteOrder; +import java.util.Arrays; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.MutableBytes; +// import tech.pegasys.teku.infrastructure.unsigned.UInt64; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.primitive.SszBit; +// import tech.pegasys.teku.ssz.primitive.SszByte; +// import tech.pegasys.teku.ssz.primitive.SszBytes32; +// import tech.pegasys.teku.ssz.primitive.SszBytes4; +// import tech.pegasys.teku.ssz.primitive.SszUInt64; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszPrimitiveSchema; +// import tech.pegasys.teku.ssz.tree.LeafDataNode; +// import tech.pegasys.teku.ssz.tree.LeafNode; +// import tech.pegasys.teku.ssz.tree.TreeNode; +// import tech.pegasys.teku.ssz.type.Bytes4; + +/** The collection of commonly used basic types */ +public interface SszPrimitiveSchemas { + AbstractSszPrimitiveSchema BIT_SCHEMA = + new AbstractSszPrimitiveSchema<>(1) { + @Override + public SszBit createFromLeafBackingNode(LeafDataNode node, int idx) { + return SszBit.of((node.getData().get(idx / 8) & (1 << (idx % 8))) != 0); + } + + @Override + public TreeNode updateBackingNode(TreeNode srcNode, int idx, SszData newValue) { + int byteIndex = idx / 8; + int bitIndex = idx % 8; + Bytes originalBytes = ((LeafNode) srcNode).getData(); + byte b = byteIndex < originalBytes.size() ? originalBytes.get(byteIndex) : 0; + boolean bit = ((SszBit) newValue).get(); + if (bit) { + b = (byte) (b | (1 << bitIndex)); + } else { + b = (byte) (b & ~(1 << bitIndex)); + } + Bytes newBytes = updateExtending(originalBytes, byteIndex, Bytes.of(b)); + return LeafNode.create(newBytes); + } + + @Override + public SszBit boxed(Boolean rawValue) { + return SszBit.of(rawValue); + } + + @Override + public TreeNode getDefaultTree() { + return LeafNode.ZERO_LEAVES[1]; + } + + @Override + public String toString() { + return "Bit"; + } + }; + + AbstractSszPrimitiveSchema BYTE_SCHEMA = + new AbstractSszPrimitiveSchema<>(8) { + @Override + public SszByte createFromLeafBackingNode(LeafDataNode node, int internalIndex) { + return SszByte.of(node.getData().get(internalIndex)); + } + + @Override + public TreeNode updateBackingNode(TreeNode srcNode, int index, SszData newValue) { + byte aByte = ((SszByte) newValue).get(); + Bytes curVal = ((LeafNode) srcNode).getData(); + Bytes newBytes = updateExtending(curVal, index, Bytes.of(aByte)); + return LeafNode.create(newBytes); + } + + @Override + public SszByte boxed(Byte rawValue) { + return SszByte.of(rawValue); + } + + @Override + public TreeNode getDefaultTree() { + return LeafNode.ZERO_LEAVES[1]; + } + + @Override + public String toString() { + return "Byte"; + } + }; + + AbstractSszPrimitiveSchema UINT64_SCHEMA = + new AbstractSszPrimitiveSchema<>(64) { + @Override + public SszUInt64 createFromLeafBackingNode(LeafDataNode node, int internalIndex) { + Bytes leafNodeBytes = node.getData(); + try { + Bytes elementBytes = leafNodeBytes.slice(internalIndex * 8, 8); + return SszUInt64.of(UInt64.fromLongBits(elementBytes.toLong(ByteOrder.LITTLE_ENDIAN))); + } catch (Exception e) { + // additional info to track down the bug https://github.com/PegaSysEng/teku/issues/2579 + String info = + "Refer to https://github.com/PegaSysEng/teku/issues/2579 if see this exception. "; + info += "internalIndex = " + internalIndex; + info += ", leafNodeBytes: " + leafNodeBytes.getClass().getSimpleName(); + try { + info += ", leafNodeBytes = " + leafNodeBytes.copy(); + } catch (Exception ex) { + info += "(" + ex + ")"; + } + try { + info += ", leafNodeBytes[] = " + Arrays.toString(leafNodeBytes.toArray()); + } catch (Exception ex) { + info += "(" + ex + ")"; + } + throw new RuntimeException(info, e); + } + } + + @Override + public TreeNode updateBackingNode(TreeNode srcNode, int index, SszData newValue) { + Bytes uintBytes = + Bytes.ofUnsignedLong(((SszUInt64) newValue).longValue(), ByteOrder.LITTLE_ENDIAN); + Bytes curVal = ((LeafNode) srcNode).getData(); + Bytes newBytes = updateExtending(curVal, index * 8, uintBytes); + return LeafNode.create(newBytes); + } + + @Override + public SszUInt64 boxed(UInt64 rawValue) { + return SszUInt64.of(rawValue); + } + + @Override + public TreeNode getDefaultTree() { + return LeafNode.ZERO_LEAVES[8]; + } + + @Override + public String toString() { + return "UInt64"; + } + }; + + AbstractSszPrimitiveSchema BYTES4_SCHEMA = + new AbstractSszPrimitiveSchema<>(32) { + @Override + public SszBytes4 createFromLeafBackingNode(LeafDataNode node, int internalIndex) { + return SszBytes4.of(new Bytes4(node.getData().slice(internalIndex * 4, 4))); + } + + @Override + public TreeNode updateBackingNode(TreeNode srcNode, int internalIndex, SszData newValue) { + checkArgument( + internalIndex >= 0 && internalIndex < 8, "Invalid internal index: %s", internalIndex); + Bytes bytes = ((SszBytes4) newValue).get().getWrappedBytes(); + Bytes curVal = ((LeafNode) srcNode).getData(); + Bytes newBytes = updateExtending(curVal, internalIndex * 4, bytes); + return LeafNode.create(newBytes); + } + + @Override + public SszBytes4 boxed(Bytes4 rawValue) { + return SszBytes4.of(rawValue); + } + + @Override + public TreeNode getDefaultTree() { + return LeafNode.ZERO_LEAVES[4]; + } + + @Override + public String toString() { + return "Bytes4"; + } + }; + + AbstractSszPrimitiveSchema BYTES32_SCHEMA = + new AbstractSszPrimitiveSchema<>(256) { + @Override + public SszBytes32 createFromLeafBackingNode(LeafDataNode node, int internalIndex) { + return SszBytes32.of(node.hashTreeRoot()); + } + + @Override + public TreeNode updateBackingNode(TreeNode srcNode, int internalIndex, SszData newValue) { + return LeafNode.create(((SszBytes32) newValue).get()); + } + + @Override + public SszBytes32 boxed(Bytes32 rawValue) { + return SszBytes32.of(rawValue); + } + + @Override + public TreeNode getDefaultTree() { + return LeafNode.ZERO_LEAVES[32]; + } + + @Override + public String toString() { + return "Bytes32"; + } + }; + + private static Bytes updateExtending(Bytes origBytes, int origOff, Bytes newBytes) { + if (origOff == origBytes.size()) { + return Bytes.wrap(origBytes, newBytes); + } else { + final MutableBytes dest; + if (origOff + newBytes.size() > origBytes.size()) { + dest = MutableBytes.create(origOff + newBytes.size()); + origBytes.copyTo(dest, 0); + } else { + dest = origBytes.mutableCopy(); + } + newBytes.copyTo(dest, origOff); + return dest; + } + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveVector.java b/src/org/minima/system/network/base/ssz/SszPrimitiveVector.java new file mode 100644 index 000000000..b8ce45fcd --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveVector.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.SszVector; + +public interface SszPrimitiveVector< + ElementT, SszElementT extends SszPrimitive> + extends SszPrimitiveCollection, SszVector { + + @Override + SszMutablePrimitiveVector createWritableCopy(); +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveVectorImpl.java b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorImpl.java new file mode 100644 index 000000000..7662b3500 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorImpl.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Supplier; +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.collections.SszMutablePrimitiveVector; +// import tech.pegasys.teku.ssz.collections.SszPrimitiveVector; +// import tech.pegasys.teku.ssz.impl.SszVectorImpl; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszPrimitiveVectorImpl< + ElementT, SszElementT extends SszPrimitive> + extends SszVectorImpl implements SszPrimitiveVector { + + public SszPrimitiveVectorImpl(SszCompositeSchema schema, Supplier lazyBackingNode) { + super(schema, lazyBackingNode); + } + + public SszPrimitiveVectorImpl(SszCompositeSchema schema, TreeNode backingNode) { + super(schema, backingNode); + } + + @Override + public SszMutablePrimitiveVector createWritableCopy() { + return new SszMutablePrimitiveVectorImpl<>(this); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchema.java b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchema.java new file mode 100644 index 000000000..b651177ef --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchema.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.collections.SszPrimitiveVector; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints; +// import tech.pegasys.teku.ssz.schema.SszVectorSchema; +// import tech.pegasys.teku.ssz.schema.collections.impl.SszPrimitiveVectorSchemaImpl; + +public interface SszPrimitiveVectorSchema< + ElementT, + SszElementT extends SszPrimitive, + SszVectorT extends SszPrimitiveVector> + extends SszPrimitiveCollectionSchema, + SszVectorSchema { + + static > + SszPrimitiveVectorSchema create( + SszPrimitiveSchema elementSchema, int length) { + return create(elementSchema, length, SszSchemaHints.none()); + } + + @SuppressWarnings("unchecked") + static > + SszPrimitiveVectorSchema create( + SszPrimitiveSchema elementSchema, long length, SszSchemaHints hints) { + if (elementSchema == SszPrimitiveSchemas.BIT_SCHEMA) { + return (SszPrimitiveVectorSchema) SszBitvectorSchema.create(length); + } else if (elementSchema == SszPrimitiveSchemas.BYTE_SCHEMA) { + return (SszPrimitiveVectorSchema) + SszByteVectorSchema.create((int) length); + } else if (elementSchema == SszPrimitiveSchemas.BYTES32_SCHEMA) { + return (SszPrimitiveVectorSchema) + SszBytes32VectorSchema.create((int) length); + } else { + return new SszPrimitiveVectorSchemaImpl<>(elementSchema, length); + } + } +} diff --git a/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchemaImpl.java b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchemaImpl.java new file mode 100644 index 000000000..4876ba8bb --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszPrimitiveVectorSchemaImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszPrimitive; +// import tech.pegasys.teku.ssz.collections.SszPrimitiveVector; +// import tech.pegasys.teku.ssz.collections.impl.SszPrimitiveVectorImpl; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchema; +// import tech.pegasys.teku.ssz.schema.collections.SszPrimitiveVectorSchema; +// import tech.pegasys.teku.ssz.schema.impl.AbstractSszVectorSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszPrimitiveVectorSchemaImpl< + ElementT, + SszElementT extends SszPrimitive, + SszVectorT extends SszPrimitiveVector> + extends AbstractSszVectorSchema + implements SszPrimitiveVectorSchema { + + public SszPrimitiveVectorSchemaImpl( + SszPrimitiveSchema elementSchema, long vectorLength) { + super(elementSchema, vectorLength); + } + + @Override + @SuppressWarnings("unchecked") + public SszVectorT createFromBackingNode(TreeNode node) { + return (SszVectorT) new SszPrimitiveVectorImpl(this, node); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszReader.java b/src/org/minima/system/network/base/ssz/SszReader.java new file mode 100644 index 000000000..4ba5037c4 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszReader.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.io.Closeable; +import org.apache.tuweni.bytes.Bytes; + +/** Simple reader interface for SSZ stream */ +public interface SszReader extends Closeable { + + /** Creates an instance from {@link Bytes} */ + static SszReader fromBytes(Bytes bytes) { + return new SimpleSszReader(bytes); + } + + /** Number of bytes available for reading */ + int getAvailableBytes(); + + /** + * Returns {@link SszReader} instance limited with {@code size} bytes Advances this reader current + * read position to {@code size} bytes + * + * @throws SszDeserializeException If not enough bytes available for slice + */ + SszReader slice(int size) throws SszDeserializeException; + + /** + * Returns {@code length} bytes and advances this reader current read position to {@code length} + * bytes + * + * @throws SszDeserializeException If not enough bytes available + */ + Bytes read(int length) throws SszDeserializeException; + + /** + * Closes this reader. Doesn't affect the 'parent' reader (if any) this instance was 'sliced' from + * + * @throws SszDeserializeException If unread bytes remain + */ + @Override + void close() throws SszDeserializeException; +} diff --git a/src/org/minima/system/network/base/ssz/SszSchema.java b/src/org/minima/system/network/base/ssz/SszSchema.java new file mode 100644 index 000000000..744e7fd57 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszSchema.java @@ -0,0 +1,92 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** + * Base class for any SSZ structure schema like Vector, List, Container, primitive types + * (https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/simple-serialize.md#typing) + */ +public interface SszSchema extends SszType { + + @SuppressWarnings("unchecked") + static SszSchema as(final Class clazz, final SszSchema schema) { + return (SszSchema) schema; + } + + /** + * Creates a default backing binary tree for this schema + * + *

E.g. if the schema is primitive then normally just a single leaf node is created + * + *

E.g. if the schema is a complex structure with multi-level nested vectors and containers + * then the complete tree including all descendant members subtrees is created + */ + TreeNode getDefaultTree(); + + /** + * Creates immutable ssz structure over the tree which should correspond to this schema. + * + *

Note: if the tree structure doesn't correspond this schema that fact could only be detected + * later during access to structure members + */ + SszDataT createFromBackingNode(TreeNode node); + + /** Returns the default immutable structure of this scheme */ + default SszDataT getDefault() { + return createFromBackingNode(getDefaultTree()); + } + + boolean isPrimitive(); + + /** + * For packed primitive values. Extracts a packed value from the tree node by its 'internal + * index'. For example in `Bitvector(512)` the bit value at index `300` is stored at the second + * leaf node and it's 'internal index' in this node would be `45` + */ + default SszDataT createFromBackingNode(TreeNode node, int internalIndex) { + return createFromBackingNode(node); + } + + /** + * For packed primitive values. Packs the value to the existing node at 'internal index' For + * example in `Bitvector(512)` the bit value at index `300` is stored at the second leaf node and + * it's 'internal index' in this node would be `45` + */ + default TreeNode updateBackingNode(TreeNode srcNode, int internalIndex, SszData newValue) { + return newValue.getBackingNode(); + } + + default Bytes sszSerialize(SszDataT view) { + return sszSerializeTree(view.getBackingNode()); + } + + default int sszSerialize(SszDataT view, SszWriter writer) { + return sszSerializeTree(view.getBackingNode(), writer); + } + + default SszDataT sszDeserialize(SszReader reader) throws SszDeserializeException { + return createFromBackingNode(sszDeserializeTree(reader)); + } + + default SszDataT sszDeserialize(Bytes ssz) throws SszDeserializeException { + return sszDeserialize(SszReader.fromBytes(ssz)); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszSchemaHints.java b/src/org/minima/system/network/base/ssz/SszSchemaHints.java new file mode 100644 index 000000000..f1cc2a7e4 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszSchemaHints.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +//import tech.pegasys.teku.ssz.tree.SszSuperNode; + +/** + * A set of hints for {@link SszSchema} classes on strategies to use for optimizing memory or/and + * performance. + */ +public class SszSchemaHints { + + private static class SszSchemaHint {} + + /** + * Hint to use {@link SszSuperNode} for lists/vectors to save the memory when the list content is + * expected to be rarely updated + * + *

The depth parameter specifies the maximum number (2 ^ depth) of + * list/vector elements a single node can contain. Increasing this parameter saves memory but + * makes list/vector update and hashTreeRoot recalculation more CPU expensive + */ + public static final class SszSuperNodeHint extends SszSchemaHint { + private final int depth; + + public SszSuperNodeHint(int depth) { + this.depth = depth; + } + + public int getDepth() { + return depth; + } + } + + public static SszSchemaHints of(SszSchemaHint... hints) { + return new SszSchemaHints(Arrays.asList(hints)); + } + + public static SszSchemaHints none() { + return of(); + } + + public static SszSchemaHints sszSuperNode(int superNodeDepth) { + return of(new SszSuperNodeHint(superNodeDepth)); + } + + private final List hints; + + private SszSchemaHints(List hints) { + this.hints = hints; + } + + @SuppressWarnings("unchecked") + public Optional getHint(Class hintClass) { + return (Optional) hints.stream().filter(h -> h.getClass() == hintClass).findFirst(); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszSuperNode.java b/src/org/minima/system/network/base/ssz/SszSuperNode.java new file mode 100644 index 000000000..994f76e5e --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszSuperNode.java @@ -0,0 +1,176 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.MutableBytes; +//import org.apache.tuweni.crypto.Hash; +import org.jetbrains.annotations.NotNull; +//import tech.pegasys.teku.ssz.tree.GIndexUtil.NodeRelation; +import org.minima.system.network.base.ssz.SszNodeTemplate.Location; +import org.minima.system.network.base.ssz.GIndexUtil.NodeRelation; +import org.minima.utils.Crypto; + +/** + * Stores consecutive elements of the same fixed size type as a single packed bytes of their leaves + * (this representation exactly matches SSZ representation of elements sequence). + * + *

This node represents a subtree of binary merkle tree for sequence (list or vector) of elements + * with maximum length of 2 ^ depth. If the sequence has less than maximum elements + * then ssz bytes store only existing elements (what again matches SSZ representation + * of a list) + * + *

To address individual nodes inside elements and resolve their internal generalized indexes the + * node uses {@link SszNodeTemplate} which represents element type tree structure + * + *

This node favors memory efficiency over update performance and thus is the best choice for + * rarely updated and space consuming structures (e.g. Eth2 BeaconState.validators + * list) + */ +public class SszSuperNode implements TreeNode, LeafDataNode { + private static final TreeNode DEFAULT_NODE = LeafNode.EMPTY_LEAF; + + private final int depth; + private final SszNodeTemplate elementTemplate; + private final Bytes ssz; + private final Supplier hashTreeRoot = Suppliers.memoize(this::calcHashTreeRoot); + + public SszSuperNode(int depth, SszNodeTemplate elementTemplate, Bytes ssz) { + this.depth = depth; + this.elementTemplate = elementTemplate; + this.ssz = ssz; + checkArgument(ssz.size() % elementTemplate.getSszLength() == 0); + checkArgument(getElementsCount() <= getMaxElements()); + } + + private int getMaxElements() { + return 1 << depth; + } + + private int getElementsCount() { + return ssz.size() / elementTemplate.getSszLength(); + } + + @Override + public Bytes32 hashTreeRoot() { + return hashTreeRoot.get(); + } + + private Bytes32 calcHashTreeRoot() { + return hashTreeRoot(0, 0); + } + + private Bytes32 hashTreeRoot(int curDepth, int offset) { + if (curDepth == depth) { + if (offset < ssz.size()) { + return elementTemplate.calculateHashTreeRoot(ssz, offset); + } else { + assert offset <= elementTemplate.getSszLength() * (getMaxElements() - 1); + return DEFAULT_NODE.hashTreeRoot(); + } + } else { + return Bytes32.wrap((new Crypto()).hashSHA2(Bytes.wrap( + hashTreeRoot(curDepth + 1, offset), + hashTreeRoot( + curDepth + 1, + offset + elementTemplate.getSszLength() * (1 << ((depth - curDepth) - 1)))).toArray())); + // return Hash.sha2_256( + // Bytes.wrap( + // hashTreeRoot(curDepth + 1, offset), + // hashTreeRoot( + // curDepth + 1, + // offset + elementTemplate.getSszLength() * (1 << ((depth - curDepth) - 1))))); + } + } + + @NotNull + @Override + public TreeNode get(long generalizedIndex) { + if (GIndexUtil.gIdxIsSelf(generalizedIndex)) { + return this; + } + int childIndex = GIndexUtil.gIdxGetChildIndex(generalizedIndex, depth); + int childOffset = childIndex * elementTemplate.getSszLength(); + checkArgument(childOffset < ssz.size(), "Invalid index"); + long relativeGIndex = GIndexUtil.gIdxGetRelativeGIndex(generalizedIndex, depth); + Location nodeLoc = elementTemplate.getNodeSszLocation(relativeGIndex); + if (nodeLoc.isLeaf()) { + return LeafNode.create(ssz.slice(childOffset + nodeLoc.getOffset(), nodeLoc.getLength())); + } else if (GIndexUtil.gIdxIsSelf(relativeGIndex)) { + return new SszSuperNode( + 0, elementTemplate, ssz.slice(childOffset, elementTemplate.getSszLength())); + } else { + SszNodeTemplate subTemplate = elementTemplate.getSubTemplate(relativeGIndex); + return new SszSuperNode( + 0, subTemplate, ssz.slice(childOffset + nodeLoc.getOffset(), nodeLoc.getLength())); + } + } + + @Override + public boolean iterate( + long thisGeneralizedIndex, long startGeneralizedIndex, TreeVisitor visitor) { + if (GIndexUtil.gIdxCompare(thisGeneralizedIndex, startGeneralizedIndex) == NodeRelation.Left) { + return true; + } else { + return visitor.visit(this, thisGeneralizedIndex); + } + } + + @Override + public TreeNode updated(TreeUpdates newNodes) { + if (newNodes.isEmpty()) { + return this; + } + long leftmostUpdateIndex = newNodes.getRelativeGIndex(newNodes.size() - 1); + int leftmostChildIndex = GIndexUtil.gIdxGetChildIndex(leftmostUpdateIndex, depth); + int newSszSize = (leftmostChildIndex + 1) * elementTemplate.getSszLength(); + Bytes updatedSizeSsz = + newSszSize <= ssz.size() + ? ssz + : Bytes.wrap(ssz, Bytes.wrap(new byte[newSszSize - ssz.size()])); + MutableBytes mutableCopy = updatedSizeSsz.mutableCopy(); + for (int i = 0; i < newNodes.size(); i++) { + long updateGIndex = newNodes.getRelativeGIndex(i); + int childIndex = GIndexUtil.gIdxGetChildIndex(updateGIndex, depth); + long childGIndex = GIndexUtil.gIdxGetRelativeGIndex(updateGIndex, depth); + int childOffset = childIndex * elementTemplate.getSszLength(); + MutableBytes childMutableSlice = + mutableCopy.mutableSlice(childOffset, elementTemplate.getSszLength()); + elementTemplate.update(childGIndex, newNodes.getNode(i), childMutableSlice); + } + return new SszSuperNode(depth, elementTemplate, mutableCopy); + } + + @Override + public Bytes getData() { + return ssz; + } + + @Override + public String toString() { + int sszLength = elementTemplate.getSszLength(); + return "SszSuperNode{" + + IntStream.range(0, getElementsCount()) + .mapToObj(i -> ssz.slice(i * sszLength, sszLength).toString()) + .collect(Collectors.joining(", ")) + + "}"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszType.java b/src/org/minima/system/network/base/ssz/SszType.java new file mode 100644 index 000000000..1810db0e8 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszType.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.nio.ByteOrder; +import org.apache.tuweni.bytes.Bytes; +// import tech.pegasys.teku.ssz.sos.SszByteArrayWriter; +// import tech.pegasys.teku.ssz.sos.SszDeserializeException; +// import tech.pegasys.teku.ssz.sos.SszLengthBounds; +// import tech.pegasys.teku.ssz.sos.SszReader; +// import tech.pegasys.teku.ssz.sos.SszWriter; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +/** Base class of {@link SszSchema} with SSZ serialization related methods */ +public interface SszType { + + // the size of SSZ UIn32 lengths and offsets + int SSZ_LENGTH_SIZE = 4; + + // serializes int length to SSZ 4 bytes + static Bytes sszLengthToBytes(int length) { + return Bytes.ofUnsignedInt(length, ByteOrder.LITTLE_ENDIAN); + } + + // deserializes int length from SSZ 4 bytes + static int sszBytesToLength(Bytes bytes) { + if (!bytes.slice(SSZ_LENGTH_SIZE).isZero()) { + throw new SszDeserializeException("Invalid length bytes: " + bytes); + } + int ret = bytes.slice(0, SSZ_LENGTH_SIZE).toInt(ByteOrder.LITTLE_ENDIAN); + if (ret < 0) { + throw new SszDeserializeException("Invalid length: " + ret); + } + return ret; + } + + /** Indicates whether the type is fixed or variable size */ + boolean isFixedSize(); + + /** Returns the size of the fixed SSZ part for this type */ + int getSszFixedPartSize(); + + /** Returns the size of the variable SSZ part for this type and specified backing subtree */ + int getSszVariablePartSize(TreeNode node); + + /** Calculates the full SSZ size in bytes for this type and specified backing subtree */ + default int getSszSize(TreeNode node) { + return getSszFixedPartSize() + getSszVariablePartSize(node); + } + + /** SSZ serializes the backing tree instance of this type */ + default Bytes sszSerializeTree(TreeNode node) { + SszByteArrayWriter writer = new SszByteArrayWriter(getSszSize(node)); + sszSerializeTree(node, writer); + return writer.toBytes(); + } + + /** + * SSZ serializes the backing tree of this type and returns the data as bytes 'stream' via passed + * {@code writer} + */ + int sszSerializeTree(TreeNode node, SszWriter writer); + + TreeNode sszDeserializeTree(SszReader reader) throws SszDeserializeException; + + SszLengthBounds getSszLengthBounds(); +} diff --git a/src/org/minima/system/network/base/ssz/SszUInt64.java b/src/org/minima/system/network/base/ssz/SszUInt64.java new file mode 100644 index 000000000..c93cfa994 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszUInt64.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.infrastructure.unsigned.UInt64; +// import tech.pegasys.teku.ssz.impl.AbstractSszPrimitive; +// import tech.pegasys.teku.ssz.schema.SszPrimitiveSchemas; + +public class SszUInt64 extends AbstractSszPrimitive { + + public static SszUInt64 of(UInt64 val) { + return new SszUInt64(val); + } + + private SszUInt64(UInt64 val) { + super(val, SszPrimitiveSchemas.UINT64_SCHEMA); + } + + public long longValue() { + return get().longValue(); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszVector.java b/src/org/minima/system/network/base/ssz/SszVector.java new file mode 100644 index 000000000..3733b47b6 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszVector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.schema.SszVectorSchema; + +/** + * Immutable SSZ Vector + * + * @param Type of vector elements + */ +public interface SszVector extends SszCollection { + + @Override + SszMutableVector createWritableCopy(); + + @Override + SszVectorSchema getSchema(); +} diff --git a/src/org/minima/system/network/base/ssz/SszVectorImpl.java b/src/org/minima/system/network/base/ssz/SszVectorImpl.java new file mode 100644 index 000000000..9af24f897 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszVectorImpl.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import java.util.function.Supplier; +import java.util.stream.Collectors; +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszMutableVector; +// import tech.pegasys.teku.ssz.SszVector; +// import tech.pegasys.teku.ssz.cache.ArrayIntCache; +// import tech.pegasys.teku.ssz.cache.IntCache; +// import tech.pegasys.teku.ssz.schema.SszCompositeSchema; +// import tech.pegasys.teku.ssz.schema.SszVectorSchema; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszVectorImpl extends AbstractSszCollection + implements SszVector { + + public SszVectorImpl(SszCompositeSchema schema, Supplier lazyBackingNode) { + super(schema, lazyBackingNode); + } + + public SszVectorImpl(SszCompositeSchema schema, TreeNode backingNode) { + super(schema, backingNode); + } + + public SszVectorImpl( + SszCompositeSchema schema, TreeNode backingNode, IntCache cache) { + super(schema, backingNode, cache); + } + + @Override + protected int sizeImpl() { + return (int) Long.min(Integer.MAX_VALUE, this.getSchema().getMaxLength()); + } + + @Override + public SszMutableVector createWritableCopy() { + return new SszMutableVectorImpl<>(this); + } + + @SuppressWarnings("unchecked") + @Override + public SszVectorSchema getSchema() { + return (SszVectorSchema) super.getSchema(); + } + + @Override + protected IntCache createCache() { + return size() > 16384 ? new ArrayIntCache<>() : new ArrayIntCache<>(size()); + } + + @Override + protected void checkIndex(int index) { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException( + "Invalid index " + index + " for vector with size " + size()); + } + } + + @Override + public String toString() { + return "SszVector{" + stream().map(Object::toString).collect(Collectors.joining(", ")) + "}"; + } +} diff --git a/src/org/minima/system/network/base/ssz/SszVectorSchema.java b/src/org/minima/system/network/base/ssz/SszVectorSchema.java new file mode 100644 index 000000000..792dba861 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszVectorSchema.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszVector; +// import tech.pegasys.teku.ssz.schema.collections.SszPrimitiveVectorSchema; +// import tech.pegasys.teku.ssz.schema.impl.SszVectorSchemaImpl; +// import tech.pegasys.teku.ssz.tree.GIndexUtil; + +public interface SszVectorSchema< + ElementDataT extends SszData, SszVectorT extends SszVector> + extends SszCollectionSchema { + + long MAX_VECTOR_LENGTH = 1L << (GIndexUtil.MAX_DEPTH - 1); + + default int getLength() { + long maxLength = getMaxLength(); + if (maxLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Vector size too large: " + maxLength); + } + return (int) maxLength; + } + + static SszVectorSchema create( + SszSchema elementSchema, long length) { + return create(elementSchema, length, SszSchemaHints.none()); + } + + @SuppressWarnings("unchecked") + static SszVectorSchema create( + SszSchema elementSchema, long length, SszSchemaHints hints) { + checkArgument(length >= 0 && length <= MAX_VECTOR_LENGTH); + if (elementSchema instanceof SszPrimitiveSchema) { + return (SszVectorSchema) + SszPrimitiveVectorSchema.create((SszPrimitiveSchema) elementSchema, length, hints); + } else { + return new SszVectorSchemaImpl<>(elementSchema, length, false, hints); + } + } +} diff --git a/src/org/minima/system/network/base/ssz/SszVectorSchemaImpl.java b/src/org/minima/system/network/base/ssz/SszVectorSchemaImpl.java new file mode 100644 index 000000000..e42d53c94 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszVectorSchemaImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +// import tech.pegasys.teku.ssz.SszData; +// import tech.pegasys.teku.ssz.SszVector; +// import tech.pegasys.teku.ssz.impl.SszVectorImpl; +// import tech.pegasys.teku.ssz.schema.SszSchema; +// import tech.pegasys.teku.ssz.schema.SszSchemaHints; +// import tech.pegasys.teku.ssz.tree.TreeNode; + +public class SszVectorSchemaImpl + extends AbstractSszVectorSchema> { + + SszVectorSchemaImpl(SszSchema elementType, long vectorLength) { + this(elementType, vectorLength, false, SszSchemaHints.none()); + } + + public SszVectorSchemaImpl( + SszSchema elementSchema, + long vectorLength, + boolean isListBacking, + SszSchemaHints hints) { + super(elementSchema, vectorLength, isListBacking, hints); + } + + @Override + public SszVector createFromBackingNode(TreeNode node) { + return new SszVectorImpl<>(this, node); + } +} diff --git a/src/org/minima/system/network/base/ssz/SszWriter.java b/src/org/minima/system/network/base/ssz/SszWriter.java new file mode 100644 index 000000000..bae2a610a --- /dev/null +++ b/src/org/minima/system/network/base/ssz/SszWriter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import org.apache.tuweni.bytes.Bytes; + +public interface SszWriter { + + default void write(Bytes bytes) { + write(bytes.toArrayUnsafe()); + } + + default void write(byte[] bytes) { + write(bytes, 0, bytes.length); + } + + void write(byte[] bytes, int offset, int length); +} diff --git a/src/org/minima/system/network/base/ssz/TillIndexVisitor.java b/src/org/minima/system/network/base/ssz/TillIndexVisitor.java new file mode 100644 index 000000000..6ccce3f33 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TillIndexVisitor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +//import tech.pegasys.teku.ssz.tree.GIndexUtil.NodeRelation; +import org.minima.system.network.base.ssz.GIndexUtil.NodeRelation; + +class TillIndexVisitor implements TreeVisitor { + + static TreeVisitor create(TreeVisitor delegate, long tillGeneralizedIndex) { + return new TillIndexVisitor(delegate, tillGeneralizedIndex, true); + } + + private final TreeVisitor delegate; + private final long tillGIndex; + private final boolean inclusive; + + public TillIndexVisitor(TreeVisitor delegate, long tillGIndex, boolean inclusive) { + this.delegate = delegate; + this.tillGIndex = tillGIndex; + this.inclusive = inclusive; + } + + @Override + public boolean visit(TreeNode node, long generalizedIndex) { + NodeRelation compareRes = GIndexUtil.gIdxCompare(generalizedIndex, tillGIndex); + if (inclusive && compareRes == NodeRelation.Right) { + return false; + } else if (!inclusive && (compareRes == NodeRelation.Same)) { + return false; + } else { + return delegate.visit(node, generalizedIndex); + } + } +} diff --git a/src/org/minima/system/network/base/ssz/TreeNode.java b/src/org/minima/system/network/base/ssz/TreeNode.java new file mode 100644 index 000000000..cf70290c8 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TreeNode.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static java.util.Collections.singletonList; + +import java.util.function.Consumer; +import java.util.function.Function; +import org.apache.tuweni.bytes.Bytes32; +import org.jetbrains.annotations.NotNull; + +/** + * Basic interface for Backing Tree node Backing Binary Tree concept for SSZ structures is described + * here: https://github.com/protolambda/eth-merkle-trees/blob/master/typing_partials.md#tree + * + *

Tree node is immutable by design. Any update on a tree creates new nodes which refer both new + * data nodes and old unmodified nodes + */ +public interface TreeNode { + + /** + * Calculates (if necessary) and returns `hash_tree_root` of this tree node. Worth to mention that + * `hash_tree_root` of a {@link LeafNode} is the node {@link Bytes32} content + */ + Bytes32 hashTreeRoot(); + + /** + * Gets this node descendant by its 'generalized index' + * + * @param generalizedIndex generalized index of a tree is specified here: + * https://github.com/ethereum/eth2.0-specs/blob/2787fea5feb8d5977ebee7c578c5d835cff6dc21/specs/light_client/merkle_proofs.md#generalized-merkle-tree-index + * @return node descendant + * @throws IllegalArgumentException if no node exists for the passed generalized index + */ + @NotNull + TreeNode get(long generalizedIndex); + + /** + * Iterates recursively this node children (including the node itself) in the order Self -> Left + * subtree -> Right subtree + * + *

This method can be considered low-level and mostly intended as a single implementation point + * for subclasses. Consider using higher-level methods {@link #iterateRange(long, long, + * TreeVisitor)}, {@link #iterateAll(TreeVisitor)} and {@link #iterateAll(Consumer)} + * + * @param thisGeneralizedIndex the generalized index of this node or {@link + * GIndexUtil#SELF_G_INDEX} if this node is considered the root. {@link + * TreeVisitor#visit(TreeNode, long)} index will be calculated with respect to this parameter + * @param startGeneralizedIndex The generalized index to start iteration from. All tree + * predecessor and successor nodes of a node at this index will be visited. All nodes 'to the + * left' of start node are to be skipped. The index may point to a non-existing node, in this + * case the nearest existing predecessor node would be the starting node To start iteration + * from the leftmost node use {@link GIndexUtil#LEFTMOST_G_INDEX} + * @param visitor Callback for nodes. When visitor returns false, iteration breaks + * @return true if the iteration should proceed or false to break iteration + */ + boolean iterate(long thisGeneralizedIndex, long startGeneralizedIndex, TreeVisitor visitor); + + /** + * Iterates all nodes between and including startGeneralizedIndex and endGeneralizedIndexInclusive + * in order Self -> Left subtree -> Right subtree + * + *

All tree predecessor and successor nodes of startGeneralizedIndex and + * endGeneralizedIndexInclusive nodes will be visited. All nodes 'to the left' of the start node + * and 'to the right' of the end node are to be skipped. An index may point to a non-existing + * node, in this case the nearest existing predecessor node would be considered the starting node. + * + *

To start iteration from the leftmost node specify startGeneralizedIndex equal to {@link + * GIndexUtil#LEFTMOST_G_INDEX} To iteration till the rightmost node specify + * endGeneralizedIndexInclusive equal to {@link GIndexUtil#RIGHTMOST_G_INDEX} + */ + default void iterateRange( + long startGeneralizedIndex, long endGeneralizedIndexInclusive, TreeVisitor visitor) { + iterate( + GIndexUtil.SELF_G_INDEX, + startGeneralizedIndex, + TillIndexVisitor.create(visitor, endGeneralizedIndexInclusive)); + } + + /** Iterates all tree nodes in the order Self -> Left subtree -> Right subtree */ + default void iterateAll(TreeVisitor visitor) { + iterate(GIndexUtil.SELF_G_INDEX, GIndexUtil.LEFTMOST_G_INDEX, visitor); + } + + /** Iterates all tree nodes in the order Self -> Left subtree -> Right subtree */ + default void iterateAll(Consumer simpleVisitor) { + iterateAll( + (node, __) -> { + simpleVisitor.accept(node); + return true; + }); + } + + /** + * The same as {@link #updated(long, TreeNode)} except that existing node can be used to calculate + * a new node + * + *

Three method overloads call each other in a cycle. The implementation class should override + * one of them and may override more for efficiency + * + * @see #updated(TreeUpdates) + * @see #updated(long, TreeNode) + * @see #updated(long, Function) + */ + default TreeNode updated(long generalizedIndex, Function nodeUpdater) { + TreeNode newNode = nodeUpdater.apply(get(generalizedIndex)); + return updated( + new TreeUpdates(singletonList(new TreeUpdates.Update(generalizedIndex, newNode)))); + } + + /** + * Updates the tree in a batch. + * + *

Three method overloads call each other in a cycle. The implementation class should override + * one of them and may override more for efficiency + * + * @see #updated(TreeUpdates) + * @see #updated(long, TreeNode) + * @see #updated(long, Function) + */ + default TreeNode updated(TreeUpdates newNodes) { + TreeNode ret = this; + for (int i = 0; i < newNodes.size(); i++) { + ret = ret.updated(newNodes.getRelativeGIndex(i), newNodes.getNode(i)); + } + return ret; + } + + /** + * 'Sets' a new node on place of the node at generalized index. This node and all its descendants + * are left immutable. The updated subtree node is returned. + * + *

Three method overloads call each other in a cycle. The implementation class should override + * one of them and may override more for efficiency + * + * @param generalizedIndex index of tree node to be replaced + * @param node new node either leaf of subtree root node + * @return the updated subtree root node + * @see #updated(TreeUpdates) + * @see #updated(long, TreeNode) + * @see #updated(long, Function) + */ + default TreeNode updated(long generalizedIndex, TreeNode node) { + return updated(generalizedIndex, oldNode -> node); + } +} diff --git a/src/org/minima/system/network/base/ssz/TreeNodeImpl.java b/src/org/minima/system/network/base/ssz/TreeNodeImpl.java new file mode 100644 index 000000000..785586e08 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TreeNodeImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Arrays; +import java.util.Objects; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.jetbrains.annotations.NotNull; + +abstract class TreeNodeImpl implements TreeNode { + + static class LeafNodeImpl extends TreeNodeImpl implements LeafNode { + private final byte[] data; + + public LeafNodeImpl(Bytes data) { + checkArgument(data.size() <= MAX_BYTE_SIZE); + this.data = data.toArrayUnsafe(); + } + + @Override + public Bytes getData() { + return Bytes.wrap(data); + } + + @Override + public Bytes32 hashTreeRoot() { + if (data.length == MAX_BYTE_SIZE) { + return Bytes32.wrap(data); + } else { + return Bytes32.wrap(Arrays.copyOf(data, MAX_BYTE_SIZE)); + } + } + + @Override + public TreeNode updated(TreeUpdates newNodes) { + if (newNodes.isEmpty()) { + return this; + } else { + newNodes.checkLeaf(); + return newNodes.getNode(0); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof LeafNode)) { + return false; + } + LeafNode otherLeaf = (LeafNode) o; + return Objects.equals(getData(), otherLeaf.getData()); + } + + @Override + public int hashCode() { + return getData().hashCode(); + } + + @Override + public String toString() { + return "[" + getData() + "]"; + } + } + + static class BranchNodeImpl extends TreeNodeImpl implements BranchNode { + private final TreeNode left; + private final TreeNode right; + private volatile Bytes32 cachedHash = null; + + public BranchNodeImpl(TreeNode left, TreeNode right) { + this.left = left; + this.right = right; + } + + @NotNull + @Override + public TreeNode left() { + return left; + } + + @NotNull + @Override + public TreeNode right() { + return right; + } + + @Override + public BranchNode rebind(boolean left, TreeNode newNode) { + return left ? new BranchNodeImpl(newNode, right()) : new BranchNodeImpl(left(), newNode); + } + + @Override + public TreeNode updated(TreeUpdates newNodes) { + if (newNodes.isEmpty()) { + return this; + } else if (newNodes.isFinal()) { + return newNodes.getNode(0); + } else { + Pair children = newNodes.splitAtPivot(); + return new BranchNodeImpl( + left().updated(children.getLeft()), right().updated(children.getRight())); + } + } + + @Override + public Bytes32 hashTreeRoot() { + if (cachedHash == null) { + cachedHash = BranchNode.super.hashTreeRoot(); + } + return cachedHash; + } + + @Override + public String toString() { + return left == right ? ("(2x " + left + ")") : ("(" + left + ", " + right + ')'); + } + } +} diff --git a/src/org/minima/system/network/base/ssz/TreeUpdates.java b/src/org/minima/system/network/base/ssz/TreeUpdates.java new file mode 100644 index 000000000..8f9d0b6c1 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TreeUpdates.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; + +/** + * The collection of nodes and their target generalized indexes to be updated The class also + * contains the target generalized index this set of changes is applicable to. + * + * @see TreeNode#updated(TreeUpdates) + */ +public class TreeUpdates { + + /** A single tree update with target generalized index and the new target {@link TreeNode} */ + public static class Update { + private final long generalizedIndex; + private final TreeNode newNode; + + public Update(long generalizedIndex, TreeNode newNode) { + this.generalizedIndex = generalizedIndex; + this.newNode = newNode; + } + + public long getGeneralizedIndex() { + return generalizedIndex; + } + + public TreeNode getNewNode() { + return newNode; + } + } + + /** Convenient collector for the stream with {@link Update} elements */ + public static Collector collector() { + return Collectors.collectingAndThen(Collectors.toList(), TreeUpdates::new); + } + + private final List gIndexes; + private final List nodes; + + private final long prefix; + private final int heightFromLeaf; + + /** + * Creates a new instance of TreeNodes + * + * @param updates the list of {@link Update}s + *

NOTE: the list should conform to the following prerequisites: + *

    + *
  • all generalized indexes are unique + *
  • the list should be sorted by the target generalized index + *
  • the generalized indexes should be on the same tree level. I.e. the highest order bit + * should be the same for all indexes + *
+ * + * @throws IllegalArgumentException if the list doesn't conform to above restrictions + */ + public TreeUpdates(List updates) { + this( + updates.stream().map(Update::getGeneralizedIndex).collect(Collectors.toList()), + updates.stream().map(Update::getNewNode).collect(Collectors.toList())); + } + + private TreeUpdates(List gIndexes, List nodes) { + this(gIndexes, nodes, 1, getDepthAndValidate(gIndexes)); + } + + private static TreeUpdates create( + List gIndexes, List nodes, long prefix, int heightFromLeaf) { + return new TreeUpdates(gIndexes, nodes, prefix, heightFromLeaf); + } + + private TreeUpdates(List gIndexes, List nodes, long prefix, int heightFromLeaf) { + assert gIndexes.size() == nodes.size(); + + this.gIndexes = gIndexes; + this.nodes = nodes; + this.prefix = prefix; + this.heightFromLeaf = heightFromLeaf; + } + + /** + * Split the nodes to left and right subtree subsets according the target generalized index + * + * @return the pair of node updates for left and right subtrees with accordingly adjusted target + * generalized indexes + */ + public Pair splitAtPivot() { + if (heightFromLeaf <= 0) { + throw new IllegalStateException("Can't split leaf update"); + } + long lPrefix = prefix << 1; + long rPrefix = lPrefix | 1; + long pivotGIndex = rPrefix << (heightFromLeaf - 1); + + int idx = Collections.binarySearch(gIndexes, pivotGIndex); + int insIdx = idx < 0 ? -idx - 1 : idx; + return Pair.of( + TreeUpdates.create( + gIndexes.subList(0, insIdx), nodes.subList(0, insIdx), lPrefix, heightFromLeaf - 1), + TreeUpdates.create( + gIndexes.subList(insIdx, gIndexes.size()), + nodes.subList(insIdx, nodes.size()), + rPrefix, + heightFromLeaf - 1)); + } + + /** Number of updated nodes in this set */ + public int size() { + return gIndexes.size(); + } + + public boolean isEmpty() { + return size() == 0; + } + + /** Gets generalized index for update at position [index] */ + @VisibleForTesting + long getGIndex(int index) { + return gIndexes.get(index); + } + + /** Calculates and returns relative generalized index */ + public long getRelativeGIndex(int index) { + return GIndexUtil.gIdxGetRelativeGIndex(gIndexes.get(index), GIndexUtil.gIdxGetDepth(prefix)); + } + + /** Gets new tree node for update at position [index] */ + public TreeNode getNode(int index) { + return nodes.get(index); + } + + private static int getDepthAndValidate(List gIndexes) { + if (gIndexes.isEmpty()) { + return 0; + } + long highestBit = Long.highestOneBit(gIndexes.get(0)); + long mask = highestBit - 1; + long checkMask = ~mask; + + long lastGIdx = -1; + for (int i = 0; i < gIndexes.size(); i++) { + long gIdx = gIndexes.get(i); + if (gIdx < 1) { + throw new IllegalArgumentException("Invalid gIndex: " + gIdx); + } + if (gIdx <= lastGIdx) { + throw new IllegalArgumentException("Invalid gIndex ordering: " + gIndexes); + } + if ((gIdx & checkMask) != highestBit) { + throw new IllegalArgumentException("Indexes are of different depth: [0] and [" + i + "]"); + } + lastGIdx = gIdx; + } + return Long.bitCount(mask); + } + + /** + * Checks if this instance is correct for the leaf node + * + * @throws IllegalArgumentException if not correct + */ + public void checkLeaf() { + if (heightFromLeaf != 0) { + throw new IllegalArgumentException( + "Non-zero heightFromLeaf for the leaf node: " + heightFromLeaf); + } + if (gIndexes.size() != 1) { + throw new IllegalArgumentException( + "Number of nodes should be 1 for a leaf node: " + gIndexes.size()); + } + if (gIndexes.get(0) != prefix) { + throw new IllegalArgumentException( + "Leaf gIndex != prefix: " + gIndexes.get(0) + " != " + prefix); + } + } + + /** Indicates that this update should be applied to the node target generalized index */ + public boolean isFinal() { + return (gIndexes.size() == 1 && gIndexes.get(0) == prefix); + } +} diff --git a/src/org/minima/system/network/base/ssz/TreeUtil.java b/src/org/minima/system/network/base/ssz/TreeUtil.java new file mode 100644 index 000000000..e32af6a43 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TreeUtil.java @@ -0,0 +1,200 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.tuweni.bytes.Bytes; +import org.minima.system.network.base.ssz.TreeNodeImpl.BranchNodeImpl; +//import tech.pegasys.teku.ssz.tree.TreeNodeImpl.BranchNodeImpl; +//import tech.pegasys.teku.ssz.tree.TreeNodeImpl.LeafNodeImpl; +import org.minima.system.network.base.ssz.TreeNodeImpl.LeafNodeImpl; + +/** Misc Backing binary tree utils */ +public class TreeUtil { + + static class ZeroLeafNode extends LeafNodeImpl { + public ZeroLeafNode(int size) { + super(Bytes.wrap(new byte[size])); + } + + @Override + public String toString() { + return "(" + getData() + ")"; + } + } + + private static class ZeroBranchNode extends BranchNodeImpl { + private final int height; + + public ZeroBranchNode(TreeNode left, TreeNode right, int height) { + super(left, right); + this.height = height; + } + + @Override + public String toString() { + return "(ZeroBranch-" + height + ")"; + } + } + + @VisibleForTesting public static final TreeNode[] ZERO_TREES; + + static { + ZERO_TREES = new TreeNode[64]; + ZERO_TREES[0] = LeafNode.EMPTY_LEAF; + for (int i = 1; i < ZERO_TREES.length; i++) { + ZERO_TREES[i] = new ZeroBranchNode(ZERO_TREES[i - 1], ZERO_TREES[i - 1], i); + ZERO_TREES[i].hashTreeRoot(); // pre-cache + } + } + + public static int bitsCeilToBytes(int bits) { + return (bits + 7) / 8; + } + + public static long bitsCeilToBytes(long bits) { + return (bits + 7) / 8; + } + + /** + * Creates a binary tree with `nextPowerOf2(maxLength)` width and following leaf nodes + * [zeroElement] * maxLength + [ZERO_LEAF] * (nextPowerOf2(maxLength) - maxLength) + * + * + * @param maxLength max number of leaf nodes + * @param defaultNode default leaf element. For complex vectors it could be default vector element + * struct subtree + */ + public static TreeNode createDefaultTree(long maxLength, TreeNode defaultNode) { + return createTree( + defaultNode, LeafNode.EMPTY_LEAF.equals(defaultNode) ? 0 : maxLength, treeDepth(maxLength)); + } + + /** Creates a binary tree of width `nextPowerOf2(leafNodes.size())` with specific leaf nodes */ + public static TreeNode createTree(List children) { + return createTree(children, treeDepth(children.size())); + } + + private static TreeNode createTree(TreeNode defaultNode, long defaultNodesCount, int depth) { + if (defaultNodesCount == 0) { + return ZERO_TREES[depth]; + } else if (depth == 0) { + checkArgument(defaultNodesCount == 1); + return defaultNode; + } else { + long leftNodesCount = Math.min(defaultNodesCount, 1 << (depth - 1)); + long rightNodesCount = defaultNodesCount - leftNodesCount; + TreeNode lTree = createTree(defaultNode, leftNodesCount, depth - 1); + TreeNode rTree = + leftNodesCount == rightNodesCount + ? lTree + : createTree(defaultNode, rightNodesCount, depth - 1); + return new BranchNodeImpl(lTree, rTree); + } + } + + public static TreeNode createTree(List leafNodes, int depth) { + if (leafNodes.isEmpty()) { + return ZERO_TREES[depth]; + } else if (depth == 0) { + checkArgument(leafNodes.size() == 1); + return leafNodes.get(0); + } else { + long index = 1L << (depth - 1); + int iIndex = index > leafNodes.size() ? leafNodes.size() : (int) index; + + List leftSublist = leafNodes.subList(0, iIndex); + List rightSublist = leafNodes.subList(iIndex, leafNodes.size()); + return BranchNode.create( + createTree(leftSublist, depth - 1), createTree(rightSublist, depth - 1)); + } + } + + public static TreeNode createTree( + List leafNodes, TreeNode defaultNode, int depth) { + if (leafNodes.isEmpty()) { + if (depth > 0) { + TreeNode defaultChild = createTree(leafNodes, defaultNode, depth - 1); + return BranchNode.create(defaultChild, defaultChild); + } else { + return defaultNode; + } + } else if (depth == 0) { + checkArgument(leafNodes.size() == 1); + return leafNodes.get(0); + } else { + long index = 1L << (depth - 1); + int iIndex = index > leafNodes.size() ? leafNodes.size() : (int) index; + + List leftSublist = leafNodes.subList(0, iIndex); + List rightSublist = leafNodes.subList(iIndex, leafNodes.size()); + return BranchNode.create( + createTree(leftSublist, defaultNode, depth - 1), + createTree(rightSublist, defaultNode, depth - 1)); + } + } + + public static long nextPowerOf2(long x) { + return x <= 1 ? 1 : Long.highestOneBit(x - 1) << 1; + } + + public static int treeDepth(long maxChunks) { + return Long.bitCount(nextPowerOf2(maxChunks) - 1); + } + + /** + * Iterate all leaf tree nodes starting from the node with general index {@code fromGeneralIndex} + * (including all node descendants if this is a branch node) and ending with the node with general + * index {@code toGeneralIndex} inclusive (including all node descendants if this is a branch + * node). On every {@link LeafNode} the supplied {@code visitor} is invoked. + */ + @VisibleForTesting + static void iterateLeaves( + TreeNode node, long fromGeneralIndex, long toGeneralIndex, Consumer visitor) { + node.iterateRange( + fromGeneralIndex, + toGeneralIndex, + (n, idx) -> { + if (n instanceof LeafNode) { + visitor.accept((LeafNode) n); + } + return true; + }); + } + + public static void iterateLeavesData( + TreeNode node, long fromGeneralIndex, long toGeneralIndex, Consumer visitor) { + node.iterateRange( + fromGeneralIndex, + toGeneralIndex, + (n, idx) -> { + if (n instanceof LeafDataNode) { + visitor.accept(((LeafDataNode) n).getData()); + } + return true; + }); + } + + public static Bytes concatenateLeavesData(TreeNode tree) { + List leavesData = new ArrayList<>(); + iterateLeavesData( + tree, GIndexUtil.LEFTMOST_G_INDEX, GIndexUtil.RIGHTMOST_G_INDEX, leavesData::add); + return Bytes.wrap(leavesData.toArray(new Bytes[0])); + } +} diff --git a/src/org/minima/system/network/base/ssz/TreeVisitor.java b/src/org/minima/system/network/base/ssz/TreeVisitor.java new file mode 100644 index 000000000..4a26f63d1 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/TreeVisitor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +/** Visitor callback interface for traversing binary tree {@link TreeNode}s */ +public interface TreeVisitor { + + /** + * 'Visits' a tree node with specifying its generalized index + * + * @return true if the visitor wishes to continue or false if further iteration should break + */ + boolean visit(TreeNode node, long generalizedIndex); +} diff --git a/src/org/minima/system/network/base/ssz/UInt64.java b/src/org/minima/system/network/base/ssz/UInt64.java new file mode 100644 index 000000000..dcbd3d326 --- /dev/null +++ b/src/org/minima/system/network/base/ssz/UInt64.java @@ -0,0 +1,530 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.minima.system.network.base.ssz; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.stream.Stream; + +/** An unsigned 64-bit integer. All instances are immutable. */ +public final class UInt64 implements Comparable { + + private static final long UNSIGNED_MASK = 0x7fffffffffffffffL; + private static final long HIGH_MASK = 0xffffffff00000000L; + private static final long LOW_MASK = 0x00000000ffffffffL; + + public static final int BYTES = 8; + + public static final UInt64 ZERO = new UInt64(0); + public static final UInt64 ONE = new UInt64(1); + public static final UInt64 MAX_VALUE = new UInt64(-1L); + + private final long value; + + private UInt64(final long value) { + this.value = value; + } + + /** + * Create a UInt64 from a long value. The value is treated as signed and must be >= 0. + * + * @param value the signed value to convert to UInt64 + * @return UInt64 value equal to the supplied signed value + * @throws IllegalArgumentException if the value is negative + */ + public static UInt64 valueOf(final long value) { + checkPositive(value); + return fromLongBits(value); + } + + /** + * Parse the string value into a UInt64. + * + * @param value the value to parse + * @return UInt64 parsed from value + * @throws NumberFormatException if value is not a valid unsigned long. + */ + public static UInt64 valueOf(final String value) { + return fromLongBits(Long.parseUnsignedLong(value)); + } + + /** + * Create a UInt64 from a {@link BigInteger} value. The value is treated as signed and must be >= + * 0 and less than {@link #MAX_VALUE}. + * + * @param value the signed value to convert to UInt64 + * @return UInt64 value equal to the supplied signed value + * @throws IllegalArgumentException if the value is negative or too large + */ + public static UInt64 valueOf(final BigInteger value) { + checkArgument( + value.signum() >= 0 && value.bitLength() <= Long.SIZE, + "value (%s) is outside the range for uint64", + value); + return fromLongBits(value.longValue()); + } + + /** + * Create a UInt64 from an unsigned long. The value is treated as unsigned. + * + * @param value the unsigned value to create as a UInt64. + * @return the created UInt64. + */ + public static UInt64 fromLongBits(final long value) { + return value == 0 ? ZERO : new UInt64(value); + } + + /** + * Return the result of adding this value and the specified one. + * + * @param other the value to add. Treated as signed. + * @return a new UInt64 equal to this value + the specified value. + * @throws ArithmeticException if the result exceeds {@link #MAX_VALUE} + * @throws IllegalArgumentException if the specified value is negative + */ + public UInt64 plus(final long other) { + checkPositive(other); + return plus(this.value, other); + } + + /** + * Increment this value by one and return the result. + * + * @return The result of incrementing this value by 1. + */ + public UInt64 increment() { + return plus(1); + } + + /** + * Decrement this value by one and return the result. + * + * @return The result of decrementing this value by 1. + */ + public UInt64 decrement() { + return minus(1); + } + + /** + * Return the result of adding this value and the specified one. + * + * @param other the unsigned value to add. + * @return a new UInt64 equal to this value + the specified value. + * @throws ArithmeticException if the result exceeds {@link #MAX_VALUE} + */ + public UInt64 plus(final UInt64 other) { + return plus(value, other.value); + } + + private UInt64 plus(final long longBits1, final long longBits2) { + if (longBits1 != 0 && Long.compareUnsigned(longBits2, MAX_VALUE.longValue() - longBits1) > 0) { + throw new ArithmeticException("uint64 overflow"); + } + return fromLongBits(longBits1 + longBits2); + } + + /** + * Return the result of subtracting the specified value from this one. + * + * @param other the value to subtract, treated as signed. + * @return a new UInt64 equal to this value minus the specified value. + * @throws ArithmeticException if the result is less than zero. + * @throws IllegalArgumentException if the specified value is negative + */ + public UInt64 minus(final long other) { + checkPositive(other); + return minus(value, other); + } + + /** + * Return the result of subtracting the specified value from this one. + * + * @param other the value to subtract. + * @return a new UInt64 equal to this value minus the specified value. + * @throws ArithmeticException if the result is less than zero. + */ + public UInt64 minus(final UInt64 other) { + return minus(value, other.value); + } + + /** + * Return the result of subtracting the specified value from this one. If the operation would + * cause an underflow, an empty result is returned. + * + * @param other the value to subtract. + * @return a new UInt64 equal to this value minus the specified value. + */ + public Optional safeMinus(final long other) { + checkPositive(other); + if (Long.compareUnsigned(value, other) < 0) { + return Optional.empty(); + } + + return Optional.of(fromLongBits(value - other)); + } + + /** + * Return the result of subtracting the specified value from this one. If the operation would + * cause an underflow, an empty result is returned. + * + * @param other the value to subtract. + * @return a new UInt64 equal to this value minus the specified value. + */ + public Optional safeMinus(final UInt64 other) { + if (isLessThan(other)) { + return Optional.empty(); + } + return Optional.of(fromLongBits(value - other.value)); + } + + private UInt64 minus(final long longBits1, final long longBits2) { + if (Long.compareUnsigned(longBits1, longBits2) < 0) { + throw new ArithmeticException("uint64 underflow"); + } + return fromLongBits(longBits1 - longBits2); + } + + public UInt64 minusMinZero(final long other) { + return minusMinZero(valueOf(other)); + } + + public UInt64 minusMinZero(final UInt64 other) { + return isGreaterThan(other) ? minus(other) : ZERO; + } + + /** + * Return the result of multiplying the specified value with this one. + * + * @param other the value to multiply, treated as signed. + * @return a new UInt64 equal to this value times the specified value. + * @throws ArithmeticException if the result is exceeds {@link #MAX_VALUE} + * @throws IllegalArgumentException if the specified value is negative + */ + public UInt64 times(final long other) { + checkPositive(other); + return times(value, other); + } + + /** + * Return the result of multiplying the specified value with this one. + * + * @param other the value to multiply. + * @return a new UInt64 equal to this value times the specified value. + * @throws ArithmeticException if the result is exceeds {@link #MAX_VALUE} + */ + public UInt64 times(final UInt64 other) { + return times(value, other.value); + } + + /** Naive long-multiplication is quite efficient */ + private UInt64 times(final long longBits1, final long longBits2) { + if (Long.numberOfLeadingZeros(longBits1) + Long.numberOfLeadingZeros(longBits2) >= 64) { + return UInt64.fromLongBits(longBits1 * longBits2); + } + final long longBits1Hi = longBits1 >>> 32; + final long longBits1Lo = longBits1 & LOW_MASK; + final long longBits2Hi = longBits2 >>> 32; + final long longBits2Lo = longBits2 & LOW_MASK; + if (longBits1Hi * longBits2Hi != 0) { + throw new ArithmeticException("uint64 overflow"); + } + // One or the other of longBits1Hi and longBits2Hi is zero + final long crossProduct = + (longBits1Hi == 0) ? longBits1Lo * longBits2Hi : longBits1Hi * longBits2Lo; + if ((crossProduct & HIGH_MASK) != 0) { + throw new ArithmeticException("uint64 overflow"); + } + return plus(crossProduct << 32, longBits1Lo * longBits2Lo); + } + + /** + * Return the result of dividing this value by the specified value. + * + * @param divisor the value to divide by, treated as signed. + * @return a new UInt64 equal to this value divided by the specified value. + * @throws ArithmeticException if the specified divisor is 0. + * @throws IllegalArgumentException if the specified value is negative + */ + public UInt64 dividedBy(final long divisor) { + checkPositive(divisor); + return dividedBy(value, divisor); + } + + /** + * Return the result of dividing this value by the specified value. + * + * @param divisor the value to divide by. + * @return a new UInt64 equal to this value divided by the specified value. + * @throws ArithmeticException if the specified divisor is 0. + */ + public UInt64 dividedBy(final UInt64 divisor) { + return dividedBy(value, divisor.value); + } + + private UInt64 dividedBy(final long unsignedDividend, final long unsignedDivisor) { + return fromLongBits(Long.divideUnsigned(unsignedDividend, unsignedDivisor)); + } + + /** + * Returns this value modulo the specified value. + * + * @param divisor the divisor, treated as signed. + * @return a new UInt64 equal to this value modulo the specified value. + * @throws ArithmeticException if the specified divisor is 0. + * @throws IllegalArgumentException if the specified value is negative + */ + public UInt64 mod(final long divisor) { + checkPositive(divisor); + return mod(value, divisor); + } + + /** + * Returns this value modulo the specified value. + * + * @param divisor the divisor. + * @return a new UInt64 equal to this value modulo the specified value. + * @throws ArithmeticException if the specified divisor is 0. + */ + public UInt64 mod(final UInt64 divisor) { + return mod(value, divisor.value); + } + + private UInt64 mod(final long dividendBits, final long divisorBits) { + return fromLongBits(Long.remainderUnsigned(dividendBits, divisorBits)); + } + + /** + * Return the larger of this value or the specified value. + * + * @param other the value to compare with + * @return the larger value + */ + public UInt64 max(final UInt64 other) { + return compareTo(other) >= 0 ? this : other; + } + + /** + * Return the larger of this value or the specified value. + * + * @param other the value to compare with, treated as signed + * @return the larger value + */ + public UInt64 max(final long other) { + checkPositive(other); + return Long.compareUnsigned(value, other) >= 0 ? this : UInt64.valueOf(other); + } + + /** + * Return the smaller of this value or the specified value. + * + * @param other the value to compare with + * @return the larger value + */ + public UInt64 min(final UInt64 other) { + return compareTo(other) >= 0 ? other : this; + } + + /** + * Return the smaller of this value or the specified value. + * + * @param other the value to compare with, treated as signed + * @return the larger value + */ + public UInt64 min(final long other) { + checkPositive(other); + return Long.compareUnsigned(value, other) <= 0 ? this : UInt64.valueOf(other); + } + + @Override + public int compareTo(final UInt64 o) { + return Long.compareUnsigned(value, o.value); + } + + public int compareTo(final long other) { + checkPositive(other); + return Long.compareUnsigned(value, other); + } + + /** + * Returns true if this value is zero. + * + * @return true if this value is zero. + */ + public boolean isZero() { + return value == 0; + } + + /** + * Returns true if this value is strictly greater than the specified value. + * + * @param other the value to compare to + * @return true if this value is strictly greater than the specified value + */ + public boolean isGreaterThan(final UInt64 other) { + return compareTo(other) > 0; + } + + /** + * Returns true if this value is strictly greater than the specified value. + * + * @param other the value to compare to + * @return true if this value is strictly greater than the specified value + */ + public boolean isGreaterThan(final long other) { + return compareTo(other) > 0; + } + + /** + * Returns true if this value is greater than or equal to the specified value. + * + * @param other the value to compare to + * @return true if this value is greater or equal than the specified value + */ + public boolean isGreaterThanOrEqualTo(final UInt64 other) { + return compareTo(other) >= 0; + } + + /** + * Returns true if this value is greater than or equal to the specified value. + * + * @param other the value to compare to + * @return true if this value is greater or equal than the specified value + */ + public boolean isGreaterThanOrEqualTo(final long other) { + return compareTo(other) >= 0; + } + + /** + * Returns true if this value is strictly less than the specified value. + * + * @param other the value to compare to + * @return true if this value is strictly less than the specified value + */ + public boolean isLessThan(final UInt64 other) { + return compareTo(other) < 0; + } + + /** + * Returns true if this value is strictly less than the specified value. + * + * @param other the value to compare to + * @return true if this value is strictly less than the specified value + */ + public boolean isLessThan(final long other) { + return compareTo(other) < 0; + } + + /** + * Returns true if this value is less than or equal to the specified value. + * + * @param other the value to compare to + * @return true if this value is less than or equal to the specified value + */ + public boolean isLessThanOrEqualTo(final UInt64 other) { + return compareTo(other) <= 0; + } + + /** + * Returns true if this value is less than or equal to the specified value. + * + * @param other the value to compare to + * @return true if this value is less than or equal to the specified value + */ + public boolean isLessThanOrEqualTo(final long other) { + return compareTo(other) <= 0; + } + + /** + * Returns the value as a long. If this value exceeds {@link Long#MAX_VALUE} the result will be + * negative. + * + * @return this value as an unsigned long + */ + public long longValue() { + return value; + } + + /** + * This value as an int. + * + * @return this value as a signed int. + * @throws ArithmeticException if the value is greater than {@link Integer#MAX_VALUE} + */ + public int intValue() { + final int intValue = Math.toIntExact(value); + if (intValue < 0) { + throw new ArithmeticException("integer overflow"); + } + return intValue; + } + + /** + * This value as a double. + * + * @return this value as a double. + */ + public double doubleValue() { + return value; + } + + /** + * Returns this value as a {@link BigInteger} + * + * @return this value as a BigInteger + */ + public BigInteger bigIntegerValue() { + return toUnsignedBigInteger(value); + } + // From Guava UnsignedLong.bigIntegerValue(). Apache 2 license. + + private static BigInteger toUnsignedBigInteger(final long value) { + BigInteger bigInt = BigInteger.valueOf(value & UNSIGNED_MASK); + if (value < 0) { + bigInt = bigInt.setBit(Long.SIZE - 1); + } + return bigInt; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final UInt64 uInt64 = (UInt64) o; + return value == uInt64.value; + } + + @Override + public int hashCode() { + return Long.hashCode(value); + } + + @Override + public String toString() { + return Long.toUnsignedString(value); + } + + private static void checkPositive(final long other) { + checkArgument(other >= 0, "value (%s) must be >= 0", other); + } + + public static Stream range(final UInt64 fromInclusive, final UInt64 toExclusive) { + return Stream.iterate(fromInclusive, value -> value.isLessThan(toExclusive), UInt64::increment); + } +} diff --git a/src/resources/log4j2-test.properties b/src/resources/log4j2-test.properties new file mode 100644 index 000000000..24bdc81ee --- /dev/null +++ b/src/resources/log4j2-test.properties @@ -0,0 +1,3 @@ +# Root logger option +log4j.rootLogger=TRACE, stdout + diff --git a/src/resources/log4j2.xml b/src/resources/log4j2.xml new file mode 100644 index 000000000..d953bc3fc --- /dev/null +++ b/src/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +