From e2025c5752990b56f4b810cedb3aa07d2b03c15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 10 Jun 2020 17:21:32 +0200 Subject: [PATCH 01/70] Prometheus & Grafana refactoring - moved "process_dashboard.nim" in "tools/" - README: made Witti the documented testnet and added instructions for getting metrics out of the local node - moved Prometheus config file generation in its own script - the static Grafana dashboard definition now covers all nodes, using a variable; only the remote testnet dashboards need to be dynamically generated - "launch_local_testnet.sh" no longer needs a "--grafana" option --- .gitignore | 2 + Makefile | 2 +- README.md | 27 ++- .../beacon_nodes_Grafana_dashboard.json | 124 +++++++----- media/monitoring.png | Bin 101351 -> 201482 bytes scripts/connect_to_testnet.nims | 3 + scripts/launch_local_testnet.sh | 37 +--- scripts/make_prometheus_config.sh | 92 +++++++++ scripts/reset_testnet.sh | 5 +- tests/simulation/.gitignore | 1 - tests/simulation/process_dashboard.nim | 180 ------------------ tests/simulation/start.sh | 31 +-- tools/process_dashboard.nim | 126 ++++++++++++ 13 files changed, 333 insertions(+), 297 deletions(-) rename tests/simulation/beacon-chain-sim-node0-Grafana-dashboard.json => grafana/beacon_nodes_Grafana_dashboard.json (91%) create mode 100755 scripts/make_prometheus_config.sh delete mode 100644 tests/simulation/process_dashboard.nim create mode 100644 tools/process_dashboard.nim diff --git a/.gitignore b/.gitignore index d2fc23dc3..c73c8da16 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ build/ /local_testnet_data*/ +# Prometheus db +/data # Grafana dashboards /docker/*.json diff --git a/Makefile b/Makefile index 85c2b87f0..6eea6ad1b 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ TOOLS_DIRS := \ ncli \ nbench \ research \ - tests/simulation + tools TOOLS_CSV := $(subst $(SPACE),$(COMMA),$(TOOLS)) .PHONY: \ diff --git a/README.md b/README.md index ad0519803..e023f6c65 100644 --- a/README.md +++ b/README.md @@ -102,16 +102,29 @@ apt install build-essential git libpcre3-dev Nimbus connects to any of the testnets published in the [eth2-clients/eth2-testnets repo](https://github.com/eth2-clients/eth2-testnets/tree/master/nimbus). -Once the [prerequisites](#prerequisites) are installed you can connect to testnet0 with the following commands: +Once the [prerequisites](#prerequisites) are installed you can connect to the [Witti testnet](https://github.com/goerli/witti) with the following commands: ```bash git clone https://github.com/status-im/nim-beacon-chain cd nim-beacon-chain -make testnet0 # This will build Nimbus and all other dependencies - # and connect you to testnet0 +make witti # This will build Nimbus and all other dependencies + # and connect you to Witti ``` -The testnets are restarted once per week, usually on Monday evenings (UTC)) and integrate the changes for the past week. +### Getting metrics from a local testnet client + +```bash +# the primitive HTTP server started to serve the metrics is considered insecure +make NIMFLAGS="-d:insecure" witti +``` + +You can now see the raw metrics on http://127.0.0.1:8008/metrics but they're not very useful like this, so let's feed them to a Prometheus instance: + +```bash +prometheus --config.file=build/data/shared_witti/prometheus.yml +``` + +For some pretty pictures, get [Grafana](https://grafana.com/) up and running, then import the dashboard definition in "grafana/beacon\_nodes\_Grafana\_dashboard.json". ## Interop (for other Eth2 clients) @@ -178,8 +191,8 @@ The [generic instructions from the Nimbus repo](https://github.com/status-im/nim Specific steps: ```bash -# This will generate the Prometheus config and the Grafana dashboard on the fly, -# based on the number of nodes (which you can control by passing something like NODES=6 to `make`). +# This will generate the Prometheus config on the fly, based on the number of +# nodes (which you can control by passing something like NODES=6 to `make`). # The `-d:insecure` flag starts an HTTP server from which the Prometheus daemon will pull the metrics. make VALIDATORS=192 NODES=6 USER_NODES=0 NIMFLAGS="-d:insecure" eth2_network_simulation @@ -188,7 +201,7 @@ cd tests/simulation/prometheus prometheus ``` -The dashboard you need to import in Grafana is "tests/simulation/beacon-chain-sim-all-nodes-Grafana-dashboard.json". +The dashboard you need to import in Grafana is "grafana/beacon\_nodes\_Grafana\_dashboard.json". ![monitoring dashboard](./media/monitoring.png) diff --git a/tests/simulation/beacon-chain-sim-node0-Grafana-dashboard.json b/grafana/beacon_nodes_Grafana_dashboard.json similarity index 91% rename from tests/simulation/beacon-chain-sim-node0-Grafana-dashboard.json rename to grafana/beacon_nodes_Grafana_dashboard.json index da256b59a..65e6fbc14 100644 --- a/tests/simulation/beacon-chain-sim-node0-Grafana-dashboard.json +++ b/grafana/beacon_nodes_Grafana_dashboard.json @@ -101,27 +101,27 @@ "steppedLine": false, "targets": [ { - "expr": "rate(process_cpu_seconds_total{node=\"0\"}[2s]) * 100", + "expr": "rate(process_cpu_seconds_total{node=\"${node}\"}[2s]) * 100", "legendFormat": "CPU usage %", "refId": "A" }, { - "expr": "process_open_fds{node=\"0\"}", + "expr": "process_open_fds{node=\"${node}\"}", "legendFormat": "open file descriptors", "refId": "C" }, { - "expr": "process_resident_memory_bytes{node=\"0\"}", + "expr": "process_resident_memory_bytes{node=\"${node}\"}", "legendFormat": "RSS", "refId": "D" }, { - "expr": "nim_gc_mem_bytes{node=\"0\"}", + "expr": "nim_gc_mem_bytes{node=\"${node}\"}", "legendFormat": "Nim GC mem total", "refId": "F" }, { - "expr": "nim_gc_mem_occupied_bytes{node=\"0\"}", + "expr": "nim_gc_mem_occupied_bytes{node=\"${node}\"}", "legendFormat": "Nim GC mem used", "refId": "G" } @@ -130,7 +130,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "resources #0", + "title": "resources #${node}", "tooltip": { "shared": true, "sort": 0, @@ -210,12 +210,12 @@ "steppedLine": false, "targets": [ { - "expr": "libp2p_open_bufferstream{node=\"0\"}", + "expr": "libp2p_open_bufferstream{node=\"${node}\"}", "legendFormat": "BufferStream", "refId": "A" }, { - "expr": "libp2p_open_connection{node=\"0\"}", + "expr": "libp2p_open_connection{node=\"${node}\"}", "legendFormat": "Connection", "refId": "B" } @@ -224,7 +224,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "open streams #0", + "title": "open streams #${node}", "tooltip": { "shared": true, "sort": 0, @@ -304,13 +304,13 @@ "steppedLine": false, "targets": [ { - "expr": "beacon_current_validators{node=\"0\"}", + "expr": "beacon_current_validators{node=\"${node}\"}", "interval": "", "legendFormat": "current validators", "refId": "A" }, { - "expr": "beacon_current_live_validators{node=\"0\"}", + "expr": "beacon_current_live_validators{node=\"${node}\"}", "interval": "", "legendFormat": "current live validators", "refId": "B" @@ -320,7 +320,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "validators #0", + "title": "validators #${node}", "tooltip": { "shared": true, "sort": 0, @@ -405,7 +405,7 @@ "steppedLine": false, "targets": [ { - "expr": "nim_gc_heap_instance_occupied_bytes{node=\"0\"}", + "expr": "nim_gc_heap_instance_occupied_bytes{node=\"${node}\"}", "interval": "", "legendFormat": "{{type_name}}", "refId": "A" @@ -415,7 +415,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "GC heap objects #0", + "title": "GC heap objects #${node}", "tooltip": { "shared": true, "sort": 0, @@ -493,7 +493,7 @@ "steppedLine": false, "targets": [ { - "expr": "beacon_state_data_cache_hits_total{node=\"0\"} * 100 / (beacon_state_data_cache_hits_total{node=\"0\"} + beacon_state_data_cache_misses_total{node=\"0\"})", + "expr": "beacon_state_data_cache_hits_total{node=\"${node}\"} * 100 / (beacon_state_data_cache_hits_total{node=\"${node}\"} + beacon_state_data_cache_misses_total{node=\"${node}\"})", "interval": "", "legendFormat": "cache hit rate", "refId": "A" @@ -503,7 +503,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "pool.cachedStates #0", + "title": "pool.cachedStates #${node}", "tooltip": { "shared": true, "sort": 0, @@ -587,7 +587,7 @@ "steppedLine": false, "targets": [ { - "expr": "sqlite3_memory_used_bytes{node=\"0\"}", + "expr": "sqlite3_memory_used_bytes{node=\"${node}\"}", "interval": "", "legendFormat": "Memory used", "refId": "A" @@ -597,7 +597,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "SQLite3 #0", + "title": "SQLite3 #${node}", "tooltip": { "shared": true, "sort": 0, @@ -698,14 +698,14 @@ "tableColumn": "", "targets": [ { - "expr": "process_resident_memory_bytes{node=\"0\"}", + "expr": "process_resident_memory_bytes{node=\"${node}\"}", "refId": "A" } ], "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "RSS mem #0", + "title": "RSS mem #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -781,14 +781,14 @@ "tableColumn": "", "targets": [ { - "expr": "rate(process_cpu_seconds_total{node=\"0\"}[2s]) * 100", + "expr": "rate(process_cpu_seconds_total{node=\"${node}\"}[2s]) * 100", "refId": "A" } ], "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "CPU usage #0", + "title": "CPU usage #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -864,7 +864,7 @@ "tableColumn": "", "targets": [ { - "expr": "beacon_slot{node=\"0\"}", + "expr": "beacon_slot{node=\"${node}\"}", "interval": "", "legendFormat": "", "refId": "A" @@ -873,7 +873,7 @@ "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "current slot #0", + "title": "current slot #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1034,14 +1034,14 @@ "tableColumn": "", "targets": [ { - "expr": "beacon_attestations_received_total{node=\"0\"}", + "expr": "beacon_attestations_received_total{node=\"${node}\"}", "refId": "A" } ], "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "att'ns recv'd #0", + "title": "att'ns recv'd #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1097,13 +1097,13 @@ "steppedLine": false, "targets": [ { - "expr": "rate(beacon_blocks_received_total{node=\"0\"}[4s]) * 3", + "expr": "rate(beacon_blocks_received_total{node=\"${node}\"}[4s]) * 3", "interval": "", "legendFormat": "received", "refId": "B" }, { - "expr": "rate(beacon_blocks_proposed_total{node=\"0\"}[4s]) * 3", + "expr": "rate(beacon_blocks_proposed_total{node=\"${node}\"}[4s]) * 3", "interval": "", "legendFormat": "proposed", "refId": "A" @@ -1113,7 +1113,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "blocks #0", + "title": "blocks #${node}", "tooltip": { "shared": true, "sort": 0, @@ -1213,7 +1213,7 @@ "tableColumn": "", "targets": [ { - "expr": "beacon_current_epoch{node=\"0\"}", + "expr": "beacon_current_epoch{node=\"${node}\"}", "interval": "", "legendFormat": "", "refId": "A" @@ -1222,7 +1222,7 @@ "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "current epoch #0", + "title": "current epoch #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1297,7 +1297,7 @@ "tableColumn": "", "targets": [ { - "expr": "beacon_current_justified_epoch{node=\"0\"}", + "expr": "beacon_current_justified_epoch{node=\"${node}\"}", "interval": "", "legendFormat": "", "refId": "A" @@ -1306,7 +1306,7 @@ "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "current justified epoch #0", + "title": "current justified epoch #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1382,7 +1382,7 @@ "tableColumn": "", "targets": [ { - "expr": "time() - process_start_time_seconds{node=\"0\"}", + "expr": "time() - process_start_time_seconds{node=\"${node}\"}", "interval": "", "legendFormat": "", "refId": "A" @@ -1391,7 +1391,7 @@ "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "runtime #0", + "title": "runtime #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1467,14 +1467,14 @@ "tableColumn": "", "targets": [ { - "expr": "libp2p_peers{node=\"0\"}", + "expr": "libp2p_peers{node=\"${node}\"}", "refId": "A" } ], "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "peers #0", + "title": "peers #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1549,7 +1549,7 @@ "tableColumn": "", "targets": [ { - "expr": "beacon_finalized_epoch{node=\"0\"}", + "expr": "beacon_finalized_epoch{node=\"${node}\"}", "interval": "", "legendFormat": "", "refId": "A" @@ -1558,7 +1558,7 @@ "thresholds": "", "timeFrom": null, "timeShift": null, - "title": "last finalized epoch #0", + "title": "last finalized epoch #${node}", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ @@ -1611,13 +1611,13 @@ "steppedLine": false, "targets": [ { - "expr": "rate(beacon_attestations_received_total{node=\"0\"}[4s]) * 3", + "expr": "rate(beacon_attestations_received_total{node=\"${node}\"}[4s]) * 3", "interval": "", "legendFormat": "received", "refId": "A" }, { - "expr": "rate(beacon_attestations_sent_total{node=\"0\"}[4s]) * 3", + "expr": "rate(beacon_attestations_sent_total{node=\"${node}\"}[4s]) * 3", "interval": "", "legendFormat": "sent", "refId": "B" @@ -1627,7 +1627,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "attestations #0", + "title": "attestations #${node}", "tooltip": { "shared": true, "sort": 0, @@ -1697,7 +1697,7 @@ "reverseYBuckets": false, "targets": [ { - "expr": "rate(beacon_attestation_received_seconds_from_slot_start_bucket{node=\"0\"}[4s]) * 3", + "expr": "rate(beacon_attestation_received_seconds_from_slot_start_bucket{node=\"${node}\"}[4s]) * 3", "format": "heatmap", "instant": false, "interval": "", @@ -1708,7 +1708,7 @@ ], "timeFrom": null, "timeShift": null, - "title": "received attestation delay (s) #0", + "title": "received attestation delay (s) #${node}", "tooltip": { "show": true, "showHistogram": false @@ -1738,7 +1738,35 @@ "style": "dark", "tags": [], "templating": { - "list": [] + "list": [ + { + "allValue": null, + "current": { + "tags": [], + "text": "0", + "value": "0" + }, + "datasource": "Prometheus", + "definition": "label_values(process_virtual_memory_bytes,node)", + "hide": 0, + "includeAll": false, + "index": -1, + "label": null, + "multi": false, + "name": "node", + "options": [], + "query": "label_values(process_virtual_memory_bytes,node)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] }, "time": { "from": "now-15m", @@ -1759,10 +1787,10 @@ ] }, "timezone": "", - "title": "beacon chain sim (node0)", - "uid": "pgeNfj2Wz2", + "title": "NBC local testnet/sim (all nodes)", + "uid": "pgeNfj2Wz2a", "variables": { "list": [] }, "version": 38 -} +} \ No newline at end of file diff --git a/media/monitoring.png b/media/monitoring.png index 6b2869084df033f54bd6157d2e1511747ae33f60..96b24f468d921ec86a04f1ed3305fec75e4719e7 100644 GIT binary patch literal 201482 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

TORK+B`%{#y7MjQaqlf^>XCr==*z+t# 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

^palJ}Vo)ScC;?C}E=I;yYc>c;l%U2y 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 AWdHyG diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index f35238a49..359143626 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -112,6 +112,9 @@ cli do (skipGoerliKey {. rmDir dataDir cd rootDir + + exec &"""./scripts/make_prometheus_config.sh --nodes 1 --base-metrics-port 8008 --config-file "{dataDir}/prometheus.yml"""" + exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{beaconNodeBinary}" beacon_chain/beacon_node.nim""" proc execIgnoringExitCode(s: string) = diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index d274532df..602925d9e 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -24,7 +24,7 @@ if [ ${PIPESTATUS[0]} != 4 ]; then fi OPTS="ht:n:d:" -LONGOPTS="help,testnet:,nodes:,data-dir:,disable-htop,log-level:,grafana,base-port:,base-metrics-port:" +LONGOPTS="help,testnet:,nodes:,data-dir:,disable-htop,log-level:,base-port:,base-metrics-port:" # default values TESTNET="1" @@ -32,7 +32,6 @@ NUM_NODES="10" DATA_DIR="local_testnet_data" USE_HTOP="1" LOG_LEVEL="DEBUG" -ENABLE_GRAFANA="0" BASE_PORT="9000" BASE_METRICS_PORT="8008" @@ -51,7 +50,6 @@ CI run: $(basename $0) --disable-htop -- --verify-finalization --stop-at-epoch=5 --base-metrics-port bootstrap node's metrics server port (default: ${BASE_METRICS_PORT}) --disable-htop don't use "htop" to see the beacon_node processes --log-level set the log level (default: ${LOG_LEVEL}) - --grafana generate Grafana dashboards (and Prometheus config file) EOF } @@ -89,10 +87,6 @@ while true; do LOG_LEVEL="$2" shift 2 ;; - --grafana) - ENABLE_GRAFANA="1" - shift - ;; --base-port) BASE_PORT="$2" shift 2 @@ -137,7 +131,7 @@ else fi NETWORK_NIM_FLAGS=$(scripts/load-testnet-nim-flags.sh ${NETWORK}) -$MAKE -j2 LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${NETWORK_NIM_FLAGS}" beacon_node process_dashboard +$MAKE LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${NETWORK_NIM_FLAGS}" beacon_node ./build/beacon_node makeDeposits \ --quickstart-deposits=${QUICKSTART_VALIDATORS} \ @@ -157,29 +151,10 @@ BOOTSTRAP_IP="127.0.0.1" --bootstrap-port=${BASE_PORT} \ --genesis-offset=30 # Delay in seconds -if [[ "$ENABLE_GRAFANA" == "1" ]]; then - # Prometheus config - cat > "${DATA_DIR}/prometheus.yml" <> "${DATA_DIR}/prometheus.yml" < /dev/null +if [ ${PIPESTATUS[0]} != 4 ]; then + echo '`getopt --test` failed in this environment.' + exit 1 +fi + +OPTS="h" +LONGOPTS="help,nodes:,base-metrics-port:,config-file:" + +# default values +NUM_NODES="10" +BASE_METRICS_PORT="8008" +CONFIG_FILE="prometheus.yml" + +print_help() { + cat < "${CONFIG_FILE}" <> "${CONFIG_FILE}" </dev/null || { echo $GANACHE is missing; USE_GANACHE="no"; } USE_PROMETHEUS="${LAUNCH_PROMETHEUS:-no}" type "$PROMETHEUS" &>/dev/null || { echo $PROMETHEUS is missing; USE_PROMETHEUS="no"; } -# Prometheus config (continued inside the loop) -mkdir -p "${METRICS_DIR}" -cat > "${METRICS_DIR}/prometheus.yml" <> "${METRICS_DIR}/prometheus.yml" < Date: Thu, 11 Jun 2020 00:13:25 +0200 Subject: [PATCH 02/70] Jenkins: archive testnet logs as artifacts --- Jenkinsfile | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index ac7c86877..026fb1e35 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -40,8 +40,8 @@ def runStages() { // EXECUTOR_NUMBER will be 0 or 1, since we have 2 executors per Jenkins node sh """#!/bin/bash set -e - timeout -k 20s 10m ./scripts/launch_local_testnet.sh --testnet 0 --nodes 4 --log-level INFO --disable-htop --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --stop-at-epoch=5 - timeout -k 20s 40m ./scripts/launch_local_testnet.sh --testnet 1 --nodes 4 --log-level INFO --disable-htop --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --stop-at-epoch=5 + timeout -k 20s 10m ./scripts/launch_local_testnet.sh --testnet 0 --nodes 4 --log-level INFO --disable-htop --data-dir local_testnet0_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --stop-at-epoch=5 + timeout -k 20s 40m ./scripts/launch_local_testnet.sh --testnet 1 --nodes 4 --log-level INFO --disable-htop --data-dir local_testnet1_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --stop-at-epoch=5 """ } } @@ -49,10 +49,19 @@ def runStages() { ) } } catch(e) { - echo "'${env.STAGE_NAME}' stage failed" // we need to rethrow the exception here throw e } finally { + // archive testnet logs + if ("${NODE_NAME}" ==~ /linux.*/) { + sh """#!/bin/bash + for D in local_testnet0_data local_testnet1_data; do + [[ -d "\$D" ]] && tar cjf "\${D}.tar.bz2" "\${D}"/*.txt + done + """ + archiveArtifacts("*.tar.bz2") + } + // clean the workspace cleanWs(disableDeferredWipeout: true, deleteDirs: true) } } From 8e648da3994ac1680b4797fece082b1382ac2f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Thu, 11 Jun 2020 04:11:30 +0200 Subject: [PATCH 03/70] connect_to_testnet.nims: don't die when GNU getopts is missing --- scripts/connect_to_testnet.nims | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index 359143626..be9d2e6f7 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -111,12 +111,6 @@ cli do (skipGoerliKey {. echo "Detected testnet restart. Deleting previous database..." rmDir dataDir - cd rootDir - - exec &"""./scripts/make_prometheus_config.sh --nodes 1 --base-metrics-port 8008 --config-file "{dataDir}/prometheus.yml"""" - - exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{beaconNodeBinary}" beacon_chain/beacon_node.nim""" - proc execIgnoringExitCode(s: string) = # reduces the error output when interrupting an external command with Ctrl+C try: @@ -124,6 +118,13 @@ cli do (skipGoerliKey {. except OsError: discard + cd rootDir + + # macOS may not have gnu-getopts installed and in the PATH + execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes 1 --base-metrics-port 8008 --config-file "{dataDir}/prometheus.yml"""" + + exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{beaconNodeBinary}" beacon_chain/beacon_node.nim""" + if not skipGoerliKey and depositContractOpt.len > 0 and not system.dirExists(validatorsDir): mode = Silent echo "\nPlease enter your Goerli Eth1 private key in hex form (e.g. 0x1a2...f3c) in order to become a validator (you'll need access to 32 GoETH)." From 016cc2217310a1c095a0fbd218b1ae57a2551ce3 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 11 Jun 2020 07:14:26 +0200 Subject: [PATCH 04/70] show peer info on connect (#1155) --- beacon_chain/eth2_network.nim | 16 ++++++++-------- beacon_chain/inspector.nim | 11 +---------- beacon_chain/sync_protocol.nim | 6 ++++-- beacon_chain/sync_protocol.nim.generated.nim | 6 ++++-- vendor/nim-libp2p | 2 +- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 48c89a304..fe7d59d0a 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -654,13 +654,13 @@ proc handleOutgoingPeer*(peer: Peer): Future[bool] {.async.} = let network = peer.network proc onPeerClosed(udata: pointer) {.gcsafe.} = - debug "Peer (outgoing) lost", peer = $peer.info + debug "Peer (outgoing) lost", peer libp2p_peers.set int64(len(network.peerPool)) let res = await network.peerPool.addOutgoingPeer(peer) if res: peer.updateScore(NewPeerScore) - debug "Peer (outgoing) has been added to PeerPool", peer = $peer.info + debug "Peer (outgoing) has been added to PeerPool", peer peer.getFuture().addCallback(onPeerClosed) result = true @@ -670,13 +670,13 @@ proc handleIncomingPeer*(peer: Peer): Future[bool] {.async.} = let network = peer.network proc onPeerClosed(udata: pointer) {.gcsafe.} = - debug "Peer (incoming) lost", peer = $peer.info + debug "Peer (incoming) lost", peer libp2p_peers.set int64(len(network.peerPool)) let res = await network.peerPool.addIncomingPeer(peer) if res: peer.updateScore(NewPeerScore) - debug "Peer (incoming) has been added to PeerPool", peer = $peer.info + debug "Peer (incoming) has been added to PeerPool", peer peer.getFuture().addCallback(onPeerClosed) result = true @@ -713,7 +713,7 @@ proc toPeerInfo(r: Option[enr.TypedRecord]): PeerInfo = return r.get.toPeerInfo proc dialPeer*(node: Eth2Node, peerInfo: PeerInfo) {.async.} = - logScope: peer = $peerInfo + logScope: peer = peerInfo.id debug "Connecting to discovered peer" await node.switch.connect(peerInfo) @@ -749,16 +749,16 @@ proc connectWorker(network: Eth2Node) {.async.} = # will be stored in PeerPool. if fut.finished(): if fut.failed() and not(fut.cancelled()): - debug "Unable to establish connection with peer", peer = $pi, + debug "Unable to establish connection with peer", peer = pi.id, errMsg = fut.readError().msg inc libp2p_failed_dials network.addSeen(pi, SeenTableTimeDeadPeer) continue - debug "Connection to remote peer timed out", peer = $pi + debug "Connection to remote peer timed out", peer = pi.id inc libp2p_timeout_dials network.addSeen(pi, SeenTableTimeTimeout) else: - trace "Peer is already connected or already seen", peer = $pi, + trace "Peer is already connected or already seen", peer = pi.id, peer_pool_has_peer = $r1, seen_table_has_peer = $r2, seen_table_size = len(network.seenTable) diff --git a/beacon_chain/inspector.nim b/beacon_chain/inspector.nim index ff623f004..6e676bfbf 100644 --- a/beacon_chain/inspector.nim +++ b/beacon_chain/inspector.nim @@ -161,15 +161,6 @@ type proc `==`*(a, b: ENRFieldPair): bool {.inline.} = result = (a.eth2 == b.eth2) -proc shortLog*(a: PeerInfo): string = - for ma in a.addrs: - if TCP.match(ma): - return $ma & "/" & $a.peerId - for ma in a.addrs: - if UDP.match(ma): - return $ma & "/" & $a.peerId - result = $a - proc hasTCP(a: PeerInfo): bool = for ma in a.addrs: if TCP.match(ma): @@ -188,7 +179,7 @@ proc toNodeId(a: PeerID): Option[NodeId] = chronicles.formatIt PeerInfo: it.shortLog chronicles.formatIt seq[PeerInfo]: var res = newSeq[string]() - for item in it.items(): res.add(item.shortLog()) + for item in it.items(): res.add($item.shortLog()) "[" & res.join(", ") & "]" func getTopics(forkDigest: ForkDigest, diff --git a/beacon_chain/sync_protocol.nim b/beacon_chain/sync_protocol.nim index cfe486a03..858c53bcc 100644 --- a/beacon_chain/sync_protocol.nim +++ b/beacon_chain/sync_protocol.nim @@ -89,6 +89,8 @@ p2pProtocol BeaconSync(version = 1, peerState = BeaconSyncPeerState): onPeerConnected do (peer: Peer) {.async.}: + debug "Peer connected", + peer, peerInfo = shortLog(peer.info), wasDialed = peer.wasDialed if peer.wasDialed: let ourStatus = peer.networkState.getCurrentStatus() @@ -100,7 +102,7 @@ p2pProtocol BeaconSync(version = 1, await peer.handleStatus(peer.networkState, ourStatus, theirStatus.get()) else: - warn "Status response not received in time", peer = peer + warn "Status response not received in time", peer proc status(peer: Peer, theirStatus: StatusMsg, @@ -169,7 +171,7 @@ p2pProtocol BeaconSync(version = 1, proc goodbye(peer: Peer, reason: DisconnectionReason) {.async, libp2pProtocol("goodbye", 1).} = - debug "Received Goodbye message", reason + debug "Received Goodbye message", reason, peer proc setStatusMsg(peer: Peer, statusMsg: StatusMsg) = debug "Peer status", peer, statusMsg diff --git a/beacon_chain/sync_protocol.nim.generated.nim b/beacon_chain/sync_protocol.nim.generated.nim index 2a21d61d1..876252ed1 100644 --- a/beacon_chain/sync_protocol.nim.generated.nim +++ b/beacon_chain/sync_protocol.nim.generated.nim @@ -249,7 +249,7 @@ proc goodbyeUserHandler(peer: Peer; reason: DisconnectionReason) {.async, cast[ref[BeaconSyncNetworkState:ObjectType]](getNetworkState(peer.network, BeaconSyncProtocol)) - debug "Received Goodbye message", reason + debug "Received Goodbye message", reason, peer template callUserHandler(MSG: type statusObj; peer: Peer; stream: Connection; noSnappy: bool; msg: StatusMsg): untyped = @@ -375,6 +375,8 @@ proc BeaconSyncPeerConnected(peer: Peer; stream: Connection) {.async, gcsafe.} = cast[ref[BeaconSyncNetworkState:ObjectType]](getNetworkState(peer.network, BeaconSyncProtocol)) + debug "Peer connected", peer, peerInfo = shortLog(peer.info), + wasDialed = peer.wasDialed if peer.wasDialed: let ourStatus = peer.networkState.getCurrentStatus() @@ -382,7 +384,7 @@ proc BeaconSyncPeerConnected(peer: Peer; stream: Connection) {.async, gcsafe.} = if theirStatus.isOk: await peer.handleStatus(peer.networkState, ourStatus, theirStatus.get()) else: - warn "Status response not received in time", peer = peer + warn "Status response not received in time", peer setEventHandlers(BeaconSyncProtocol, BeaconSyncPeerConnected, nil) registerProtocol(BeaconSyncProtocol) \ No newline at end of file diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 35ff99829..8d9e231a7 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 35ff99829e741c3fe0d50eb41800b8321cf6ea8a +Subproject commit 8d9e231a74c1afc76b6745e05020f8d4e33501e7 From f9c5769d42dfdde2dde1ba81b373c3b85e032a17 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 11 Jun 2020 10:12:52 +0200 Subject: [PATCH 05/70] bump libp2p --- vendor/nim-libp2p | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 8d9e231a7..92579435b 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 8d9e231a74c1afc76b6745e05020f8d4e33501e7 +Subproject commit 92579435b6b5637d573bd2e0b7338791f7a768d4 From b45885b35725a337557b53d94a84d61bcbcadfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Thu, 11 Jun 2020 15:59:19 +0200 Subject: [PATCH 06/70] README: document Jenkins artifacts and bring the ToC up to speed --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e023f6c65..06483e11e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ You can check where the beacon chain fits in the Ethereum ecosystem our Two-Poin - [Windows](#windows) - [For users](#for-users) - [Connecting to testnets](#connecting-to-testnets) + - [Getting metrics from a local testnet client](#getting-metrics-from-a-local-testnet-client) - [Interop (for other Eth2 clients)](#interop-for-other-eth2-clients) - [For researchers](#for-researchers) - [State transition simulation](#state-transition-simulation) @@ -42,6 +43,7 @@ You can check where the beacon chain fits in the Ethereum ecosystem our Two-Poin - [Linux, MacOS](#linux-macos) - [Raspberry Pi](#raspberry-pi) - [Makefile tips and tricks for developers](#makefile-tips-and-tricks-for-developers) + - [CI setup](#ci-setup) - [License](#license) ## Prerequisites for everyone @@ -375,6 +377,12 @@ make USE_LIBBACKTRACE=0 # expect the resulting binaries to be 2-3 times slower make publish-book ``` +### CI setup + +Local testnets run for 4 epochs each, to test finalization. That happens only on Jenkins Linux hosts, and their logs are available for download as artifacts, from the job's page. Don't expect these artifacts to be kept more than a day after the corresponding branch is deleted. + +![Jenkins artifacts](./media/jenkins_artifacts.png) + ## License Licensed and distributed under either of From 1c238a609dbda941e47ff3a0129c56fd8b49fd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Thu, 11 Jun 2020 16:00:56 +0200 Subject: [PATCH 07/70] forgot the image --- media/jenkins_artifacts.png | Bin 0 -> 31048 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 media/jenkins_artifacts.png diff --git a/media/jenkins_artifacts.png b/media/jenkins_artifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..58c2009cbe86133601003a7db5893ff3a05ef6c7 GIT binary patch literal 31048 zcmeFZcT|&U-!AHmql^vgh_q2D(u>mDC?bX`9YPl=0U^>ms3=I+NN-UHy+cATBGP+k z5}JU35CT#{fRJS0ab~{v{l2}=S?lb*&mX5`u@I7{+|RFG*Y&$G{GpcWnUhyf9yxO4 z4CKK*-6Kbi!HyjHCGPj*;2qVH_ot2=`Qr%W-W`3Pw6$rHr9ML@jYejeCO0D2rp=I3)!DmZo964j)Ej*7vpf&DDR>3XL>_1-eO#k3T0J=on*eK| zzuJ{5d%C;2f4=(=`z!seXU}?t1$YH2NpjE1??C=K?pN{pqO_vknT;du$GL^*M;-aS zbFIRyLfpUk=oQvD^v}2c_~ZZNVxL4PyT4$Jopx^Kv_Z2_PLrCm?HA7d*Nfd~5I@OO zfELzPk7;^xP!Sc%t!ilqGtWSry6LE_@?Tf;yNIdw@(t6$Iw6^Z3TTl;j!4Hn<^Oun zoQJ&q^qbNS&H)qq1Gi{yNRG%+yo{n|0b;k1q7BoWvedAQvAW0Il*_57#4>dIGKa6u zERve27`{K6Cj!@Km%;^GmtSVzFJ75+kw3NUb(KJ2jj?&k6{JQ}BZ~+!u8Ks>RJGlc z=TY@fR+`;zJ_s0~r8H##lO!emZAyRX>AcP>y4a<-_SlI-Dc<1sOXsFeRZT#%`nouy z99VeNePv9>Zl&>PLdc`kT>Vj96i1jQ>$@4LK;Oe~wE!dGT91Q+ers);{u$n#vU*KK zoSTB*NMh~qm>}eUk0LN!J?nG1gm4>2mSXGp`T*FO8zRQ?w%rD~9%N=o2i#x2cV~6YO_?(>y1AT(#R2Vyf$HkY2U){x7rHBr8pB*SKwP zUnva+q4X?#X?Qvozul;}vp>UU(R>H}?Tf&{V-hXOM{#l>D5q0-Ak;JELf5i?uEX40 zW@1|tj3ZVxw{K{iAwx27pQmnM_t1^a-)48~nA7dZPLdQg_+T}6S%UXq;+&>^Yd8~e zv47S_ewP$YE4F2@cWV34oMcg>m4m1PamVdxgxUCXy-z{ZJW+i<;4%_?zfJn+L1->B z2A>(|dVA_8bg@^qduz9@1>U;c3mq6unBdQ^FQ{e6VPNxGf4)Nh1-zF7FMXW^dHB<8 z7-v${h5RyExo^S0EwfXj5v9%~K3QiiY zK=ypd=~v_a1|PQf=NLz1iS%$M&X>5bYzDCPb2!UwZ&W8>RNiNjhGlvHmM-lszZHhH z>I~lhET7Y+8jP<-o#BdOPOy=2;)2xQx)I1&DNJ<^^k3DqVb2;~a#*evksy6M9Xr=l zd5FwBKReve+Po>d4$RYRts0a$Gp&lN%Mm^taiJ8}7Cyc+N?lQ`Rw?WF_R-Vi z3i-uuhGm`s@9gh>N-yjBT0H}PTX-~d0-|d^8OYuVy&jIZd_;6}D!(aDfP~lqsSzQ6 zTW@H&>7kK!m@fSDcaT{E*Md_MID0qfNe$9L|PQuEyDx-KwsX?#4F350XLidT_U{F1!3$v(r$3wy-d%DLlA|){ zkyd!?X}sUyNl_kh(ckt8UHB5d)#tGr4;ofq45LW1mZ(;tC`(^^%PykNiQN!^y(@Jd zpU-NcoPDV26kg{idnoLbTWkVK>T36WPr14%&oy+lgZU@R=(#C8r@!my8`puEsba;% z;VYysixRQ{uGy}bbS{ji#P>xqTGD$UBNB&un3 zaVLstAbavFrM&$$HafH6(d4MQ;V(LKn|Ax4pn24Oo);5>)R`p1;=GXrUo#W$*sjIZ z?JGGI`!NaZw7|~E5u{Z@M`L2{iNS=x;6;2r!* zYkl)0sWOz%O(g-iEA5xh<5h2d<$Sm9xi3b%u%KyeI8^?gq+B*BpVpyzvoz$y{vsD^Zn2+4j*TMpA=pEO=HbMl=BeY?#FuQHrT5=nx3S=;kf-4en!!c< z4|Y3eqwelf@@QAZl}JBMk(vtNv4@(Vn!P|sE{SnlPOcNTQ{{kh7US9la&IesRd@sf zi3wj(yFjQ-!}>rF#ks@wI(Ycu!-Q*bb8^moH>l+mZ)N@6wL7cHY%5+@PXwzFe1sfs zJmMHa?52j?GMKe+Nt3_92i|EfL76B1FoNjX%~?0M)<&|~BbDvp>q9L?wtj;ilhS)r zt@{EcPZZMCI%j!$M*CJhakd+Qd5Ve3GTEE5NsSs3#S}{hzJe3$0vqcqyR~U*^%$eD zip0XAxfHGyYwy&}907p0Y-GGM$hval)p5o_+mFJeDz0L{GE+3t7_kxnAo{Y)UhKP`ZH=cJ)_JK7bpFDy*3p@ zW8%VucJ$rNKgV<>Jmep`+hf)mnLy=MiryUKJ~W7Cj|b*5J;ToAQ7Jy`qb*v7Vwdl$ zDKs4!G09%)cT`w&8Lr!_4oO+^sRAGZfdUY3Qzpuvatol${7SdZgW#s+V9)YB)Ds26 zQOtB=Bk6-z8fD7HEO=5_7#@k^7{hB3xk0KMOzRfaOC(ZU#>Wf$6ZIr36q@ai$=QT^)+QSg8mHMoW zs{_Fowh>$_Pg;JNs3VsS3KrMqJYQMItW*-T)e*wenuGp2wERMDd->3n?~Y@+N?PO7 z-Da^JdZ%_-w{ho5`E_-2QOhZEP8h2V&d{jgX{X^ZQ>rTWCsJM6&sd<1H zn=NKM-khQO`A|2;ug76(>to{cwv5uuvGljSyd#^Gcr?8TMaCSC;W_Q-!1vT6B$H#B zwl@v8z?w#((bRo?DJ``p46uwd$n{Jwd1g>iW33>p>n|j)+pVcVy~8hjUp4T4yC5ua z;YAuG0U!+D9HZC(?iLJFfi^p|GbyT4OZbCo=M|XJH-**I&6{^`4%qc#7p_VnO zqX|cgbG4nz=-r2UeB%aS#;t47HS#El^>a=?f2=D*Q?0q*nm2{^qxRso{f2&@(qWp; z?!KQ5i|==ng^2#=B0R{A^y`I=q0Qq5p(8+Nyfva3l5Z{gxNobna0JW(-s`0 zxt;H6&|(cAZ24(JgC{@Dipe~!sR+d@TL`?EU}|}?{gjill}>k(@{T8>)nh4IePdT` znc0o*^$y-8x8aiwwz}#fHB${9d+H_mZS`|*!qai$VY+dw0Ib8YKI;A* zG9AWBI)z-t@hhC1Q1_k>i$e<=AIU0O?$L7@Uc(~S~< zC9VZ4BR72Rb2{&1#NiyHq3<~9(I-%frkVP0do^du$k`RtkLRrBobQ{Y$n{N#>4n`I z|DGk(Jk~LpaYej2oo^dJuzo=-Ln;-SsYSi@oxGyh{nO0LNi9ZQbpyte3_imq z?b3f}YBW11MlrfS>CahEMabyBTH;GdP?H{WN0aOg)Q4(>>Gic7;WhKY#9D-2S#wa7 zN3$0yNQ(L@de$EoH+L8A&a$P!ibzse0zk+gWiR`L0mQXB;jKbVt0 z9-5+t~)5X6dZynSC zp{u_Z)2s@$i8NO_2>CUpe(M_^P9dcGCU8RpF zpj*wyTeu0v&WS@eVbz}luR~-#VX2$OFoFb8wk~>-p++4ym3cAN_0)R$yPY0|M+}MR z4HZf*s9$fmN*7mVrKr`6s8R%%b2#~)f4^`%mkO_{pZQqJ_T*JsGFLuI*3xudaR|iE zY~GU_m9Z0ZR|6(PqCSNvhOq}oIM=}4zg~7c&Wu@)@?g2kGGhuM6Y`bvTgM(>j0PCP zFt<^~a?SrPovUv$l)p~u(4PoU70_x}@@5MLVc2WK4{wl}D?xd(8jq+gRKJgY&9cl* zNU3Sg15MIZZZ{IF>T_|&Kn z=)-nj;uXg?JV3bVxjL^8h_r`AaSGGp47gj_OixPZNPR~VJtXJ-!=@b8R#5hmB1TECkGFAORHC6+r_S|xjy!S z$o)v#kuobIUrTc4SYNP_z6tx~$c54L)yO)%K%LZgN`tt)>d=6)Eu*|3zyl#z(Bv?{ z@g@XkSx@fn5b~r%O9I+|K;)sRQONa29H*rFHlM7N&#EJW9z_3;(RfdceeJ7Zs!h{7 zwwPbw!eaal(YXA9E58r-KAVAh4??a~ z%|c;fdJww+hj%vzw=a~p$DAzI_EGJLp;-V@z0=o{3k(JlncnQO~`D1)0HZE3wxr5+iFi;b^W3cv^m3E zsfK%)$aj57Ou}ntBT>&t(hc&YD|Me}7^*E;P)4OJ;E{9#xzL??WM|N2rQ$pb{dIA@yPTAKl& zC<36+)ouEeO?pmj&K3gUX+g$Gb2j0_6=N);w|Qec+NRw6leuDlc#vVBaIT~2x`3j6 z7(-a4QGB$DwFoTM%vb~>`-WOfr}UZc$jzIzLe?=}T){T-%o-5pda0;F4!>tY(18MI@u$!F<5l1 zRp@nK&`8O9PgcTDN2_fG9d(AcyMyfE*YM{1=0>FRX9<8BJUy$`5nATqTUUaCxueNC z=}k>~x-`n?sR5Cwz!u=eFH)LJ^<-L|V{Kj6m31HtvWWaiuP#*?)YNXy9uu3>mESwz z(E>LBpcY3tud0joTv!WuVFL0NE{Z=FVf-cl)emot18AaEY%9RKz|(V0F*-nS#e2CAa1^^+Z046l_ zs$Mnv)Nz2H@|^7z4e#~-P{aK>%(gN1nWCb=n7Tg7?4Ud^cK}Emy^Uev8%Fe&N7U|| zUD^yOa5bvFva$Sv4+f}7oq2rrHzzqDg{IR3Hj8Z+W4;W^A7&HIGTB_PU$UES`S_sf zblcC&_S;?J0L*TXPW&2_E8CyczpM}Nu&k|HUaPn^u$Y(wK_sxJ5h_uA!cp%=g4+lQ zHQF!uVZ(5?w_JhVI|@QGG}YHx6xkW*ogeH3&zB zx+|!<2iL(}zY;n=_nNi0fS3ZEe!DM*;UY#e-GD=d@Z!s|f{U=i)nlN0d{0EPykZDz zc)n>bpNNiWvM>*Rs%5m!UC?2j$hA15E*i>e+#Wx+xT4#|XCN^vv1>^arI3E9z?sIs zY1XuNSWbbiiI$^T}q`l3yl!x8#29H*EJi$}zLGHY)j4?KfR(&nf_V#G7RG z%>3JKl0WIh9IKRX>E-=GWU_|}k`lAnC)p=EPLWDFkao1%sfqK=vvAzeirNUXj<^REOU2QtmE zTB3VyO-DyFreirQyCWz!GsSEIpsd2&pE7T?f2bbzc*@efA3z>5Fiv-F;B?=W@u=rl z2R-)kQ%WM6zY1W-qv(XyJHyWGy%s#Cu@ZSdiLPhmb-nO?rJMUWMmmHBhHyP~+j7zq zNSyk^(1N-uag1WoaJ+wbJJUPOi$~RQb0v6OP4#1ZIk2vt-Q1kmr0|?DXR2($D9mZJ zxmuHTYwF+v?G4Zzds9^d6VQc>&BZ0_G1P(IF(AJ?DiHwsMD8}07&5GyC~Bps3ROSUY1v3BV(iQBXaJx zEjt@%I^}Ps0T~1osX~y6OKWG=4f$_OYXQ;sJ^S92kC0=BCy3OA#ys=0v%<{PS%#DQfcYEXe9ZVUf@ ziFvd2UDj@v^@Va&I%rcs1wbN|UqlyoT;2V(56F+^(8OhuCA_aYyb85T;pNA$Tf3&5Dl0MvL2Ueh0`L`0~?zFzTQWHjYv~xwjtD zG>ZZ$uSh+C+KzYDww0T3ijRXpS^o5e`6uFGTyBExI?%EezAT>L`a( zaxwtFgO@DJ6DsbxWoyEadX|pB2L&9bS^5MV7@&X8ih6*InA}7Gqp@eI<^Y%|io|@} zs|3mnT_d9Nm~YrsYsxR2Ha&k27nzK}@J|iDg$Cdj%w(oF8tvWH7J$T0AIN;U0>7OA z6ORK4L1E?>lG%;8%)Uvxv76OZgz&=HR)Ou_cu^M2N9fTRW7G#W=9^@{4rf9-9n2O?mi}(gxm&aB}DS>%=R{c8T%t z;wQ)oX)gMYeHtUYik6yX_K(KSN?p~uQ!le0a!#oAF`o!;k6-BUOM#y^hMt7d?|Gy> zX2N#yvF&aUQ|^KcKJ+3IbgASfGaFb%T;e-hg;v{#Y!Zy84E}v`3Yj+GW_}_SXu@+9sJ`^q z**AaRUv&?B!a3CF+Tji@!dyhR-uthE)BpKd@Bh}p@&C%j4pmXM5kH?Bd|RUP>%U$X z=+Nuu>sdIRTK#-|y!ZDU`^XWi>wlZ;$Pv+hX4D{H{xSNIBO3o$>&Ouvx^Vf|>;LLy zo3$N_|Je)h*ADf4NeU zDA89bFE+oj){|an3mJ%Bm46oQ;NCtN(ae(F5b@ih0oP5cU7VzS1A78YL@i0|n^^Gj z{L>t%FE93TKQ%>P{;ZV!YIFF^%u>1FicIl?prIYgiKtimuEPO})qkva>5!#7xk^FF z01uepYL2a%aFmcflTN~rx6;Hc6Flu{Hz9Q%v@dymHkzx2Ze7`NpPX-`^dHv15Z^El z8OOnD>|j_yu#5)?+!IXQY+5O~Z@(R{VWINrHTA5=t_zwAri?>IbsVNgwkFS5mHS3= zLribYo|yDXu}=ot#`J<`b<;w|=3#eSqCUU#zIG2q_VIh^Jj(p7u}Cd>P$cq|Y&DnvE4ty>(f)KR^46M`ruOR$n zeK!t$wAn66FX%1;P2OS?dhM^9ik-y*_o2lXxe1*IxngSvmGGNOSbM0_*OR5sTSYWe zvePFxoq$7>m+GZmb22Mw*#r|#N9#im6{x&AuI83aR-(b(Zb?qZSAu)0r<>pww+69)|wbS&u2hoCXvoztkziwqRg! z*XW~5|DFK1kVd>^^3D2IS=2L(Tv45QYQ98%sUz_G1iRs)5l`rCf&kOJ{*a$KSo@YK z_CT)VMS8|0xNyNnP410-^-&k6A2(ss%R3R@n9-6%q9up6pBBEKI<(=-23$d@wZsRx zH;peh4FqU|2r$e(9J4X0V>g%evCY^t7Apv`yoJ~_E-U5iL1YZ)>{O$r=U8GpLiS04kon38SyC`Z@N1R^jTslGPcn=k za$e5954yB?FesAv}e0KX|X@PxO(_XHPEN>+-devro$Fbm%lanJ;srnOyi@O^#p@a`h8 zKBIq@4z_ODyTl<8Zgb`4qJxxv*P zt49k{n5pl*OheSJx{bi#y3VTMYFFJa!8R$>5O+4<)+-7ZGEt4=9tNb8tr2T@ z9Hw4nvhtuu)!TJd7bzY=`tH+mQV5ckKeeS7QM;2&4|P+_3Swvbk`Sd8tYajWS#bn6 zGCp!xrZycT+*heFS}-h9*a6cE>`-uiKz?VK9%K?;E$OK9DeBom2}t_JewXQdIi*Zb zFXlX8q=MlYRI3s5)dBNDwV~Qf^Y*Y(5DNF7n?Fo0nh))XK>H-1XVdpXaLz0SNeDO0dMJbDEGf{EivT!Zm8+^fSz&l!m#BI z&Eb#)eBHRmsB{%W0{YZKF4(+bn7eUk6_sjIp-oJ;wHSB>B5c&9aD1YEsNKf&mi&Ny zFm0@3HcFiO{ZVbXmXP`AooF&)#ps)dOq9~6CmOU!zx3%8PN}{PGE=`Ry^il_yhiLk zGAXL)dF0tBYtX1wzbe;*Uft=r@9LK&JBr`iUdZ8FZ$4XEg6lDYj2hTMkngwrvX1dW`GipysZ zaZd%*G{;W1ovG3F`AT#G7R z_M1pNm+i%kO=wzwrG~*_-_23axbAF48FNMm_PM}Yi?HUcKeP}#z%y}v-eFuY``zX+ zLG|~pt(gAxUx~&oZVGEKJuE?pAb0Ua;kyWiK#L3(E5{!j{@cJQh zxqD|_!rVaJD0)la!WrQ5^)Xw%l`*^0Z6~zik3IffOu8DU1^OZw*b(uZ+ZmPJ97S-K0jPy77P0$)s_W?`#l$i@G+wMAW69_{ z6=(^AZ=YTwF62fczuRf1ILfJwI0z+jfq3D`b1jvBJxp2pHTx1l-*eJD`w|5OLaBK} z>AFzb0*KC!4#`isouAA+YUXy{A_yT+HK}-v;EGg4pXqrI2tH%?V`2Xk81+nN5I=yK z-U5H(^A|#};D#R`V!nwOj1M%ADEe@GxcC6y-}GUu1d6nL=fzwGiZs#pm=L#6@0+Ir zj-B!LAlhIaaPPAvh~2PLoHr%m;S8qmFx`#y!QCPz)$E^FH(MR9KrN#sw%Kb(mN!$X zL6aj&*LnOQg|%t;lb*5SHK=H`#!#}HQa1=naI*!r$qeFKAczR5C0U=YzP_&l!GiV} zs^P6VrUY84e5m)Sor81kEAGHztV)loHm8uv{4n^q-S-jV2Dq{=E7p!Rmh4XxJG(3m zjB(ZiR9J<;npcW6d?B;;O->q>IgOnQGgU{=2wDYFzMltX6x|DJZMcN?3iJG; z#I_`PnY&o(xr_H%HHe&&ONzMK?iJE!Gvsa#fOT=?uW@3%jXCo1x0f7!vF^|`X+5a| zz0pCF5`c^E`q6U}4r0nSVtovZR9$2G0E)ctR4W|EF(}`oPybDTNwC1Ne!;Wu)O?2i zcB`6gVw)qtOG_phgW*nvXu*}*XQiC>>raeK^fETh)aI(*s6m~)BO_Kt_=~^ua!*BI zOCpgD8H*=Clu360DEn;a)-^C@uk(`w7`Sb3aIRQ-I*ytkyB3Y$0w>P96x|Bi<4G#C zuC{B+s*BNu=z`rx0Fj} z97Qve8=(D`V6)rSOVB*^A11Gf#r`=wk+K=HDz~2hVY>-*B>p^&-I@a5_f{v3)u2h- z&Eh(s>Ing~39w#4*?I%LeOHe1gZJqW1B)`LAbN!c6ovroSpZsO>98<;{6D&Yh7prz znF_%kA_aJBSS8B*EG@1-aXoR`+5oH?G#%!iM+zldrWuC~z`}(Ci~8Vga^JE0`Rv_J z$3@@n@xv0iDfj!B3g!FDiT1ODAUOff1YhwW^XqQ~|BxX>uLSF8`o9Gbadj0o9=)nB zq?N|IBwU02oC9?xg2Dj(>uE49uMty9V%}9cIr=e?Vs<<4l9pKqXsX5^tIb zej0NW#C4WEU}QJSukRlNO`m;tPyIZSctBJRzT7@=Wk?W;h z0@F<~fC*Kgc-?-BA%OBFIw`L4MlcZDX((89N zrLxd63xqNae>&D{6?`4sy|GPpTXrR7 znvCT%0NWB=W;rjDC&BW%@@jn>pX4; zv%pc$UH=Dc8pN|2VvGMg2&1vG+~9vGbpYdq)M{e{y&IGc48*4LKn^xPh2_9Sjx^tN zsd>`Cu;MN=3UDm&^f=(3VG4cWZcQTEK^AhAG=+IW;fBfJI{MMjq0x>ww8l3MsoT#5|*HeKjNym%G8(nD_}AQNwbX1!1j&=&#)0HCTg zffE${9#|-@7c_{ySy42IdNc zx`%!r?dJk2m(u72#XrERND$PWl-)-l=|LjNecJ~}&eq%5VAz7qO{e~kb;u=so*vxv zd^KwN%`Pu!UI2wbAg44$!QDKI`BgwN4bi!#63GcL^f;Xg`!B<3HJ9Z97?VP)3(_t?CP!N;Vsm1 z9MB0M7Tr_88u6w%I}12auckuTGEGu7=OS#^a)j= zpe(AK&=y#odV;K0H3q$LEIrVsbAi5=z1B|fs8GeP_{yGlNGYn^Qxl`VWC4alei9vn zfTxZfhgNfk5Ad%}(O^2ChOY_T5}mmuWyZ0EdjsGkXciIZ-xN2dT2IS&+?APSpvygH zV3i6pv4Yxu{rH-rv&ufKBG|1Srx&HR5BLrht$xA7ldSsv2b{%}S+SwQVWs23mZgyA zV@_WsS!&WlD=q12hFI^@Doi!P>#OVSryMOsre&ooz-L#K=wm+jl}cpk`NnVFHGp=D zmRP+*mCP^#77cxyzz6l-jYXgk?PfA$KW+#h@3w%N0I7j1AJBy9#O2dYdw@|u5G4lL zHv#Yf1X8}_DZv#-S7AKAuqyNGmKt*1T%B5bA3B{(fwwIQo$w_e@WU8V!E^nr)bvh? zZ5M#XiQf3?=gSKG0xC)Z>YCBu4B)UHyUp?(lZkRhh`V(pA_z^4Xi(jz-`8SPU*Us? zLAHqNpvPFlPa9+H659&)t>d9}jw7SPB^$S9Z-vtPbV|`BdS{z9ECv04f%wRGYg;+t z8X!fZUU5DeG@lkVnS(l8JYfW26!SvaVnCDmq450dLMXoRBLAs*Bo>HKAe|&U5(+1W zvKwVASq6|mGb2}1ek+X$vzfTE?0ze-;X@!j0c;QLeUeeYc7&A!*$E&9AsOXazNr*9kNt&JA(=i zw%c}DAX|%MDHQ-86yR@;9-yT#KuR^Jo;D~2Chb3LxoHj%C+3=S)vg(JemQfrgtMP! zZYoYK0Pq9^$0bNrrEqsU{$nt|qpTFpgtAOyli&T}~gx^c`^dsTO0=zX=` zSfesDVTW~rHlEW|Qcs)!ki;l9OI^wd+h8$z`3aalsKy2UDe$lY8ad!97oQH-Guf<|(8e07#72D1Y^#{$C;R zJ%ICx>%4|1E|m`D7s{Os_+*XH>cD0>U_eTTA@WAU7DamF<9qBPNwzf}G~RS!Gg!z~DFM>iEfXdJwmJSL9NDMrnUVChcrPo`Q z-9c~1f5yN8hAg-?a7+&y51X42W0QLyjeANpMkq^%*J8oG z#|^3Fuz;SB2azO%*S~86-~xr19e(WVsHMk)-#OC;WbttL&7ZCgO z+DZgly_*^h1Q3?L`vp!{dmCR-GXID0pWOLL_-}j_PYnz>(gK>jAi$YGFd4xa6arU_ z7xp{2&i6iBP6zVPAV}Q*P7Bck^$^HFx7CI~buj>uN|*QOPP?K7VFm7TmH`+>7{X!1 z&J;kn`sAyV?t9|F%A=QTfeIGz3ujk@!qJDibU)Q2wQDYA17&aic*FQrX}~C0)}3TQ zJ?@oqQO5z1n$<{i?#KmR zPZ#%W?X}#If^#<6Y$xOa=@xK*_AbkU{`IA63!tK6c$a%Xaq9F(W2_wE^bC89135@Tz3YGvzFiC_Y7 z6N?Rw|M3yA>*X9upO`VY-JsWVF^l)MlH|Q3FXjjtXqV^x_0NxI*nCKMK3S=#Upl9;Q`}a7U;)l*P~6y%;2ZR#&tHI|)v@)eLi4yg zbKZ@Kv7<;OfS=48**P{g@AAPK+2M9bWnkgopG+mtwO0YIDW$Il`oTIYZDQSgO4f%D zJ|kWxgIzfE78_!;hD0`1aLx`p7}L%h;acJ=mBB!Ue&_tpfZdAGvVG6wWsqGS#`l*$&qIx`l(+Q%AZf# zlz~c@w{SG8Z=5NtmX1r-DvZHD5=ESh?%VWTSFil)Ls~fJS&J6|^8RyMP)`D=Bqj1? zc?E=+jvc*|)qKFZJ#T@E6r_%$E3M>S@91SGT%`PZ$ak-$ey@0mMHU)Tx26y5-1Mr> z%cEvuBZ72+@9E^mb++%%p3_m@R5jd#NZL;T;m9Hl?f`}bbd_g8p{0K<7K6v;wu=HS zU=(ZMJSC?z=+h$Dx{sw4I5cL1fmM58E(5E)28erq8M+JxV2w}|@+`Wtf!+xLKeuAD*IwP6ADq3sLrQ6K=Wftm#fcN`cmEeLdn(EEca z+`h9~_6uEKOp^pxLmUJAI>vN)&TUWm^>7FR~5(Mp?uE;epsg%^$*S`V9U|ZuG5YLOuLTG z#z1=P?B90>+x?GHhCmfjH!PiEJDtrQ`zrxEXNfR@!zDWWM)(7;!h{Vno{s0>@pjQ{k`w7hoXD zr+G)Z?joQqWl!)=Pb_Z&DdkzYm^Ijya3%bIq~{n2KtO1ui5?^PR4U+=J&9i5&4z?Q zt^EL+Wg&1OQuuyS(~~2jP{|B(B^Y>h0twuJuUR4xYzRCeS`PL@BuDk(nXyh z+e|P^I)mpx|DZ=S5GLu+8SwqAvLin_$(x^)=&cMuJUbvU(dX@1|I?cw!0F)if0wrN zUFqKM*Iu5z`}eoc=!e+z|NH{pejWY+#r?03U!J|73{n606L>-q3IB06I8fs>x6r>` z?`6^zHR!)x?{_h=IP||?@6n^1uoM4w!spNbUt9p^TjTn*0=T1|vyH%C{4m*F-6U*T{NVlIwdh&;W#)dn@h@*azZ-TtNEt%@{pE$If8F}GlmGvP|904anBvWl zVoRDS z-wkC^!)>yv;Z*;~3Vb1xrlqnGGnx^?J2Eo_lb@Lx%KtuHUQkpb!GT9B>hG)@(?9N; zR8h`3*C=O5jMn5B_LfbLP{_yw$MFk;zs&Ie_88G`Zz-lfJ`%g?_fN%Onf_8=@A=1A zW)|kAgpVP8NtMFp@mvP_V?F^q8d`nyd!)-Gw*9A-*%)B&gZsLwN3b!5 zacyuD;tx}C?mu4k+gf0U-gPPGY!1(4MQAH7i%~w+XCJ^ehOTiJlR54FF-GkDb)`IA zaAk?xlIjRE;Y2?_KkhR^FoyO;H$KJvv|SF_{4jG#MXB@Ry$`W5KjW9B5V8BIO#oa> zCmY!;2y=i;j*sVtsLp%L&0jJYAK~-!E?Qk>Rmgekxl)kn-Er0Oj;k~qj?+nJEL;9R zDx2@;;lVFOz&2{K@2oFk*7wMQ#!o3#;P`@uO#ZKBFVD_?K(RM}Kn4CLZpclTo)@2P z(#r(L*BKbgqfo_pMLJ;Fqhk`Ci*74l-%8s)UOb(AAKI?ME*CGD*pvMNUyWJ*dcq{Q zN%PZ3gd|v#*Sbt*x_`{gTLRwt<8S{?w$3kH%d4J*Dfe$Q_{L0gYTgA}EG>BAq-kwl zna^1j>$yr{fk=E}nz<5wZlUBw?lHc^guCayU6(p{?%2iWi`NBz$3R=|rm7F4HL+;e zdKaFo`~YXh3sb+vo)POlV=s~1U@yKIy<0mR?zN#%)Y>u7N!V25=d@r53vk3s7xd8f zuaT}ON&Tv)jFJ$!UVNX`!BSK3P1NH+G$I( z=@^le*`)*2$c!9D;>m(6u!YSUMoy(Hu&i$`jlAJV*5`cJu_?@N$9}bw!}6Rz!7fLP z-{p`_h(s^Uy@rz5hj_!ZSALY=sx9~5u=b>El{v%+jJxC)FXS<@DQ4fht|N}!a{5`0 zt(-~qbr^8etJb#&rBinBdMW8|J^t+f{-|E4cyUtJwD%7mVeCxpc=ohElM|P83JJG> zL=Kk^3|#6HcSn?;zOx2!aWA`uExIi!aKRi6WCsaWY3fGI{Y8jkOJ9LOL z)3O0;QT=J;lJ6KLZ>5p-#<0a*ANBsX2K?B|F=X>*#pr7KWaUT_cdKHv7P2vkEVD6^ zyj?qa@#1})NK>8hmVN6D-llKM_pKscqnA`rm?M|3&`+=uhH$;}B_$mbm2-ITZ|?(w zJou(J@`2MYf}-@}fTH~2S~A_}##TRnJtBHnS)3jWWgq|g-RfoNV~cyxQ&!La+E@DV z_cL7$uhEx3{z{)A033JjD#G_1Oz7D$`g{z$ZrtXgKj)ECr~mr>{{K@Ku)}n~a{72k ziAJw`%C*h{yHv-%5&2uu|2aY~-a3iC2TeK# zf~vRdqx_8%+1O#^@_lXN=bIx@{U$C!KjXs6SuZ>*{IRb2G*dV_$pA!H9_iQZ-`-V7 z$as7+e2R1Ln_L_5TUK_vp&}`@)HSqUe7x>!V zjXDcJ^3mUZa7VZ(h{|Lpq*e|v4=7mlr7bQcqR?EqO4p3udz@x$!>+Q&2%JaGk%fQW z;8|`7P8!NTIJR}*M&`z5Lcufm;7$o9Bc5n722%7M2#|>Qc(3ZqPrO&PX78<;eVHHW zQ=J2U_PzUgXLTm1(bwTm`XSQ;VQ%L<-0lgbZFd&GH$>MH)#^<});#hjFBCfVckv9+ z23d!@11Zp^j^f10se^@IVh;i~r>azges?rUHD%R4?YQESIL7wFVIjOFS)uml;=bnR z^TZ^aLXS3CAVLFzT19ROu*H(ohJ%7J|5RB4Y|u`$b{8*mew8_+gG#*|smKN7O27+G|=gds0m61ZSCQu&pBwg*3G4Y}rix)2vT-d4B z^BL9tCBLB^n^p7Cw{ItmxSO(XDb@0mfTf#fm-Jt3u>GtTR{f$8Me)Bn7dxK75PwIX ze>>Joca7=WU0(0oq@*6?o8qo(;Q!zVGqQdtR88npwWb86>Wv4TTE0&neMWWj`BQiC zft+vuw)r(7xVJiDF72J|GHvMGXhSh0HxFe(?y_dj*tbD=h%0elTt_mYSKq<%W`3xh zYL}_hPs0=}kjS>xlkN+-$e;q!wU&8)6@_GGy^o7*j&d?_;=%r9iBn2pXpic`@B90cY;{8y z61XJNNw8LX3-%htOr^lpA>Q%G(%zq2>-9yhMz40q3)G6q%bRT|zi5k(M2{ZFE;#NzmI>7w!4qQJoCGUV>l`{k-~P>ck7i!{EO9Z&T{5D5<&8=uN*(*--Bk!sQ1mT zL|is!VZAc&`J1Y8G8x-MYJ1bvpp;X{)atgU7F_C4>}4u>2aj{I51A#U ztD%Z-+uwd`*y9~UWbD{p(ng`ifeEb-5%`gB%|s%{Yg; zZ~h# zT^)m(e-DLCQs2Iuky#&oLC_MKnH#}{aCp{<|Ki2}whEh;VYOsYVZR=Isc&P87d>~Y z^m3K-e{1i{!=Y~bzFV&9zEZkcP}VC;wnSO8SF(rf4H;Kb7(u{GQ+MXZg-K zm8H!EapHR?1&AAC{0H)u8}cHTXX3In@tV)EJV7w>to72KQ47`f?fZF=9Q_jP3#-uw zzN(4VZGVSV{P0H*JS7eZU>*ui#!wUw#fNp1IT#4*1+4% zqZQxEQ^?qI-mbw@VEfav_?CDKTxks-q)fKS_?Mn2Ay(wj~uR#HC20Fi_lchFCw|ZPGXYQeNr;>7YgtGyf zU-nncuvQoBLLQB2J{Hq#=n;c=mb$BuXk1vtI+@5_5e+bGMcL~qr1!M%Lhr%w{&|t( zCNsq%s`OxhNs_0Q67rTi@0ReU37|ZRE`@3;{A`o868a`{?tp9a2wPG}ukzOHGf|5q zY-@JraAg??p?udsHLZQ)B`X)`B##wq+NQNF*NVu4LzN+O||%DCBE)L}Vnw|F&EVY-0M8 zw%YkV6Q2ehm?eb2yUcS58*{IeGESte)ZcPL8!mzT(XZ8T+*~5X8su1zO&x{?N_5Jr z@Q5f}Zvb50$K=IPSGxE<=Ex)O6n#S?PY?=2)u1~;6w6Wbb{D$8F`Q%4m$s!GDxBCo zMNF@}*_u=+g2dzZbLmpj&@xyLsSnj-|Q2T;OCS+wtwc$gMF}yTX7=+ zv<%5Qqdq&CyEwV-Q%MHGGPe#w80)ZWZ_+ci&Y<)q3)j{$qxbOffek$oN#=oOu6Wv3 zo1tRj$~4xo^|z-~c)`&AuPqj{LIx6wu6~%QRyHbF6m6X;TkIe86y~87zT^{l!_CF_ zJx|(G(KLO;8{fP$TE|Pi3W`al7#H1^C|tQx`)#Wrrt*}MqnFV4 z;X{79vM0C3Kwn?3&m4KCCr8ifGR4rFD~;dvXw=`8JcjeDM?=*7iXJo~%RA8;MQckm zn^hWUUet8%tTb*9TD-qfj6(?MsT5&1W zwLV`X9T|8Hn_aNV(Hu)v?>*D)NDqO!vx^&-JWt&N;H027XMf8%o0}nv6Hl#;*R59E zigS@`h&Ab68yy#`au=7F;5=Hm&Lo+TC{8 zx2NROqtHB0P36<9)(cIWkJCz+tS}|?@zbj8g!)CRI*j(|RI(6^s(R0Re1l$h?EPg} zj<@Y>uoWew9OjU&0rwO*@tdDQ!Ta1I_lmCIrmWiZRJCB zSlNwmCHD_mJP!UB9z1B)sve~KVjT!DI-Tx@ME1Xn;{#B3a%t_wO2kzg-NZ`sEUcAG zc9q~?uu7i9o_^>yVy!n4Y}NAI73`38q+GFDa6f$Q!_p_4!HUB^Qi&^5Ii(>D!~#S^ zxOdK`n|A1V^(=JuE08LKdZ!6Wxh-qxYad4)N9VPDexZDN`lb*K<+XXCn93625MQt(=JEfuHF8cZ|(ItGPXBSL=W1} zyZ3QErcN!-^r{B0<5X2uaW|UxS5{S-(6Bis{|d`LCF%!`(S8HSl1oD?H2sn?sH5_)0(OmoH=fm)3;<@!xA@8-Gw$X~E( z>RGfZ)n)eJ*f3WnSfiMRn;|j{X`9D7{l*(Go_?i^Z?k+Y!J&=K@ORAd;*BM{iLgJHL8*7G^+M%z%fk_vMsH{0e;* zx-^Nw@YrPdHCQO5ZB@LbR3d!F-$vE7#0n#2yq&MqC%*ydysx`E%jn6OWa_E*u{-Z? zRTR|L4t>4SXxVm|;?6x}8|uL1MW7U>OEIyTGVF4>A&lPhPJSn&$IO29 z`c;i;#jUlMb_pV|njy>?KTB5wLerwF9(b;0oRHeVlB#=qj=)A3WU%AJXlJ8wx-7|O z3vaZI8xUTff`7DDL6et;GHTXKzoJX8`K=41ia#1owkDFN`MCundsdQ1E@1}9ysotx z5S7`ld+zdzucr=3Pm>v$FEgHOzyiB-%3LM`4ZdaVz&eK#cct`;*=8dX(l*6?Jn*2z z)u&f;vChRW8+WkQXqgE1##+E)rDg8H@Qa`j*Lc(77t{Y<@HqNqB{<)rUv@)^%WY1; zMNAl*3S=oqB@v{s7-6KRgxtOpLrVR5AZR2{D_AuPm#brr^6_l&b!b!1G`uZ5;UbE; zr2GKIdaA5UOSF5FCPGI`?}XOe680q_@3FB#k=Ek$L`;YnT7e)WaFxo4d#{BimFzyc z+H@TZ0nop;P76trHN~`GO?1YyrHk+1?itEJfl>v=Z>^G&a$7WeGH@8S8Kvhb$lHzY{0*=*L^STu-W#wFv~5}v{yD9Rce*~krs$UsJ@x#*=*$({h9vD@7N(w-HoJ0csIb{DhVJeaG72$03j?V zSUdo*B0qZ&x*5fv6pG^p0vl0xpBQy{`psfRcGvW26x(D1%It0H&!1ei`okme^yv;l zSsL8Vzy4HLaxmjH^RNuU8v)qoh~^ZrdG7k?zMBXymyxQdGO({h{P`2HtyjJ5173&z z4T@CjvnnbQwbbH(r772ux&YJ?QJML9tZ8^DBO}Aj-QCj3DF*;SMhU^8L|rg}mzVe6 zR7Yyzl$50u>4mu@l7cfpkY8REJ(5WEctX6@9w%m$GEG<=Df+tS7R1(>cQY$y1G`Pc zNrtIrh!kyUP#$}7RE)EWJ7q4lHn$#ibEc)KYwxzR9$S(2zK6Ke(J(&{hlz*?YAS5k^`KvqJ(;9S(O-ShaU|(8q zxSE^MIY*Nc#zLa*Yd4Ph3cDQGU=L`H_Rsoi^lGa7_Sm+lZ|2z>_fiMe!Tp_B98I*& zmDxxtb!&0Y`Ab$#z1uzugyuQuI|86EZDx!tqu2B1)xWRy{=Xuv$M^&|W&;gDk*L2PiPGtrn7IdA^jELG;)^yPYP5fGMDJ*8ZcU94kerJkU6YT)9V8TwRZXgy zEm8CS*kgw3^WKpGW1IzVLm)jaKdN29 z1ATgPvo8xEv9#*`Y%7)qYC6u8^%T0vr(J-zIadsRx})+%aFw-*qGk9&cz}iuGzn!d zWlnnG4LcX<)uU~gkHuoeXca?rO1ZPqh?8cfskHS=GdnvwkMve= znZJl@o)_=u$Nju|;`=DA*}jE;O57>J^htP2Y zh%SctsPMyT?iFy$z%0)}IWvIZaPYu^Pr9b23EZb`vskMOPt>a#_4zm>D}CuO!WNr_ zFY=-L)@pwJxI4V*m+*@xHT*wogDl&lwooTewRn)1m7)SW?sBwM6zB&LP%?dpb2Tst zy;!V0^ZBWv5hwg2`$)Cy@wnVR`C~RJ=q=DuuS3bHzAtv^hHogw?WEqxUoH{eMn!39X*dt6DnP~g5+q+gzoP8y3!0g-tF{Wf z{rC59+*|uHS=VA+;(+DgIpZ=|?kpyPlmrtr5OI*NO;hVUEj%cZhHnJ3LSV3N{K)#A*qC&hJuQ zlzj=`cLUbLAWPFd2vsHUxdaqfG=|I6kVp##HM|di99Su}U`}H!i6wzydTE35>jpMJ z%DnH)=xzB1LlQk|rk}8hTIvpim~?ICA;}}A(#a|8&HmDqN_-{ba~Lo{CclUqc0M{7 zqvx^5yM^6o@}x$iY?$3n&l|oAVCz_q7P?T9eQRMbm{;glkDie0(iAbt z(kE9yK;YXFEQ}D!hm1K(?W){+3ayG1l$5M}(_f~faAV;OBcG4Uu?nMdCU+q#OP=JO z%pCp9(YC*3LNjlyPX2ev7Vs24qZgHRRBV&h58hpL{E_a$O+f%zrM2N6-g6)0qG&=W z`E2>rBonHDl}ZX;cqvAx1$To~Qf}idhHS59*A;0rmEi!s!&%Jr zGgOrQ20o8OveVm(n+u$fg?Qc$xz)$fIFNZLukJeaR0#zyCn_N+q6X_z>7F>CC4@Jn z#^@Ku;aDs5UijAfK=jZDKy`tn#;4@o%^_Qt_2$i+q@kDF+!sGlIYbYz*=kge6-HpR z?TTO*ks9B_(#<~&p;W~`4I%1G5xcDgE#AnxdPd~}=L>+NGZ zfusc~fVZo46_1!$JL8U%Fy#luMw+7G?*GKhsZMFtGFIlorq{ zpoFOfQYSw-wK@l`Cgvv^DUN_K7!MK z8Ku|YekFdGV3K#l6%2BD+~i3-RglOx%1*+MGiRGh$pV7iz{Nvt&EH`}cex4Eo8jjg zQsBgpTT,QG20GIm}$VB8D!;Cdk~ z2VZ#L%6t!?HqQRWk3604&?$|IS|Ey8K?Rf3) z$pbgKOfi!XO($CSYs%FcUM9nevodCRLX!7aGCBWrR?`!e5|bPjrfudX!^~%le?w?4 z^k!-bn8D12B?&dva5Y(Hv?)eD@vcG=DLefFA-E$w9r)0W#6;h&OhZnXQj; z>qiqb>!)qT4SHE%chN^4ZUAheDtA|FxN@HcP*n2g)suyh7@(kemcxXRK!bZOU?MWbKYgbtU`6p8KO(<&6amT$Bwn|Ps0#sl;%u`(^OI4z=rGjXfrwE3DvZm`@>y^Y*?yjs3)EqyFvki5tqa5|kTyqMxJV)G1${7_(ix zYnj$^XUlm-zJIOlL_#vU9nzxoFm#U9tL=!Q^CS0Cp~EP4yw+FYx0X0oxaKo4F=0C9 zB4=Y&BTm2Ppk?#B8f|Uom`UaBZ;NNwEw=1NqlCz@DDDvvaOsX6OeN}j!`x=~lYjL( zWqokJOlg$;vBG)cI1ov&wJUh zW?{ z9-4v4lGFSB8Sf8?{6M+=)>Z-Y$nQQ=6YAqgN1P_TN^k$W)!yky6fZ?nZA zLea4MS_ih*)G27r!om;J(K10J;4<%{+m}7`?c&z-1pmTELCfO!i%#urj!e*=^LqH( z;*^%=anX-R0!xslyIwGl@!h8Iw}KoNT+D&MY=z`AH7qzHe)CJ226jReTEsuzJP_8? z8RBJDbxO}eK|$?X&1z7Coipo92>@L=RJ-fO*k&L+TF=v!hl{&-o&fikNQ;mb9O_jy zf$sLNJ2>Gwy5(i-)PsBX-1T6B81ssaevIF%&yZV8;-XD@M<>~EmIxQ4) z`dh3`{se{5>3LUO%AtCxde|}$*rBXDcVJQjo$_f}{L_BXFM>jGxyOvAc&>-OLGBoD z?eI-HcHlN!`G{#z7mtm4YQt9J*J~aPk5)$^66#=|3=lagVw^VtQxuH@erJrIiVyP_ zTWPg*pIL^>ppytRe#dj$b4>(&t+`ijM*vend3E()a5$bPqCjIF06udYk~ODoTy-iE zND}y#svvrA{KAIe3sJjAmjLgod``vMP^gf43mJ_=t&0_;kO_l=wK`J{yJx7J95|3@ zeF);u((`(``i*MsA$CPwZ%;Mj^%YG4( zVBO)qae*@isYOOlemDEp{xJJsH2`BTI^AZtQJ_DOr^qXwn@Q*3ubF@fGHRdW^=s)j zPJJ*WO?fw!XYzLtGvYFJSyx`g7(@=n(qCa4x6p!ZtX*fJIoZ*65}F`eTMc1)ApF2giK&O*o!?z6q&(zW zp@$u^*6y1QH8AHuP!+8NiW+#{TP_+pY5{;9u+$8s1YBj==YVnBoLH5%IWiM7y;aXo zO4}_6%nwq9f& zcEEtI5#k9*HE2$(8NP}U11)3Ti7UWy>*BHxx058jdz5N-jvru&wlOqfz`e_)B9^363~6n z6A5)PYFxUznGKGiR(ocsj^A>ZNhG^8M0)IHu9QW|Dyw-(KaIfwXC}s1#b??2)bT@c zVoa@l8qOm0>W0`kfS|%BW}ee1C40T#KkI%UB!Ov(Wb&u?aeyFvgL5N77KK?>HUA6; z9|a7#vnhFs`_uyx2xSfM^oEs5c72LI0tfHXzXH{xg(Ka6k(D-e;drO#%&#N=`3(-A}+UpcDNrvi4Hp-``@a*ZwVP3o4(nva_t-F OFt}`@Q+(0!{(k^BzMXdf literal 0 HcmV?d00001 From 1fc9413c4864891984929055aa6491b4057993fb Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Thu, 11 Jun 2020 17:20:53 +0300 Subject: [PATCH 08/70] Fix #1153. (#1160) Add ability for SyncQueue to recover from unexpected MissingParent. --- beacon_chain/beacon_node.nim | 6 ++++- beacon_chain/sync_manager.nim | 43 +++++++++++++++++++++++------------ tests/test_sync_manager.nim | 27 ++++++++++++++-------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 4ad9b89e6..8aab53f52 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -542,6 +542,10 @@ proc runForwardSyncLoop(node: BeaconNode) {.async.} = 1'u64 result = epoch.compute_start_slot_at_epoch() + proc getFirstSlotAtFinalizedEpoch(): Slot {.gcsafe.} = + let fepoch = node.blockPool.headState.data.data.finalized_checkpoint.epoch + compute_start_slot_at_epoch(fepoch) + proc updateLocalBlocks(list: openarray[SignedBeaconBlock]): Result[void, BlockError] = debug "Forward sync imported blocks", count = len(list), local_head_slot = getLocalHeadSlot() @@ -583,7 +587,7 @@ proc runForwardSyncLoop(node: BeaconNode) {.async.} = node.syncManager = newSyncManager[Peer, PeerID]( node.network.peerPool, getLocalHeadSlot, getLocalWallSlot, - updateLocalBlocks, + getFirstSlotAtFinalizedEpoch, updateLocalBlocks, # 4 blocks per chunk is the optimal value right now, because our current # syncing speed is around 4 blocks per second. So there no need to request # more then 4 blocks right now. As soon as `store_speed` value become diff --git a/beacon_chain/sync_manager.nim b/beacon_chain/sync_manager.nim index 589bdce4b..099ef129d 100644 --- a/beacon_chain/sync_manager.nim +++ b/beacon_chain/sync_manager.nim @@ -1,7 +1,7 @@ import chronicles import options, deques, heapqueue, tables, strutils, sequtils, math, algorithm import stew/results, chronos, chronicles -import spec/datatypes, spec/digest, peer_pool, eth2_network +import spec/[datatypes, digest], peer_pool, eth2_network import eth/async_utils import block_pools/block_pools_types @@ -66,22 +66,18 @@ type SyncQueue*[T] = ref object inpSlot*: Slot outSlot*: Slot - startSlot*: Slot lastSlot: Slot chunkSize*: uint64 queueSize*: int - counter*: uint64 pending*: Table[uint64, SyncRequest[T]] - waiters: seq[SyncWaiter[T]] syncUpdate*: SyncUpdateCallback[T] - + getFirstSlotAFE*: GetSlotCallback debtsQueue: HeapQueue[SyncRequest[T]] debtsCount: uint64 readyQueue: HeapQueue[SyncResult[T]] - zeroPoint: Option[Slot] suspects: seq[SyncResult[T]] @@ -95,6 +91,7 @@ type toleranceValue: uint64 getLocalHeadSlot: GetSlotCallback getLocalWallSlot: GetSlotCallback + getFirstSlotAFE: GetSlotCallback syncUpdate: SyncUpdateCallback[A] chunkSize: uint64 queue: SyncQueue[A] @@ -211,6 +208,7 @@ proc isEmpty*[T](sr: SyncRequest[T]): bool {.inline.} = proc init*[T](t1: typedesc[SyncQueue], t2: typedesc[T], start, last: Slot, chunkSize: uint64, updateCb: SyncUpdateCallback[T], + fsafeCb: GetSlotCallback, queueSize: int = -1): SyncQueue[T] = ## Create new synchronization queue with parameters ## @@ -296,11 +294,11 @@ proc init*[T](t1: typedesc[SyncQueue], t2: typedesc[T], chunkSize: chunkSize, queueSize: queueSize, syncUpdate: updateCb, + getFirstSlotAFE: fsafeCb, waiters: newSeq[SyncWaiter[T]](), counter: 1'u64, pending: initTable[uint64, SyncRequest[T]](), debtsQueue: initHeapQueue[SyncRequest[T]](), - zeroPoint: some[Slot](start), inpSlot: start, outSlot: start ) @@ -561,13 +559,25 @@ proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], sq.zeroPoint = none[Slot]() else: # If we got `BlockError.MissingParent` and `zero-point` is not set - # it means that peer returns chain of blocks with holes. + # it means that peer returns chain of blocks with holes or block_pool + # in incorrect state. We going to rewind to the latest finalized + # epoch. let req = item.request - warn "Received sequence of blocks with holes", peer = req.item, - request_slot = req.slot, request_count = req.count, - request_step = req.step, blocks_count = len(item.data), - blocks_map = getShortMap(req, item.data) - req.item.updateScore(PeerScoreBadBlocks) + let finalizedSlot = sq.getFirstSlotAFE() + if finalizedSlot < req.slot: + warn "Unexpected missing parent, rewind to latest finalized epoch slot", + peer = req.item, to_slot = finalizedSlot, + request_slot = req.slot, request_count = req.count, + request_step = req.step, blocks_count = len(item.data), + blocks_map = getShortMap(req, item.data) + await sq.resetWait(some(finalizedSlot)) + else: + error "Unexpected missing parent at finalized epoch slot", + peer = req.item, to_slot = finalizedSlot, + request_slot = req.slot, request_count = req.count, + request_step = req.step, blocks_count = len(item.data), + blocks_map = getShortMap(req, item.data) + req.item.updateScore(PeerScoreBadBlocks) elif res.error == BlockError.Invalid: let req = item.request warn "Received invalid sequence of blocks", peer = req.item, @@ -668,6 +678,7 @@ proc speed*(start, finish: SyncMoment): float {.inline.} = proc newSyncManager*[A, B](pool: PeerPool[A, B], getLocalHeadSlotCb: GetSlotCallback, getLocalWallSlotCb: GetSlotCallback, + getFSAFECb: GetSlotCallback, updateLocalBlocksCb: UpdateLocalBlocksCallback, maxStatusAge = uint64(SLOTS_PER_EPOCH * 4), maxHeadAge = uint64(SLOTS_PER_EPOCH * 1), @@ -687,7 +698,7 @@ proc newSyncManager*[A, B](pool: PeerPool[A, B], return res let queue = SyncQueue.init(A, getLocalHeadSlotCb(), getLocalWallSlotCb(), - chunkSize, syncUpdate, 2) + chunkSize, syncUpdate, getFSAFECb, 2) result = SyncManager[A, B]( pool: pool, @@ -695,6 +706,7 @@ proc newSyncManager*[A, B](pool: PeerPool[A, B], getLocalHeadSlot: getLocalHeadSlotCb, syncUpdate: syncUpdate, getLocalWallSlot: getLocalWallSlotCb, + getFirstSlotAFE: getFSAFECb, maxHeadAge: maxHeadAge, maxRecurringFailures: maxRecurringFailures, sleepTime: sleepTime, @@ -1000,7 +1012,8 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = else: if headSlot > man.queue.lastSlot: man.queue = SyncQueue.init(A, headSlot, wallSlot, - man.chunkSize, man.syncUpdate, 2) + man.chunkSize, man.syncUpdate, + man.getFirstSlotAFE, 2) debug "Synchronization loop starting new worker", peer = peer, wall_head_slot = wallSlot, local_head_slot = headSlot, peer_score = peer.getScore(), topics = "syncman" diff --git a/tests/test_sync_manager.nim b/tests/test_sync_manager.nim index 12e6f8ba6..4e29aa37b 100644 --- a/tests/test_sync_manager.nim +++ b/tests/test_sync_manager.nim @@ -13,6 +13,9 @@ proc `$`*(peer: SomeTPeer): string = proc updateScore(peer: SomeTPeer, score: int) = discard +proc getFirstSlotAtFinalizedEpoch(): Slot = + Slot(0) + suite "SyncManager test suite": proc createChain(start, finish: Slot): seq[SignedBeaconBlock] = doAssert(start <= finish) @@ -30,7 +33,8 @@ suite "SyncManager test suite": test "[SyncQueue] Start and finish slots equal": let p1 = SomeTPeer() - var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(0), 1'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(0), 1'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) check len(queue) == 1 var r11 = queue.pop(Slot(0), p1) check len(queue) == 0 @@ -45,7 +49,8 @@ suite "SyncManager test suite": r11.slot == Slot(0) and r11.count == 1'u64 and r11.step == 1'u64 test "[SyncQueue] Two full requests success/fail": - var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(1), 1'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(1), 1'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) let p1 = SomeTPeer() let p2 = SomeTPeer() check len(queue) == 2 @@ -72,7 +77,8 @@ suite "SyncManager test suite": r22.slot == Slot(1) and r22.count == 1'u64 and r22.step == 1'u64 test "[SyncQueue] Full and incomplete success/fail start from zero": - var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 2'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 2'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) let p1 = SomeTPeer() let p2 = SomeTPeer() let p3 = SomeTPeer() @@ -110,7 +116,8 @@ suite "SyncManager test suite": r33.slot == Slot(4) and r33.count == 1'u64 and r33.step == 1'u64 test "[SyncQueue] Full and incomplete success/fail start from non-zero": - var queue = SyncQueue.init(SomeTPeer, Slot(1), Slot(5), 3'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(1), Slot(5), 3'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) let p1 = SomeTPeer() let p2 = SomeTPeer() check len(queue) == 5 @@ -137,7 +144,8 @@ suite "SyncManager test suite": r42.slot == Slot(4) and r42.count == 2'u64 and r42.step == 1'u64 test "[SyncQueue] Smart and stupid success/fail": - var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 5'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 5'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) let p1 = SomeTPeer() let p2 = SomeTPeer() check len(queue) == 5 @@ -164,7 +172,8 @@ suite "SyncManager test suite": r52.slot == Slot(4) and r52.count == 1'u64 and r52.step == 1'u64 test "[SyncQueue] One smart and one stupid + debt split + empty": - var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 5'u64, syncUpdate) + var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(4), 5'u64, syncUpdate, + getFirstSlotAtFinalizedEpoch) let p1 = SomeTPeer() let p2 = SomeTPeer() let p3 = SomeTPeer() @@ -210,7 +219,7 @@ suite "SyncManager test suite": var chain = createChain(Slot(0), Slot(2)) var queue = SyncQueue.init(SomeTPeer, Slot(0), Slot(2), 1'u64, - syncReceiver, 1) + syncReceiver, getFirstSlotAtFinalizedEpoch, 1) let p1 = SomeTPeer() let p2 = SomeTPeer() let p3 = SomeTPeer() @@ -253,7 +262,7 @@ suite "SyncManager test suite": var chain = createChain(Slot(5), Slot(11)) var queue = SyncQueue.init(SomeTPeer, Slot(5), Slot(11), 2'u64, - syncReceiver, 2) + syncReceiver, getFirstSlotAtFinalizedEpoch, 2) let p1 = SomeTPeer() let p2 = SomeTPeer() let p3 = SomeTPeer() @@ -303,7 +312,7 @@ suite "SyncManager test suite": var chain = createChain(Slot(5), Slot(18)) var queue = SyncQueue.init(SomeTPeer, Slot(5), Slot(18), 2'u64, - syncReceiver, 2) + syncReceiver, getFirstSlotAtFinalizedEpoch, 2) let p1 = SomeTPeer() let p2 = SomeTPeer() let p3 = SomeTPeer() From 17343442ead52918db7b9a95dfde83552cd2b845 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Mon, 1 Jun 2020 22:48:20 +0300 Subject: [PATCH 09/70] Implement more of the KeyStore spec and integrate it in the beacon node --- beacon_chain/beacon_node.nim | 17 +- beacon_chain/conf.nim | 95 ++++++----- beacon_chain/interop.nim | 12 +- beacon_chain/merkle_minimal.nim | 14 +- beacon_chain/spec/crypto.nim | 19 +++ beacon_chain/spec/keystore.nim | 249 ++++++++++++++++++++++++----- beacon_chain/ssz.nim | 1 + beacon_chain/ssz/merkleization.nim | 2 +- beacon_chain/validator_client.nim | 2 +- beacon_chain/validator_duties.nim | 2 +- beacon_chain/validator_keygen.nim | 181 ++++++++++++++------- scripts/connect_to_testnet.nims | 4 +- scripts/launch_local_testnet.sh | 19 ++- scripts/reset_testnet.sh | 14 +- scripts/testnet0.env | 4 +- scripts/testnet1.env | 4 +- tests/simulation/start.sh | 6 +- tests/simulation/vars.sh | 1 + tests/test_interop.nim | 7 +- tests/test_keystore.nim | 48 ++++-- 20 files changed, 476 insertions(+), 225 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 8aab53f52..327776f40 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -998,7 +998,7 @@ programMain: of createTestnet: var deposits: seq[Deposit] for i in config.firstValidator.int ..< config.totalValidators.int: - let depositFile = config.validatorsDir / + let depositFile = config.testnetDepositsDir / validatorFileBaseName(i) & ".deposit.json" try: deposits.add Json.loadFile(depositFile, Deposit) @@ -1096,15 +1096,13 @@ programMain: node.start() of makeDeposits: - createDir(config.depositsDir) + createDir(config.outValidatorsDir) let - quickstartDeposits = generateDeposits( - config.totalQuickstartDeposits, config.depositsDir, false) - - randomDeposits = generateDeposits( - config.totalRandomDeposits, config.depositsDir, true, - firstIdx = config.totalQuickstartDeposits) + deposits = generateDeposits( + config.totalDeposits, + config.outValidatorsDir, + config.outSecretsDir).tryGet if config.web3Url.len > 0 and config.depositContractAddress.len > 0: if config.minDelay > config.maxDelay: @@ -1121,8 +1119,9 @@ programMain: depositContract = config.depositContractAddress waitFor sendDeposits( - quickstartDeposits & randomDeposits, + deposits, config.web3Url, config.depositContractAddress, config.depositPrivateKey, delayGenerator) + diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 0a5bab384..a8f26860b 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -1,11 +1,11 @@ {.push raises: [Defect].} import - os, options, strformat, strutils, + os, options, strformat, chronicles, confutils, json_serialization, confutils/defs, confutils/std/net, chronicles/options as chroniclesOptions, - spec/[crypto] + spec/[crypto, keystore] export defs, enabledLogLevel, parseCmdArg, completeCmdArg @@ -39,11 +39,6 @@ type desc: "The Eth1 network tracked by the beacon node." name: "eth1-network" }: Eth1Network - quickStart* {. - defaultValue: false - desc: "Run in quickstart mode" - name: "quick-start" }: bool - dataDir* {. defaultValue: config.defaultDataDir() desc: "The directory where nimbus will store all blockchain data." @@ -60,6 +55,10 @@ type desc: "Address of the deposit contract." name: "deposit-contract" }: string + nonInteractive* {. + desc: "Do not display interative prompts. Quit on missing configuration." + name: "non-interactive" }: bool + case cmd* {. command defaultValue: noCommand }: BNStartUpCmd @@ -106,6 +105,14 @@ type abbr: "v" name: "validator" }: seq[ValidatorKeyPath] + validatorsDir* {. + desc: "A directory containing validator keystores" + name: "validators-dir" }: Option[InputDir] + + secretsDir* {. + desc: "A directory containing validator keystore passwords" + name: "secrets-dir" }: Option[InputDir] + stateSnapshot* {. desc: "Json file specifying a recent state snapshot." abbr: "s" @@ -177,13 +184,12 @@ type name: "dump" }: bool of createTestnet: - validatorsDir* {. - desc: "Directory containing validator descriptors named 'vXXXXXXX.deposit.json'." - abbr: "d" + testnetDepositsDir* {. + desc: "Directory containing validator keystores." name: "validators-dir" }: InputDir totalValidators* {. - desc: "The number of validators in the newly created chain." + desc: "The number of validator deposits in the newly created chain." name: "total-validators" }: uint64 firstValidator* {. @@ -209,7 +215,6 @@ type genesisOffset* {. defaultValue: 5 desc: "Seconds from now to add to genesis time." - abbr: "g" name: "genesis-offset" }: int outputGenesis* {. @@ -231,34 +236,34 @@ type name: "keyfile" }: seq[ValidatorKeyPath] of makeDeposits: - totalQuickstartDeposits* {. - defaultValue: 0 - desc: "Number of quick-start deposits to generate." - name: "quickstart-deposits" }: int + totalDeposits* {. + defaultValue: 1 + desc: "Number of deposits to generate." + name: "count" }: int - totalRandomDeposits* {. - defaultValue: 0 - desc: "Number of secure random deposits to generate." - name: "random-deposits" }: int - - depositsDir* {. + outValidatorsDir* {. defaultValue: "validators" - desc: "Folder to write deposits to." - name: "deposits-dir" }: string + desc: "Output folder for validator keystores and deposits." + name: "out-validators-dir" }: string + + outSecretsDir* {. + defaultValue: "secrets" + desc: "Output folder for randomly generated keystore passphrases." + name: "out-secrets-dir" }: string depositPrivateKey* {. defaultValue: "" - desc: "Private key of the controlling (sending) account", + desc: "Private key of the controlling (sending) account.", name: "deposit-private-key" }: string minDelay* {. defaultValue: 0.0 - desc: "Minimum possible delay between making two deposits (in seconds)" + desc: "Minimum possible delay between making two deposits (in seconds)." name: "min-delay" }: float maxDelay* {. defaultValue: 0.0 - desc: "Maximum possible delay between making two deposits (in seconds)" + desc: "Maximum possible delay between making two deposits (in seconds)." name: "max-delay" }: float ValidatorClientConf* = object @@ -273,6 +278,10 @@ type abbr: "d" name: "data-dir" }: OutDir + nonInteractive* {. + desc: "Do not display interative prompts. Quit on missing configuration." + name: "non-interactive" }: bool + case cmd* {. command defaultValue: VCNoCommand }: VCStartUpCmd @@ -290,10 +299,18 @@ type validators* {. required - desc: "Path to a validator private key, as generated by makeDeposits." + desc: "Path to a validator key store, as generated by makeDeposits." abbr: "v" name: "validator" }: seq[ValidatorKeyPath] + validatorsDir* {. + desc: "A directory containing validator keystores" + name: "validators-dir" }: Option[InputDir] + + secretsDir* {. + desc: "A directory containing validator keystore passwords" + name: "secrets-dir" }: Option[InputDir] + proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string = let dataDir = when defined(windows): "AppData" / "Roaming" / "Nimbus" @@ -315,7 +332,7 @@ func dumpDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "dump" func localValidatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = - conf.dataDir / "validators" + string conf.validatorsDir.get(InputDir(conf.dataDir / "validators")) func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "db" @@ -328,26 +345,6 @@ func defaultListenAddress*(conf: BeaconNodeConf|ValidatorClientConf): ValidIpAdd func defaultAdminListenAddress*(conf: BeaconNodeConf|ValidatorClientConf): ValidIpAddress = (static ValidIpAddress.init("127.0.0.1")) -iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey = - for validatorKeyFile in conf.validators: - try: - yield validatorKeyFile.load - except CatchableError as err: - warn "Failed to load validator private key", - file = validatorKeyFile.string, err = err.msg - - try: - for kind, file in walkDir(conf.localValidatorsDir): - if kind in {pcFile, pcLinkToFile} and - cmpIgnoreCase(".privkey", splitFile(file).ext) == 0: - try: - yield ValidatorPrivKey.init(readFile(file).string) - except CatchableError as err: - warn "Failed to load a validator private key", file, err = err.msg - except OSError as err: - warn "Cannot load validator keys", - dir = conf.localValidatorsDir, err = err.msg - template writeValue*(writer: var JsonWriter, value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) = writer.writeValue(string value) diff --git a/beacon_chain/interop.nim b/beacon_chain/interop.nim index b46c927a0..41e5c362f 100644 --- a/beacon_chain/interop.nim +++ b/beacon_chain/interop.nim @@ -3,7 +3,7 @@ import stew/endians2, stint, ./extras, ./ssz/merkleization, - spec/[crypto, datatypes, digest, helpers] + spec/[crypto, datatypes, digest, helpers, keystore] func get_eth1data_stub*(deposit_count: uint64, current_epoch: Epoch): Eth1Data = # https://github.com/ethereum/eth2.0-pm/blob/e596c70a19e22c7def4fd3519e20ae4022349390/interop/mocked_eth1data/README.md @@ -16,7 +16,7 @@ func get_eth1data_stub*(deposit_count: uint64, current_epoch: Epoch): Eth1Data = block_hash: hash_tree_root(hash_tree_root(voting_period).data), ) -func makeInteropPrivKey*(i: int): BlsResult[ValidatorPrivKey] = +func makeInteropPrivKey*(i: int): ValidatorPrivKey = var bytes: array[32, byte] bytes[0..7] = uint64(i).toBytesLE() @@ -28,19 +28,13 @@ func makeInteropPrivKey*(i: int): BlsResult[ValidatorPrivKey] = privkeyBytes = eth2hash(bytes) key = (UInt256.fromBytesLE(privkeyBytes.data) mod curveOrder).toBytesBE() - ValidatorPrivKey.fromRaw(key) + ValidatorPrivKey.fromRaw(key).get const eth1BlockHash* = block: var x: Eth2Digest for v in x.data.mitems: v = 0x42 x -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials -func makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest = - var bytes = eth2hash(k.toRaw()) - bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8 - bytes - func makeDeposit*( pubkey: ValidatorPubKey, privkey: ValidatorPrivKey, epoch = 0.Epoch, amount: Gwei = MAX_EFFECTIVE_BALANCE.Gwei, diff --git a/beacon_chain/merkle_minimal.nim b/beacon_chain/merkle_minimal.nim index df31c3b69..281bb86a8 100644 --- a/beacon_chain/merkle_minimal.nim +++ b/beacon_chain/merkle_minimal.nim @@ -26,16 +26,6 @@ func round_step_down*(x: Natural, step: static Natural): int {.inline.} = else: result = x - x mod step -let ZeroHashes = block: - # hashes for a merkle tree full of zeros for leafs - var zh = @[Eth2Digest()] - for i in 1 ..< DEPOSIT_CONTRACT_TREE_DEPTH: - let nodehash = withEth2Hash: - h.update zh[i-1] - h.update zh[i-1] - zh.add nodehash - zh - type SparseMerkleTree*[Depth: static int] = object ## Sparse Merkle tree # There is an extra "depth" layer to store leaf nodes @@ -67,7 +57,7 @@ proc merkleTreeFromLeaves*( # with the zeroHash corresponding to the current depth let nodeHash = withEth2Hash: h.update result.nnznodes[depth-1][^1] - h.update ZeroHashes[depth-1] + h.update zeroHashes[depth-1] result.nnznodes[depth].add nodeHash proc getMerkleProof*[Depth: static int]( @@ -85,7 +75,7 @@ proc getMerkleProof*[Depth: static int]( if nodeIdx < tree.nnznodes[depth].len: result[depth] = tree.nnznodes[depth][nodeIdx] else: - result[depth] = ZeroHashes[depth] + result[depth] = zeroHashes[depth] proc attachMerkleProofs*(deposits: var seq[Deposit]) = let deposit_data_roots = mapIt(deposits, it.data.hash_tree_root) diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index 6f35f933e..666cd6ba1 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -69,6 +69,8 @@ type BlsResult*[T] = Result[T, cstring] + RandomSourceDepleted* = object of CatchableError + func `==`*(a, b: BlsValue): bool = if a.kind != b.kind: return false if a.kind == Real: @@ -82,6 +84,9 @@ template `==`*[N, T](a: BlsValue[N, T], b: T): bool = template `==`*[N, T](a: T, b: BlsValue[N, T]): bool = a == b.blsValue +template `==`*(a, b: ValidatorPrivKey): bool = + blscurve.SecretKey(a) == blscurve.SecretKey(b) + # API # ---------------------------------------------------------------------- # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures @@ -341,3 +346,17 @@ func init*(T: typedesc[ValidatorSig], data: array[RawSigSize, byte]): T {.noInit if v.isErr: raise (ref ValueError)(msg: $v.error) return v[] + +proc getRandomBytes*(n: Natural): seq[byte] + {.raises: [RandomSourceDepleted, Defect].} = + result = newSeq[byte](n) + if randomBytes(result) != result.len: + raise newException(RandomSourceDepleted, "Failed to generate random bytes") + +proc getRandomBytesOrPanic*(output: var openarray[byte]) = + doAssert randomBytes(output) == output.len + +proc getRandomBytesOrPanic*(n: Natural): seq[byte] = + result = newSeq[byte](n) + getRandomBytesOrPanic(result) + diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index b3b98c14f..4c6991b27 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -6,13 +6,13 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - json, math, strutils, - eth/keyfile/uuid, - stew/[results, byteutils], - nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], - ./crypto + json, math, strutils, strformat, + eth/keyfile/uuid, stew/[results, byteutils, bitseqs, bitops2], + nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], blscurve, + datatypes, crypto, digest, helpers -export results +export + results {.push raises: [Defect].} @@ -64,6 +64,22 @@ type KsResult*[T] = Result[T, cstring] + Eth2KeyKind* = enum + signingKeyKind # Also known as voting key + withdrawalKeyKind + + Mnemonic* = distinct string + KeyPath* = distinct string + KeyStorePass* = distinct string + KeyStoreContent* = distinct JsonString + KeySeed* = distinct seq[byte] + + Credentials* = object + mnemonic*: Mnemonic + keyStore*: KeyStoreContent + signingKey*: ValidatorPrivKey + withdrawalKey*: ValidatorPrivKey + const saltSize = 32 @@ -80,6 +96,108 @@ const prf: "hmac-sha256" ) + # https://eips.ethereum.org/EIPS/eip-2334 + eth2KeyPurpose = 12381 + eth2CoinType* = 3600 + + # https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md + wordListLen = 2048 + englishWords = split slurp("english_word_list.txt") + +iterator pathNodesImpl(path: string): Natural + {.raises: [ValueError].} = + for elem in path.split("/"): + if elem == "m": continue + yield parseInt(elem) + +func append*(path: KeyPath, pathNode: Natural): KeyPath = + KeyPath(path.string & "/" & $pathNode) + +func validateKeyPath*(path: TaintedString): KeyPath + {.raises: [ValueError].} = + for elem in pathNodesImpl(path.string): discard elem + KeyPath path + +iterator pathNodes(path: KeyPath): Natural = + try: + for elem in pathNodesImpl(path.string): + yield elem + except ValueError: + doAssert false, "Make sure you've validated the key path with `validateKeyPath`" + +func makeKeyPath*(validatorIdx: Natural, + keyType: Eth2KeyKind): KeyPath = + # https://eips.ethereum.org/EIPS/eip-2334 + let use = case keyType + of withdrawalKeyKind: "0" + of signingKeyKind: "0/0" + + try: + KeyPath &"m/{eth2KeyPurpose}/{eth2CoinType}/{validatorIdx}/{use}" + except ValueError: + raiseAssert "All values above can be converted successfully to strings" + +func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed = + # https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed + let salt = "mnemonic-" & password.string + KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64) + +proc generateMnemonic*(words: openarray[string], + entropyParam: openarray[byte] = @[]): Mnemonic = + # https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic + doAssert words.len == wordListLen + + var entropy: seq[byte] + if entropyParam.len == 0: + entropy = getRandomBytesOrPanic(32) + else: + doAssert entropyParam.len >= 128 and + entropyParam.len <= 256 and + entropyParam.len mod 32 == 0 + entropy = @entropyParam + + let + checksumBits = entropy.len div 4 # ranges from 4 to 8 + mnemonicWordCount = 12 + (checksumBits - 4) * 3 + checksum = sha256.digest(entropy) + + entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits)) + + var res = words[entropy.getBitsBE(0..10)] + for i in 1 ..< mnemonicWordCount: + let + firstBit = i*11 + lastBit = firstBit + 10 + res.add " " + res.add words[entropy.getBitsBE(firstBit..lastBit)] + + Mnemonic res + +proc deriveChildKey*(parentKey: ValidatorPrivKey, + index: Natural): ValidatorPrivKey = + doAssert derive_child_secretKey(SecretKey result, + SecretKey parentKey, + uint32 index) + +proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey = + doAssert derive_master_secretKey(SecretKey result, + seq[byte] seed) + +proc deriveMasterKey*(mnemonic: Mnemonic, + password: KeyStorePass): ValidatorPrivKey = + deriveMasterKey(getSeed(mnemonic, password)) + +proc deriveChildKey*(masterKey: ValidatorPrivKey, + path: KeyPath): ValidatorPrivKey = + result = masterKey + for idx in pathNodes(path): + result = deriveChildKey(result, idx) + +proc keyFromPath*(mnemonic: Mnemonic, + password: KeyStorePass, + path: KeyPath): ValidatorPrivKey = + deriveChildKey(deriveMasterKey(mnemonic, password), path) + proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] = var ctx: sha256 ctx.init() @@ -100,12 +218,11 @@ template hexToBytes(data, name: string): untyped = except ValueError: return err "ks: failed to parse " & name -proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] = - let ks = - try: - parseJson(data) - except Exception: - return err "ks: failed to parse keystore" +proc decryptKeystore*(data: KeyStoreContent, + password: KeyStorePass): KsResult[ValidatorPrivKey] = + # TODO: `parseJson` can raise a general `Exception` + let ks = try: parseJson(data.string) + except Exception: return err "ks: failed to parse keystore" var decKey: seq[byte] @@ -126,7 +243,7 @@ proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] = kdfParams = crypto.kdf.params salt = hexToBytes(kdfParams.salt, "salt") - decKey = sha256.pbkdf2(passphrase, salt, kdfParams.c, kdfParams.dklen) + decKey = sha256.pbkdf2(password.string, salt, kdfParams.c, kdfParams.dklen) iv = hexToBytes(crypto.cipher.params.iv, "iv") cipherMsg = hexToBytes(crypto.cipher.message, "cipher") checksumMsg = hexToBytes(crypto.checksum.message, "checksum") @@ -151,41 +268,41 @@ proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] = aesCipher.decrypt(cipherMsg, secret) aesCipher.clear() - result = ok secret + ValidatorPrivKey.fromRaw(secret) -proc encryptKeystore*[T: KdfParams](secret: openarray[byte]; - passphrase: string; - path=""; - salt: openarray[byte] = @[]; - iv: openarray[byte] = @[]; - ugly=true): KsResult[string] = +proc encryptKeystore*(T: type[KdfParams], + privKey: ValidatorPrivkey, + password = KeyStorePass "", + path = KeyPath "", + salt: openarray[byte] = @[], + iv: openarray[byte] = @[], + ugly = true): KeyStoreContent = var + secret = privKey.toRaw[^32..^1] decKey: seq[byte] aesCipher: CTR[aes128] aesIv = newSeq[byte](aes128.sizeBlock) kdfSalt = newSeq[byte](saltSize) cipherMsg = newSeq[byte](secret.len) - if salt.len == saltSize: + if salt.len > 0: + doAssert salt.len == saltSize kdfSalt = @salt - elif salt.len > 0: - return err "ks: invalid salt" - elif randomBytes(kdfSalt) != saltSize: - return err "ks: no random bytes for salt" + else: + getRandomBytesOrPanic(kdfSalt) - if iv.len == aes128.sizeBlock: + if iv.len > 0: + doAssert iv.len == aes128.sizeBlock aesIv = @iv - elif iv.len > 0: - return err "ks: invalid iv" - elif randomBytes(aesIv) != aes128.sizeBlock: - return err "ks: no random bytes for iv" + else: + getRandomBytesOrPanic(aesIv) when T is KdfPbkdf2: - decKey = sha256.pbkdf2(passphrase, kdfSalt, pbkdf2Params.c, + decKey = sha256.pbkdf2(password.string, kdfSalt, pbkdf2Params.c, pbkdf2Params.dklen) var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "") - kdf.params.salt = kdfSalt.toHex() + kdf.params.salt = byteutils.toHex(kdfSalt) else: return @@ -193,29 +310,75 @@ proc encryptKeystore*[T: KdfParams](secret: openarray[byte]; aesCipher.encrypt(secret, cipherMsg) aesCipher.clear() - let pubkey = (? ValidatorPrivkey.fromRaw(secret)).toPubKey() + let pubkey = privKey.toPubKey() let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg) + uuid = uuidGenerate().get keystore = Keystore[T]( crypto: Crypto[T]( kdf: kdf, checksum: Checksum( function: "sha256", - message: sum.toHex() + message: byteutils.toHex(sum) ), cipher: Cipher( function: "aes-128-ctr", - params: CipherParams(iv: aesIv.toHex()), - message: cipherMsg.toHex() + params: CipherParams(iv: byteutils.toHex(aesIv)), + message: byteutils.toHex(cipherMsg) ) ), - pubkey: pubkey.toHex(), - path: path, - uuid: $(? uuidGenerate()), - version: 4 - ) + pubkey: toHex(pubkey), + path: path.string, + uuid: $uuid, + version: 4) + + KeyStoreContent if ugly: $(%keystore) + else: pretty(%keystore, indent=4) + +proc restoreCredentials*(mnemonic: Mnemonic, + password = KeyStorePass ""): Credentials = + let + withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind) + withdrawalKey = keyFromPath(mnemonic, password, withdrawalKeyPath) + + signingKeyPath = withdrawalKeyPath.append 0 + signingKey = deriveChildKey(withdrawalKey, 0) + + Credentials( + mnemonic: mnemonic, + keyStore: encryptKeystore(KdfPbkdf2, signingKey, password, signingKeyPath), + signingKey: signingKey, + withdrawalKey: withdrawalKey) + +proc generateCredentials*(entropy: openarray[byte] = @[], + password = KeyStorePass ""): Credentials = + let mnemonic = generateMnemonic(englishWords, entropy) + restoreCredentials(mnemonic, password) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials +proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest = + var bytes = eth2hash(k.toRaw()) + bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8 + bytes + +proc prepareDeposit*(credentials: Credentials, + amount = MAX_EFFECTIVE_BALANCE.Gwei): Deposit = + let + withdrawalPubKey = credentials.withdrawalKey.toPubKey + signingPubKey = credentials.signingKey.toPubKey + + var + ret = Deposit( + data: DepositData( + amount: amount, + pubkey: signingPubKey, + withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))) + + let domain = compute_domain(DOMAIN_DEPOSIT) + let signing_root = compute_signing_root(ret.getDepositMessage, domain) + + ret.data.signature = bls_sign(credentials.signingKey, signing_root.data) + ret - result = ok(if ugly: $(%keystore) - else: pretty(%keystore, indent=4)) diff --git a/beacon_chain/ssz.nim b/beacon_chain/ssz.nim index 059d0b879..d3311421d 100644 --- a/beacon_chain/ssz.nim +++ b/beacon_chain/ssz.nim @@ -19,3 +19,4 @@ import export merkleization, ssz_serialization, types + diff --git a/beacon_chain/ssz/merkleization.nim b/beacon_chain/ssz/merkleization.nim index bafa0d6ab..e12b8784d 100644 --- a/beacon_chain/ssz/merkleization.nim +++ b/beacon_chain/ssz/merkleization.nim @@ -72,7 +72,7 @@ func computeZeroHashes: array[sizeof(Limit) * 8, Eth2Digest] = for i in 1 .. result.high: result[i] = mergeBranches(result[i - 1], result[i - 1]) -const zeroHashes = computeZeroHashes() +const zeroHashes* = computeZeroHashes() func addChunk(merkleizer: var SszChunksMerkleizer, data: openarray[byte]) = doAssert data.len > 0 and data.len <= bytesPerChunk diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index fc3689230..84a8e35d4 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -21,7 +21,7 @@ import eth2_network, eth2_discovery, validator_pool, beacon_node_types, nimbus_binary_common, version, ssz/merkleization, - sync_manager, + sync_manager, validator_keygen, spec/eth2_apis/validator_callsigs_types, eth2_json_rpc_serialization diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index af19db8df..7fabf5d21 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -20,7 +20,7 @@ import # Local modules spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network], conf, time, validator_pool, state_transition, - attestation_pool, block_pool, eth2_network, + attestation_pool, block_pool, eth2_network, validator_keygen, beacon_node_common, beacon_node_types, nimbus_binary_common, mainchain_monitor, version, ssz/merkleization, interop, attestation_aggregation, sync_manager, sszdump diff --git a/beacon_chain/validator_keygen.nim b/beacon_chain/validator_keygen.nim index 1a9be08ae..e1650e231 100644 --- a/beacon_chain/validator_keygen.nim +++ b/beacon_chain/validator_keygen.nim @@ -1,76 +1,153 @@ import - os, strutils, + os, strutils, terminal, chronicles, chronos, blscurve, nimcrypto, json_serialization, serialization, - web3, stint, eth/keys, - spec/[datatypes, digest, crypto], conf, ssz/merkleization, interop, merkle_minimal + web3, stint, eth/keys, confutils, + spec/[datatypes, digest, crypto, keystore], conf, ssz/merkleization, merkle_minimal contract(DepositContract): proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32]) +const + keystoreFileName* = "keystore.json" + depositFileName* = "deposit.json" + type DelayGenerator* = proc(): chronos.Duration {.closure, gcsafe.} -proc writeTextFile(filename: string, contents: string) = - writeFile(filename, contents) - # echo "Wrote ", filename - -proc writeFile(filename: string, value: auto) = - Json.saveFile(filename, value, pretty = true) - # echo "Wrote ", filename +{.push raises: [Defect].} proc ethToWei(eth: UInt256): UInt256 = eth * 1000000000000000000.u256 -proc generateDeposits*(totalValidators: int, - outputDir: string, - randomKeys: bool, - firstIdx = 0): seq[Deposit] = - info "Generating deposits", totalValidators, outputDir, randomKeys - for i in 0 ..< totalValidators: - let - v = validatorFileBaseName(firstIdx + i) - depositFn = outputDir / v & ".deposit.json" - privKeyFn = outputDir / v & ".privkey" +proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf, + validatorsDir, keyName: string): Option[ValidatorPrivKey] = + let + keystorePath = validatorsDir / keyName / keystoreFileName + keystoreContents = KeyStoreContent: + try: readFile(keystorePath) + except IOError as err: + error "Failed to read keystore", err = err.msg, path = keystorePath + return - if existsFile(depositFn) and existsFile(privKeyFn): - try: - result.add Json.loadFile(depositFn, Deposit) - continue - except SerializationError as err: - debug "Rewriting unreadable deposit", err = err.formatMsg(depositFn) - discard + if conf.secretsDir.isSome: + let passphrasePath = conf.secretsDir.get / keyName + if fileExists(passphrasePath): + let + passphrase = KeyStorePass: + try: readFile(passphrasePath) + except IOError as err: + error "Failed to read passphrase file", err = err.msg, path = passphrasePath + return - var - privkey{.noInit.}: ValidatorPrivKey - pubKey{.noInit.}: ValidatorPubKey + let res = decryptKeystore(keystoreContents, passphrase) + if res.isOk: + return res.get.some + else: + error "Failed to decrypt keystore", keystorePath, passphrasePath + return - if randomKeys: - (pubKey, privKey) = crypto.newKeyPair().tryGet() + if conf.nonInteractive: + error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir", + keyName, validatorsDir, secretsDir = conf.secretsDir + return + + var remainingAttempts = 3 + var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"" + while remainingAttempts > 0: + let passphrase = KeyStorePass: + try: readPasswordFromStdin(prompt) + except IOError: + error "STDIN not readable. Cannot obtain KeyStore password" + return + + let decrypted = decryptKeystore(keystoreContents, passphrase) + if decrypted.isOk: + return decrypted.get.some else: - privKey = makeInteropPrivKey(i).tryGet() - pubKey = privKey.toPubKey() + prompt = "Keystore decryption failed. Please try again" + dec remainingAttempts - let dp = makeDeposit(pubKey, privKey) +iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey = + for validatorKeyFile in conf.validators: + try: + yield validatorKeyFile.load + except CatchableError as err: + error "Failed to load validator private key", + file = validatorKeyFile.string, err = err.msg + quit 1 - result.add(dp) + let validatorsDir = conf.localValidatorsDir + try: + for kind, file in walkDir(validatorsDir): + if kind == pcDir: + let keyName = splitFile(file).name + let key = loadKeyStore(conf, validatorsDir, keyName) + if key.isSome: + yield key.get + else: + quit 1 + except OSError as err: + error "Validator keystores directory not accessible", + path = validatorsDir, err = err.msg + quit 1 + +type + GenerateDepositsError = enum + RandomSourceDepleted, + FailedToCreateValidatoDir + FailedToCreateSecretFile + FailedToCreateKeystoreFile + FailedToCreateDepositFile + +proc generateDeposits*(totalValidators: int, + validatorsDir: string, + secretsDir: string): Result[seq[Deposit], GenerateDepositsError] = + var deposits: seq[Deposit] + + info "Generating deposits", totalValidators, validatorsDir, secretsDir + for i in 0 ..< totalValidators: + let password = KeyStorePass getRandomBytesOrPanic(32).toHex + let credentials = generateCredentials(password = password) + + let + keyName = $(credentials.signingKey.toPubKey) + validatorDir = validatorsDir / keyName + passphraseFile = secretsDir / keyName + depositFile = validatorDir / depositFileName + keystoreFile = validatorDir / keystoreFileName + + if existsDir(validatorDir) and existsFile(depositFile): + continue + + try: createDir validatorDir + except OSError, IOError: return err FailedToCreateValidatoDir + + try: writeFile(secretsDir / keyName, password.string) + except IOError: return err FailedToCreateSecretFile + + try: writeFile(keystoreFile, credentials.keyStore.string) + except IOError: return err FailedToCreateKeystoreFile + + deposits.add credentials.prepareDeposit() # Does quadratic additional work, but fast enough, and otherwise more # cleanly allows free intermixing of pre-existing and newly generated # deposit and private key files. TODO: only generate new Merkle proof # for the most recent deposit if this becomes bottleneck. - attachMerkleProofs(result) + attachMerkleProofs(deposits) + try: Json.saveFile(depositFile, deposits[^1], pretty = true) + except: return err FailedToCreateDepositFile - writeTextFile(privKeyFn, privKey.toHex()) - writeFile(depositFn, result[result.len - 1]) + ok deposits -proc sendDeposits*( - deposits: seq[Deposit], - web3Url, depositContractAddress, privateKey: string, - delayGenerator: DelayGenerator = nil) {.async.} = +{.pop.} +proc sendDeposits*(deposits: seq[Deposit], + web3Url, depositContractAddress, privateKey: string, + delayGenerator: DelayGenerator = nil) {.async.} = var web3 = await newWeb3(web3Url) if privateKey.len != 0: - web3.privateKey = PrivateKey.fromHex(privateKey).tryGet() + web3.privateKey = PrivateKey.fromHex(privateKey).tryGet else: let accounts = await web3.provider.eth_accounts() if accounts.len == 0: @@ -79,9 +156,9 @@ proc sendDeposits*( web3.defaultAccount = accounts[0] let contractAddress = Address.fromHex(depositContractAddress) + let depositContract = web3.contractSender(DepositContract, contractAddress) for i, dp in deposits: - let depositContract = web3.contractSender(DepositContract, contractAddress) discard await depositContract.deposit( Bytes48(dp.data.pubKey.toRaw()), Bytes32(dp.data.withdrawal_credentials.data), @@ -91,17 +168,3 @@ proc sendDeposits*( if delayGenerator != nil: await sleepAsync(delayGenerator()) -when isMainModule: - import confutils - - cli do (totalValidators: int = 125000, - outputDir: string = "validators", - randomKeys: bool = false, - web3Url: string = "", - depositContractAddress: string = ""): - let deposits = generateDeposits(totalValidators, outputDir, randomKeys) - - if web3Url.len() > 0 and depositContractAddress.len() > 0: - echo "Sending deposits to eth1..." - waitFor sendDeposits(deposits, web3Url, depositContractAddress, "") - echo "Done" diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index be9d2e6f7..9c992a813 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -87,6 +87,7 @@ cli do (skipGoerliKey {. .replace(")", "_") dataDir = buildDir / "data" / dataDirName validatorsDir = dataDir / "validators" + secretsDir = dataDir / "secrets" beaconNodeBinary = buildDir / "beacon_node_" & dataDirName var nimFlags = "-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS") @@ -137,7 +138,8 @@ cli do (skipGoerliKey {. mode = Verbose exec replace(&"""{beaconNodeBinary} makeDeposits --random-deposits=1 - --deposits-dir="{validatorsDir}" + --out-validators-dir="{validatorsDir}" + --out-secrets-dir="{secretsDir}" --deposit-private-key={privKey} --web3-url={web3Url} {depositContractOpt} diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index 602925d9e..d20a43fd0 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -114,8 +114,13 @@ fi NETWORK="testnet${TESTNET}" rm -rf "${DATA_DIR}" + DEPOSITS_DIR="${DATA_DIR}/deposits_dir" mkdir -p "${DEPOSITS_DIR}" + +SECRETS_DIR="${DATA_DIR}/secrets" +mkdir -p "${SECRETS_DIR}" + NETWORK_DIR="${DATA_DIR}/network_dir" mkdir -p "${NETWORK_DIR}" @@ -134,17 +139,16 @@ NETWORK_NIM_FLAGS=$(scripts/load-testnet-nim-flags.sh ${NETWORK}) $MAKE LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${NETWORK_NIM_FLAGS}" beacon_node ./build/beacon_node makeDeposits \ - --quickstart-deposits=${QUICKSTART_VALIDATORS} \ - --random-deposits=${RANDOM_VALIDATORS} \ - --deposits-dir="${DEPOSITS_DIR}" + --count=${TOTAL_VALIDATORS} \ + --out-validators-dir="${DEPOSITS_DIR}" \ + --out-secrets-dir="${SECRETS_DIR}" -TOTAL_VALIDATORS="$(( $QUICKSTART_VALIDATORS + $RANDOM_VALIDATORS ))" BOOTSTRAP_IP="127.0.0.1" ./build/beacon_node createTestnet \ --data-dir="${DATA_DIR}/node0" \ --validators-dir="${DEPOSITS_DIR}" \ --total-validators=${TOTAL_VALIDATORS} \ - --last-user-validator=${QUICKSTART_VALIDATORS} \ + --last-user-validator=${USER_VALIDATORS} \ --output-genesis="${NETWORK_DIR}/genesis.ssz" \ --output-bootstrap-file="${NETWORK_DIR}/bootstrap_nodes.txt" \ --bootstrap-address=${BOOTSTRAP_IP} \ @@ -199,11 +203,12 @@ for NUM_NODE in $(seq 0 $(( ${NUM_NODES} - 1 ))); do fi # Copy validators to individual nodes. - # The first $NODES_WITH_VALIDATORS nodes split them equally between them, after skipping the first $QUICKSTART_VALIDATORS. + # The first $NODES_WITH_VALIDATORS nodes split them equally between them, after skipping the first $USER_VALIDATORS. NODE_DATA_DIR="${DATA_DIR}/node${NUM_NODE}" mkdir -p "${NODE_DATA_DIR}/validators" if [[ $NUM_NODE -lt $NODES_WITH_VALIDATORS ]]; then - for KEYFILE in $(ls ${DEPOSITS_DIR}/*.privkey | tail -n +$(( $QUICKSTART_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do + # TODO: There are no longer privkey files + for KEYFILE in $(ls ${DEPOSITS_DIR}/*.privkey | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do cp -a "$KEYFILE" "${NODE_DATA_DIR}/validators/" done fi diff --git a/scripts/reset_testnet.sh b/scripts/reset_testnet.sh index 6057e54a5..f9a8bb4dd 100755 --- a/scripts/reset_testnet.sh +++ b/scripts/reset_testnet.sh @@ -46,6 +46,7 @@ ETH2_TESTNETS_ABS=$(cd "$ETH2_TESTNETS"; pwd) NETWORK_DIR_ABS="$ETH2_TESTNETS_ABS/nimbus/$NETWORK" DATA_DIR_ABS=$(mkdir -p "$DATA_DIR"; cd "$DATA_DIR"; pwd) DEPOSITS_DIR_ABS="$DATA_DIR_ABS/deposits" +SECRETS_DIR_ABS="$DATA_DIR_ABS/secrets" DEPOSIT_CONTRACT_ADDRESS="" DEPOSIT_CONTRACT_ADDRESS_ARG="" @@ -54,6 +55,7 @@ if [ "$WEB3_URL" != "" ]; then fi mkdir -p "$DEPOSITS_DIR_ABS" +mkdir -p "$SECRETS_DIR_ABS" if [ "$ETH1_PRIVATE_KEY" != "" ]; then make deposit_contract @@ -82,17 +84,15 @@ echo "Building Docker image..." make build ../build/beacon_node makeDeposits \ - --quickstart-deposits=$QUICKSTART_VALIDATORS \ - --random-deposits=$RANDOM_VALIDATORS \ - --deposits-dir="$DEPOSITS_DIR_ABS" - -TOTAL_VALIDATORS="$(( $QUICKSTART_VALIDATORS + $RANDOM_VALIDATORS ))" + --count=$TOTAL_VALIDATORS \ + --out-validators-dir="$DEPOSITS_DIR_ABS" \ + --out-secrets-dir="$SECRETS_DIR_ABS" ../build/beacon_node createTestnet \ --data-dir="$DATA_DIR_ABS" \ --validators-dir="$DEPOSITS_DIR_ABS" \ --total-validators=$TOTAL_VALIDATORS \ - --last-user-validator=$QUICKSTART_VALIDATORS \ + --last-user-validator=$USER_VALIDATORS \ --output-genesis="$NETWORK_DIR_ABS/genesis.ssz" \ --output-bootstrap-file="$NETWORK_DIR_ABS/bootstrap_nodes.txt" \ --bootstrap-address=$BOOTSTRAP_IP \ @@ -116,7 +116,7 @@ if [[ $PUBLISH_TESTNET_RESETS != "0" ]]; then --network=$NETWORK \ --deposits-dir="$DEPOSITS_DIR_ABS" \ --network-data-dir="$NETWORK_DIR_ABS" \ - --user-validators=$QUICKSTART_VALIDATORS \ + --user-validators=$USER_VALIDATORS \ --total-validators=$TOTAL_VALIDATORS \ > /tmp/reset-network.sh diff --git a/scripts/testnet0.env b/scripts/testnet0.env index ba5f434aa..bac0383ab 100644 --- a/scripts/testnet0.env +++ b/scripts/testnet0.env @@ -1,5 +1,5 @@ CONST_PRESET=minimal -QUICKSTART_VALIDATORS=8 -RANDOM_VALIDATORS=120 +USER_VALIDATORS=10 +TOTAL_VALIDATORS=256 BOOTSTRAP_PORT=9000 WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a diff --git a/scripts/testnet1.env b/scripts/testnet1.env index 68b8162e1..104c83829 100644 --- a/scripts/testnet1.env +++ b/scripts/testnet1.env @@ -1,6 +1,6 @@ CONST_PRESET=mainnet -QUICKSTART_VALIDATORS=8 -RANDOM_VALIDATORS=120 +USER_VALIDATORS=10 +TOTAL_VALIDATORS=256 BOOTSTRAP_PORT=9100 WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index 4fd0ed637..9613a7ccf 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -9,6 +9,7 @@ source "$(dirname "$0")/vars.sh" cd "$SIM_ROOT" mkdir -p "$SIMULATION_DIR" mkdir -p "$VALIDATORS_DIR" +mkdir -p "$SECRETS_DIR" cd "$GIT_ROOT" @@ -118,8 +119,9 @@ if [ ! -f "${LAST_VALIDATOR}" ]; then fi $BEACON_NODE_BIN makeDeposits \ - --quickstart-deposits="${NUM_VALIDATORS}" \ - --deposits-dir="$VALIDATORS_DIR" \ + --count="${NUM_VALIDATORS}" \ + --out-validators-dir="$VALIDATORS_DIR" \ + --out-secrets-dir="$SECRETS_DIR" \ $MAKE_DEPOSITS_WEB3_ARG $DELAY_ARGS \ --deposit-contract="${DEPOSIT_CONTRACT_ADDRESS}" diff --git a/tests/simulation/vars.sh b/tests/simulation/vars.sh index 8d003713d..4afa7479a 100644 --- a/tests/simulation/vars.sh +++ b/tests/simulation/vars.sh @@ -28,6 +28,7 @@ MASTER_NODE=$(( TOTAL_NODES - 1 )) SIMULATION_DIR="${SIM_ROOT}/data" METRICS_DIR="${SIM_ROOT}/prometheus" VALIDATORS_DIR="${SIM_ROOT}/validators" +SECRETS_DIR="${SIM_ROOT}/secrets" SNAPSHOT_FILE="${SIMULATION_DIR}/state_snapshot.ssz" NETWORK_BOOTSTRAP_FILE="${SIMULATION_DIR}/bootstrap_nodes.txt" BEACON_NODE_BIN="${GIT_ROOT}/build/beacon_node" diff --git a/tests/test_interop.nim b/tests/test_interop.nim index d7bb8d5ca..3c85de8d6 100644 --- a/tests/test_interop.nim +++ b/tests/test_interop.nim @@ -119,7 +119,7 @@ suiteReport "Interop": timedTest "Mocked start private key": for i, k in privateKeys: let - key = makeInteropPrivKey(i)[] + key = makeInteropPrivKey(i) v = k.parse(UInt256, 16) check: @@ -144,9 +144,8 @@ suiteReport "Interop": var deposits: seq[Deposit] for i in 0..<64: - let - privKey = makeInteropPrivKey(i)[] - deposits.add(makeDeposit(privKey.toPubKey(), privKey)) + let privKey = makeInteropPrivKey(i) + deposits.add makeDeposit(privKey.toPubKey(), privKey) const genesis_time = 1570500000 var diff --git a/tests/test_keystore.nim b/tests/test_keystore.nim index 6bfb33e4b..4013de891 100644 --- a/tests/test_keystore.nim +++ b/tests/test_keystore.nim @@ -10,7 +10,7 @@ import unittest, ./testutil, json, stew/byteutils, - ../beacon_chain/spec/keystore + ../beacon_chain/spec/[crypto, keystore] from strutils import replace @@ -79,23 +79,27 @@ const }""" #" password = "testpassword" - secret = hexToSeqByte("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") + secretBytes = hexToSeqByte("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") salt = hexToSeqByte("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") iv = hexToSeqByte("264daa3f303d7259501c93d997d84fe6") suiteReport "Keystore": + setup: + let secret = ValidatorPrivKey.fromRaw(secretBytes).get + timedTest "Pbkdf2 decryption": - let decrypt = decryptKeystore(pbkdf2Vector, password) + let decrypt = decryptKeystore(KeyStoreContent pbkdf2Vector, + KeyStorePass password) check decrypt.isOk check secret == decrypt.get() timedTest "Pbkdf2 encryption": - let encrypt = encryptKeystore[KdfPbkdf2](secret, password, salt=salt, iv=iv, - path="m/12381/60/0/0") - check encrypt.isOk - + let encrypt = encryptKeystore(KdfPbkdf2, secret, + KeyStorePass password, + salt=salt, iv=iv, + path = validateKeyPath "m/12381/60/0/0") var - encryptJson = parseJson(encrypt.get()) + encryptJson = parseJson(encrypt.string) pbkdf2Json = parseJson(pbkdf2Vector) encryptJson{"uuid"} = %"" pbkdf2Json{"uuid"} = %"" @@ -103,16 +107,27 @@ suiteReport "Keystore": check encryptJson == pbkdf2Json timedTest "Pbkdf2 errors": - check encryptKeystore[KdfPbkdf2](secret, "", salt = [byte 1]).isErr - check encryptKeystore[KdfPbkdf2](secret, "", iv = [byte 1]).isErr + expect Defect: + echo encryptKeystore(KdfPbkdf2, secret, salt = [byte 1]).string - check decryptKeystore(pbkdf2Vector, "wrong pass").isErr - check decryptKeystore(pbkdf2Vector, "").isErr - check decryptKeystore("{\"a\": 0}", "").isErr - check decryptKeystore("", "").isErr + expect Defect: + echo encryptKeystore(KdfPbkdf2, secret, iv = [byte 1]).string + + check decryptKeystore(KeyStoreContent pbkdf2Vector, + KeyStorePass "wrong pass").isErr + + check decryptKeystore(KeyStoreContent pbkdf2Vector, + KeyStorePass "").isErr + + check decryptKeystore(KeyStoreContent "{\"a\": 0}", + KeyStorePass "").isErr + + check decryptKeystore(KeyStoreContent "", + KeyStorePass "").isErr template checkVariant(remove): untyped = - check decryptKeystore(pbkdf2Vector.replace(remove, ""), password).isErr + check decryptKeystore(KeyStoreContent pbkdf2Vector.replace(remove, ""), + KeyStorePass password).isErr checkVariant "d4e5" # salt checkVariant "18b1" # checksum @@ -122,4 +137,5 @@ suiteReport "Keystore": var badKdf = parseJson(pbkdf2Vector) badKdf{"crypto", "kdf", "function"} = %"invalid" - check decryptKeystore($badKdf, password).iserr + check decryptKeystore(KeyStoreContent $badKdf, + KeyStorePass password).iserr From a8113cf2bcb72a37c6b5ff4d5c0b2f2711779741 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 2 Jun 2020 22:59:51 +0300 Subject: [PATCH 10/70] Restore the local sim to a working state --- beacon_chain/beacon_node.nim | 14 ++++++++++--- beacon_chain/conf.nim | 17 +++++---------- beacon_chain/spec/keystore.nim | 28 ++++++++++++++++++------- beacon_chain/validator_keygen.nim | 2 +- docker/manage_testnet_hosts.nims | 23 ++++++++++---------- scripts/attach_validators.sh | 24 --------------------- scripts/connect_to_testnet.nims | 2 +- scripts/launch_local_testnet.sh | 10 +++++---- scripts/reset_testnet.sh | 1 - tests/simulation/run_node.sh | 35 +++++++++++++++++++------------ tests/simulation/start.sh | 17 ++++++++++----- 11 files changed, 89 insertions(+), 84 deletions(-) delete mode 100755 scripts/attach_validators.sh diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 327776f40..db8e3871b 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -997,9 +997,16 @@ programMain: case config.cmd of createTestnet: var deposits: seq[Deposit] - for i in config.firstValidator.int ..< config.totalValidators.int: - let depositFile = config.testnetDepositsDir / - validatorFileBaseName(i) & ".deposit.json" + var i = -1 + for kind, dir in walkDir(config.testnetDepositsDir.string): + if kind != pcDir: + continue + + inc i + if i < config.firstValidator.int: + continue + + let depositFile = dir / "deposit.json" try: deposits.add Json.loadFile(depositFile, Deposit) except SerializationError as err: @@ -1097,6 +1104,7 @@ programMain: of makeDeposits: createDir(config.outValidatorsDir) + createDir(config.outSecretsDir) let deposits = generateDeposits( diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index a8f26860b..377b2c9ba 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -1,7 +1,7 @@ {.push raises: [Defect].} import - os, options, strformat, + os, options, chronicles, confutils, json_serialization, confutils/defs, confutils/std/net, chronicles/options as chroniclesOptions, @@ -106,11 +106,11 @@ type name: "validator" }: seq[ValidatorKeyPath] validatorsDir* {. - desc: "A directory containing validator keystores" + desc: "A directory containing validator keystores." name: "validators-dir" }: Option[InputDir] secretsDir* {. - desc: "A directory containing validator keystore passwords" + desc: "A directory containing validator keystore passwords." name: "secrets-dir" }: Option[InputDir] stateSnapshot* {. @@ -304,11 +304,11 @@ type name: "validator" }: seq[ValidatorKeyPath] validatorsDir* {. - desc: "A directory containing validator keystores" + desc: "A directory containing validator keystores." name: "validators-dir" }: Option[InputDir] secretsDir* {. - desc: "A directory containing validator keystore passwords" + desc: "A directory containing validator keystore passwords." name: "secrets-dir" }: Option[InputDir] proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string = @@ -321,13 +321,6 @@ proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string = getHomeDir() / dataDir / "BeaconNode" -proc validatorFileBaseName*(validatorIdx: int): string = - # there can apparently be tops 4M validators so we use 7 digits.. - try: - fmt"v{validatorIdx:07}" - except ValueError as e: - raiseAssert e.msg - func dumpDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "dump" diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 4c6991b27..ad5fefdfc 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -7,8 +7,9 @@ import json, math, strutils, strformat, - eth/keyfile/uuid, stew/[results, byteutils, bitseqs, bitops2], - nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], blscurve, + stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros, + eth/keyfile/uuid, blscurve, + nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], datatypes, crypto, digest, helpers export @@ -102,7 +103,16 @@ const # https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md wordListLen = 2048 - englishWords = split slurp("english_word_list.txt") + +macro wordListArray(filename: static string): array[wordListLen, cstring] = + result = newTree(nnkBracket) + var words = slurp(filename).split() + words.setLen wordListLen + for word in words: + result.add newCall("cstring", newLit(word)) + +const + englishWords = wordListArray "english_word_list.txt" iterator pathNodesImpl(path: string): Natural {.raises: [ValueError].} = @@ -142,7 +152,7 @@ func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed = let salt = "mnemonic-" & password.string KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64) -proc generateMnemonic*(words: openarray[string], +proc generateMnemonic*(words: openarray[cstring], entropyParam: openarray[byte] = @[]): Mnemonic = # https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic doAssert words.len == wordListLen @@ -163,7 +173,9 @@ proc generateMnemonic*(words: openarray[string], entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits)) - var res = words[entropy.getBitsBE(0..10)] + var res = "" + res.add words[entropy.getBitsBE(0..10)] + for i in 1 ..< mnemonicWordCount: let firstBit = i*11 @@ -276,7 +288,7 @@ proc encryptKeystore*(T: type[KdfParams], path = KeyPath "", salt: openarray[byte] = @[], iv: openarray[byte] = @[], - ugly = true): KeyStoreContent = + pretty = true): KeyStoreContent = var secret = privKey.toRaw[^32..^1] decKey: seq[byte] @@ -334,8 +346,8 @@ proc encryptKeystore*(T: type[KdfParams], uuid: $uuid, version: 4) - KeyStoreContent if ugly: $(%keystore) - else: pretty(%keystore, indent=4) + KeyStoreContent if pretty: json.pretty(%keystore, indent=4) + else: $(%keystore) proc restoreCredentials*(mnemonic: Mnemonic, password = KeyStorePass ""): Credentials = diff --git a/beacon_chain/validator_keygen.nim b/beacon_chain/validator_keygen.nim index e1650e231..1da8e1553 100644 --- a/beacon_chain/validator_keygen.nim +++ b/beacon_chain/validator_keygen.nim @@ -52,7 +52,7 @@ proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf, return var remainingAttempts = 3 - var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"" + var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"\n" while remainingAttempts > 0: let passphrase = KeyStorePass: try: readPasswordFromStdin(prompt) diff --git a/docker/manage_testnet_hosts.nims b/docker/manage_testnet_hosts.nims index 1f91cd43a..a14c632d1 100644 --- a/docker/manage_testnet_hosts.nims +++ b/docker/manage_testnet_hosts.nims @@ -1,5 +1,5 @@ import - strformat, os, confutils + strformat, os, confutils, algorightm type Command = enum @@ -22,9 +22,6 @@ type defaultValue: "data" name: "network-data-dir" }: string - totalValidators {. - name: "total-validators" }: int - totalUserValidators {. defaultValue: 0 name: "user-validators" }: int @@ -39,6 +36,9 @@ var conf = load CliConfig var serverCount = 10 instancesCount = 2 + validators = listDirs(conf.depositsDir) + +sort(validators) proc findOrDefault[K, V](tupleList: openarray[(K, V)], key: K, default: V): V = for t in tupleList: @@ -60,7 +60,7 @@ iterator nodes: Node = iterator validatorAssignments: tuple[node: Node; firstValidator, lastValidator: int] = let - systemValidators = conf.totalValidators - conf.totalUserValidators + systemValidators = validators.len - conf.totalUserValidators defaultValidatorAssignment = proc (nodeIdx: int): int = (systemValidators div serverCount) div instancesCount @@ -110,26 +110,25 @@ of restart_nodes: of reset_network: for n, firstValidator, lastValidator in validatorAssignments(): var - keysList = "" + validatorDirs = "" networkDataFiles = conf.networkDataDir & "/{genesis.ssz,bootstrap_nodes.txt}" for i in firstValidator ..< lastValidator: - let validatorKey = fmt"v{i:07}.privkey" - keysList.add " " - keysList.add conf.depositsDir / validatorKey + validatorDirs.add " " + validatorDirs.add conf.depositsDir / validators[i] let dockerPath = &"/docker/{n.container}/data/BeaconNode" echo &"echo Syncing {lastValidator - firstValidator} keys starting from {firstValidator} to container {n.container}@{n.server} ... && \\" echo &" ssh {n.server} 'sudo rm -rf /tmp/nimbus && mkdir -p /tmp/nimbus/' && \\" echo &" rsync -a -zz {networkDataFiles} {n.server}:/tmp/nimbus/net-data/ && \\" - if keysList.len > 0: - echo &" rsync -a -zz {keysList} {n.server}:/tmp/nimbus/keys/ && \\" + if validatorDirs.len > 0: + echo &" rsync -a -zz {validatorDirs} {n.server}:/tmp/nimbus/keys/ && \\" echo &" ssh {n.server} 'sudo docker container stop {n.container}; " & &"sudo mkdir -p {dockerPath}/validators && " & &"sudo rm -rf {dockerPath}/validators/* && " & &"sudo rm -rf {dockerPath}/db && " & - (if keysList.len > 0: &"sudo mv /tmp/nimbus/keys/* {dockerPath}/validators/ && " else: "") & + (if validatorDirs.len > 0: &"sudo mv /tmp/nimbus/keys/* {dockerPath}/validators/ && " else: "") & &"sudo mv /tmp/nimbus/net-data/* {dockerPath}/ && " & &"sudo chown dockremap:docker -R {dockerPath}'" diff --git a/scripts/attach_validators.sh b/scripts/attach_validators.sh deleted file mode 100755 index 2c2f933e4..000000000 --- a/scripts/attach_validators.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -eu - -NETWORK_NAME=$1 -NODE_ID=$2 -FIRST_VALIDATOR=$3 -LAST_VALIDATOR=$4 - -cd $(dirname "$0") -cd .. - -if [ -f .env ]; then - source .env -fi - -NETWORK_DIR=$WWW_DIR/$NETWORK_NAME - -for i in $(seq $FIRST_VALIDATOR $LAST_VALIDATOR); do - VALIDATOR=v$(printf '%07d' $i) - beacon_chain/beacon_node --data-dir="$DATA_DIR/node-$NODE_ID" importValidator \ - --keyfile="$NETWORK_DIR/$VALIDATOR.privkey" -done - diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index 9c992a813..23785b890 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -137,7 +137,7 @@ cli do (skipGoerliKey {. mkDir validatorsDir mode = Verbose exec replace(&"""{beaconNodeBinary} makeDeposits - --random-deposits=1 + --count=1 --out-validators-dir="{validatorsDir}" --out-secrets-dir="{secretsDir}" --deposit-private-key={privKey} diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index d20a43fd0..6816b4d46 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -141,7 +141,7 @@ $MAKE LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${ ./build/beacon_node makeDeposits \ --count=${TOTAL_VALIDATORS} \ --out-validators-dir="${DEPOSITS_DIR}" \ - --out-secrets-dir="${SECRETS_DIR}" + --out-secrets-dir="${SECRETS_DIR}" BOOTSTRAP_IP="127.0.0.1" ./build/beacon_node createTestnet \ @@ -206,10 +206,12 @@ for NUM_NODE in $(seq 0 $(( ${NUM_NODES} - 1 ))); do # The first $NODES_WITH_VALIDATORS nodes split them equally between them, after skipping the first $USER_VALIDATORS. NODE_DATA_DIR="${DATA_DIR}/node${NUM_NODE}" mkdir -p "${NODE_DATA_DIR}/validators" + mkdir -p "${NODE_DATA_DIR}/secrets" + if [[ $NUM_NODE -lt $NODES_WITH_VALIDATORS ]]; then - # TODO: There are no longer privkey files - for KEYFILE in $(ls ${DEPOSITS_DIR}/*.privkey | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do - cp -a "$KEYFILE" "${NODE_DATA_DIR}/validators/" + for VALIDATOR in $(ls ${DEPOSITS_DIR}/* | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do + cp -ar "$VALIDATOR" "${NODE_DATA_DIR}/validators/" + cp -a "${SECRETS_DIR}/${VALIDATOR}" "${NODE_DATA_DIR}/secrets" done fi diff --git a/scripts/reset_testnet.sh b/scripts/reset_testnet.sh index f9a8bb4dd..d5c5eb3a1 100755 --- a/scripts/reset_testnet.sh +++ b/scripts/reset_testnet.sh @@ -117,7 +117,6 @@ if [[ $PUBLISH_TESTNET_RESETS != "0" ]]; then --deposits-dir="$DEPOSITS_DIR_ABS" \ --network-data-dir="$NETWORK_DIR_ABS" \ --user-validators=$USER_VALIDATORS \ - --total-validators=$TOTAL_VALIDATORS \ > /tmp/reset-network.sh bash /tmp/reset-network.sh diff --git a/tests/simulation/run_node.sh b/tests/simulation/run_node.sh index 98482e62a..42ef0f06e 100755 --- a/tests/simulation/run_node.sh +++ b/tests/simulation/run_node.sh @@ -31,7 +31,10 @@ source "${SIM_ROOT}/../../env.sh" cd "$GIT_ROOT" -DATA_DIR="${SIMULATION_DIR}/node-$NODE_ID" +NODE_DATA_DIR="${SIMULATION_DIR}/node-$NODE_ID" +NODE_VALIDATORS_DIR=$NODE_DATA_DIR/validators/ +NODE_SECRETS_DIR=$NODE_DATA_DIR/secrets/ + PORT=$(( BASE_P2P_PORT + NODE_ID )) NAT_ARG="--nat:extip:127.0.0.1" @@ -39,41 +42,47 @@ if [ "${NAT:-}" == "1" ]; then NAT_ARG="--nat:any" fi -mkdir -p "$DATA_DIR/validators" -rm -f $DATA_DIR/validators/* +rm -rf "$NODE_VALIDATORS_DIR" +mkdir -p "$NODE_VALIDATORS_DIR" + +rm -rf "$NODE_SECRETS_DIR" +mkdir -p "$NODE_SECRETS_DIR" + +VALIDATORS_PER_NODE=$((NUM_VALIDATORS / TOTAL_NODES)) if [[ $NODE_ID -lt $TOTAL_NODES ]]; then - VALIDATORS_PER_NODE=$((NUM_VALIDATORS / TOTAL_NODES)) - VALIDATORS_PER_NODE_HALF=$((VALIDATORS_PER_NODE / 2)) - FIRST_VALIDATOR_IDX=$(( VALIDATORS_PER_NODE * NODE_ID )) # if using validator client binaries in addition to beacon nodes # we will split the keys for this instance in half between the BN and the VC if [ "${SPLIT_VALIDATORS_BETWEEN_BN_AND_VC:-}" == "yes" ]; then - LAST_VALIDATOR_IDX=$(( FIRST_VALIDATOR_IDX + VALIDATORS_PER_NODE_HALF - 1 )) + ATTACHED_VALIDATORS=$((VALIDATORS_PER_NODE / 2)) else - LAST_VALIDATOR_IDX=$(( FIRST_VALIDATOR_IDX + VALIDATORS_PER_NODE - 1 )) + ATTACHED_VALIDATORS=$VALIDATORS_PER_NODE fi pushd "$VALIDATORS_DIR" >/dev/null - cp $(seq -s " " -f v%07g.privkey $FIRST_VALIDATOR_IDX $LAST_VALIDATOR_IDX) "$DATA_DIR/validators" + for VALIDATOR in $(ls | tail -n +$(( ($VALIDATORS_PER_NODE * $NODE_ID) + 1 )) | head -n $ATTACHED_VALIDATORS); do + cp -ar "$VALIDATOR" "$NODE_VALIDATORS_DIR" + cp -a "$SECRETS_DIR/$VALIDATOR" "$NODE_SECRETS_DIR" + done popd >/dev/null fi -rm -rf "$DATA_DIR/dump" -mkdir -p "$DATA_DIR/dump" +rm -rf "$NODE_DATA_DIR/dump" +mkdir -p "$NODE_DATA_DIR/dump" SNAPSHOT_ARG="" if [ -f "${SNAPSHOT_FILE}" ]; then SNAPSHOT_ARG="--state-snapshot=${SNAPSHOT_FILE}" fi -cd "$DATA_DIR" +cd "$NODE_DATA_DIR" # if you want tracing messages, add "--log-level=TRACE" below $BEACON_NODE_BIN \ --log-level=${LOG_LEVEL:-DEBUG} \ --bootstrap-file=$BOOTSTRAP_ADDRESS_FILE \ - --data-dir=$DATA_DIR \ + --data-dir=$NODE_DATA_DIR \ + --secrets-dir=$NODE_SECRETS_DIR \ --node-name=$NODE_ID \ --tcp-port=$PORT \ --udp-port=$PORT \ diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index 9613a7ccf..a6dece080 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -21,9 +21,6 @@ DEFS+="-d:MAX_COMMITTEES_PER_SLOT=${MAX_COMMITTEES_PER_SLOT:-1} " # Spec de DEFS+="-d:SLOTS_PER_EPOCH=${SLOTS_PER_EPOCH:-6} " # Spec default: 32 DEFS+="-d:SECONDS_PER_SLOT=${SECONDS_PER_SLOT:-6} " # Spec default: 12 -LAST_VALIDATOR_NUM=$(( NUM_VALIDATORS - 1 )) -LAST_VALIDATOR="$VALIDATORS_DIR/v$(printf '%07d' $LAST_VALIDATOR_NUM).deposit.json" - # Windows detection if uname | grep -qiE "mingw|msys"; then MAKE="mingw32-make" @@ -96,10 +93,20 @@ if [[ "$USE_TMUX" != "no" ]]; then $TMUX select-window -t "${TMUX_SESSION_NAME}:sim" fi -$MAKE -j3 --no-print-directory NIMFLAGS="$CUSTOM_NIMFLAGS $DEFS" LOG_LEVEL="${LOG_LEVEL:-DEBUG}" beacon_node validator_client deposit_contract +$MAKE -j3 --no-print-directory NIMFLAGS="$CUSTOM_NIMFLAGS $DEFS" LOG_LEVEL="${LOG_LEVEL:-DEBUG}" beacon_node validator_client + +count_files () { + { ls -1q $1 2> /dev/null || true ; } | wc -l +} + +EXISTING_VALIDATORS=$(count_files "$VALIDATORS_DIR/*/deposit.json") + +if [[ $EXISTING_VALIDATORS -lt $NUM_VALIDATORS ]]; then + rm -rf "$VALIDATORS_DIR" + rm -rf "$SECRETS_DIR" -if [ ! -f "${LAST_VALIDATOR}" ]; then if [ "$WEB3_ARG" != "" ]; then + make deposit_contract echo Deploying the validator deposit contract... DEPOSIT_CONTRACT_ADDRESS=$($DEPLOY_DEPOSIT_CONTRACT_BIN deploy $WEB3_ARG) echo Contract deployed at $DEPOSIT_CONTRACT_ADDRESS From e3378e52acd003d72f0e3458e87c49988a1fef84 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 3 Jun 2020 12:07:43 +0300 Subject: [PATCH 11/70] Distribute the keystore secret files to the testnet hosts --- docker/manage_testnet_hosts.nims | 19 ++++++++++++------- scripts/launch_local_testnet.sh | 5 +++-- scripts/reset_testnet.sh | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docker/manage_testnet_hosts.nims b/docker/manage_testnet_hosts.nims index a14c632d1..b6cd4f621 100644 --- a/docker/manage_testnet_hosts.nims +++ b/docker/manage_testnet_hosts.nims @@ -18,6 +18,10 @@ type defaultValue: "deposits" name: "deposits-dir" }: string + secretsDir {. + defaultValue: "secrets" + name: "secrets-dir" }: string + networkDataDir {. defaultValue: "data" name: "network-data-dir" }: string @@ -116,19 +120,20 @@ of reset_network: for i in firstValidator ..< lastValidator: validatorDirs.add " " validatorDirs.add conf.depositsDir / validators[i] + secretFiles.add " " + secretFiles.add conf.secretsDir / validators[i] let dockerPath = &"/docker/{n.container}/data/BeaconNode" echo &"echo Syncing {lastValidator - firstValidator} keys starting from {firstValidator} to container {n.container}@{n.server} ... && \\" - echo &" ssh {n.server} 'sudo rm -rf /tmp/nimbus && mkdir -p /tmp/nimbus/' && \\" + echo &" ssh {n.server} 'sudo rm -rf /tmp/nimbus && mkdir -p /tmp/nimbus/{{validators,secrets}}' && \\" echo &" rsync -a -zz {networkDataFiles} {n.server}:/tmp/nimbus/net-data/ && \\" - if validatorDirs.len > 0: - echo &" rsync -a -zz {validatorDirs} {n.server}:/tmp/nimbus/keys/ && \\" + if validator.len > 0: + echo &" rsync -a -zz {validatorDirs} {n.server}:/tmp/nimbus/validators/ && \\" + echo &" rsync -a -zz {secretFiles} {n.server}:/tmp/nimbus/secrets/ && \\" echo &" ssh {n.server} 'sudo docker container stop {n.container}; " & - &"sudo mkdir -p {dockerPath}/validators && " & - &"sudo rm -rf {dockerPath}/validators/* && " & - &"sudo rm -rf {dockerPath}/db && " & - (if validatorDirs.len > 0: &"sudo mv /tmp/nimbus/keys/* {dockerPath}/validators/ && " else: "") & + &"sudo rm -rf {dockerPath}/{{db,validators,secrets}}* && " & + (if validators.len > 0: &"sudo mv /tmp/nimbus/* {dockerPath}/ && " else: "") & &"sudo mv /tmp/nimbus/net-data/* {dockerPath}/ && " & &"sudo chown dockremap:docker -R {dockerPath}'" diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index 6816b4d46..afea81220 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -181,10 +181,11 @@ dump_logs() { PIDS="" NODES_WITH_VALIDATORS=${NODES_WITH_VALIDATORS:-4} -VALIDATORS_PER_NODE=$(( $RANDOM_VALIDATORS / $NODES_WITH_VALIDATORS )) +SYSTEM_VALIDATORS=$((TOTAL_VALIDATORS - USER_VALIDATORS)) +VALIDATORS_PER_NODE=$((SYSTEM_VALIDATORS / NODES_WITH_VALIDATORS)) BOOTSTRAP_TIMEOUT=10 # in seconds -for NUM_NODE in $(seq 0 $(( ${NUM_NODES} - 1 ))); do +for NUM_NODE in $(seq 0 $((NUM_NODES - 1))); do if [[ ${NUM_NODE} == 0 ]]; then BOOTSTRAP_ARG="" else diff --git a/scripts/reset_testnet.sh b/scripts/reset_testnet.sh index d5c5eb3a1..a46ab1ebc 100755 --- a/scripts/reset_testnet.sh +++ b/scripts/reset_testnet.sh @@ -115,6 +115,7 @@ if [[ $PUBLISH_TESTNET_RESETS != "0" ]]; then ../env.sh nim --verbosity:0 --hints:off manage_testnet_hosts.nims reset_network \ --network=$NETWORK \ --deposits-dir="$DEPOSITS_DIR_ABS" \ + --secrets-dir="$SECRETS_DIR_ABS" \ --network-data-dir="$NETWORK_DIR_ABS" \ --user-validators=$USER_VALIDATORS \ > /tmp/reset-network.sh From a75c632f7a243a77552dab3f9e1548178fc22567 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 3 Jun 2020 14:40:14 +0300 Subject: [PATCH 12/70] Fixed launch_local_testnet; Renamed validator_keygen to keystore_directories --- beacon_chain/beacon_node.nim | 2 +- ...{validator_keygen.nim => keystore_directories.nim} | 0 beacon_chain/validator_client.nim | 2 +- beacon_chain/validator_duties.nim | 2 +- scripts/launch_local_testnet.sh | 11 ++++++----- 5 files changed, 9 insertions(+), 8 deletions(-) rename beacon_chain/{validator_keygen.nim => keystore_directories.nim} (100%) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index db8e3871b..72f609970 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -25,7 +25,7 @@ import beacon_node_common, beacon_node_types, block_pools/block_pools_types, nimbus_binary_common, mainchain_monitor, version, ssz/[merkleization], sszdump, - sync_protocol, request_manager, validator_keygen, interop, statusbar, + sync_protocol, request_manager, keystore_directories, interop, statusbar, sync_manager, state_transition, validator_duties, validator_api, attestation_aggregation diff --git a/beacon_chain/validator_keygen.nim b/beacon_chain/keystore_directories.nim similarity index 100% rename from beacon_chain/validator_keygen.nim rename to beacon_chain/keystore_directories.nim diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index 84a8e35d4..d30698e62 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -21,7 +21,7 @@ import eth2_network, eth2_discovery, validator_pool, beacon_node_types, nimbus_binary_common, version, ssz/merkleization, - sync_manager, validator_keygen, + sync_manager, keystore_directories, spec/eth2_apis/validator_callsigs_types, eth2_json_rpc_serialization diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 7fabf5d21..382105b03 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -20,7 +20,7 @@ import # Local modules spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network], conf, time, validator_pool, state_transition, - attestation_pool, block_pool, eth2_network, validator_keygen, + attestation_pool, block_pool, eth2_network, keystore_directories, beacon_node_common, beacon_node_types, nimbus_binary_common, mainchain_monitor, version, ssz/merkleization, interop, attestation_aggregation, sync_manager, sszdump diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index afea81220..8e052c8e6 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -181,8 +181,8 @@ dump_logs() { PIDS="" NODES_WITH_VALIDATORS=${NODES_WITH_VALIDATORS:-4} -SYSTEM_VALIDATORS=$((TOTAL_VALIDATORS - USER_VALIDATORS)) -VALIDATORS_PER_NODE=$((SYSTEM_VALIDATORS / NODES_WITH_VALIDATORS)) +SYSTEM_VALIDATORS=$(( TOTAL_VALIDATORS - USER_VALIDATORS )) +VALIDATORS_PER_NODE=$(( SYSTEM_VALIDATORS / NODES_WITH_VALIDATORS )) BOOTSTRAP_TIMEOUT=10 # in seconds for NUM_NODE in $(seq 0 $((NUM_NODES - 1))); do @@ -210,14 +210,15 @@ for NUM_NODE in $(seq 0 $((NUM_NODES - 1))); do mkdir -p "${NODE_DATA_DIR}/secrets" if [[ $NUM_NODE -lt $NODES_WITH_VALIDATORS ]]; then - for VALIDATOR in $(ls ${DEPOSITS_DIR}/* | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do - cp -ar "$VALIDATOR" "${NODE_DATA_DIR}/validators/" + for VALIDATOR in $(ls ${DEPOSITS_DIR} | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do + cp -ar "${DEPOSITS_DIR}/$VALIDATOR" "${NODE_DATA_DIR}/validators/" cp -a "${SECRETS_DIR}/${VALIDATOR}" "${NODE_DATA_DIR}/secrets" done fi ./build/beacon_node \ - --nat:extip:127.0.0.1 \ + --non-interactive \ + --nat:extip:127.0.0.1 \ --log-level="${LOG_LEVEL}" \ --tcp-port=$(( BASE_PORT + NUM_NODE )) \ --udp-port=$(( BASE_PORT + NUM_NODE )) \ From 2acda1c11532074576e73c2a1500db01667b2377 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 3 Jun 2020 14:52:36 +0300 Subject: [PATCH 13/70] Provide a default value for secretsDir (similar to validatorsDir) --- beacon_chain/conf.nim | 11 ++++++---- beacon_chain/keystore_directories.nim | 31 +++++++++++++-------------- beacon_chain/validator_duties.nim | 2 +- scripts/launch_local_testnet.sh | 2 +- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 377b2c9ba..c9c5e3950 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -105,11 +105,11 @@ type abbr: "v" name: "validator" }: seq[ValidatorKeyPath] - validatorsDir* {. + validatorsDirFlag* {. desc: "A directory containing validator keystores." name: "validators-dir" }: Option[InputDir] - secretsDir* {. + secretsDirFlag* {. desc: "A directory containing validator keystore passwords." name: "secrets-dir" }: Option[InputDir] @@ -324,8 +324,11 @@ proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string = func dumpDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "dump" -func localValidatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = - string conf.validatorsDir.get(InputDir(conf.dataDir / "validators")) +func validatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = + string conf.validatorsDirFlag.get(InputDir(conf.dataDir / "validators")) + +func secretsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = + string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets")) func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "db" diff --git a/beacon_chain/keystore_directories.nim b/beacon_chain/keystore_directories.nim index 1da8e1553..d00e250ef 100644 --- a/beacon_chain/keystore_directories.nim +++ b/beacon_chain/keystore_directories.nim @@ -29,22 +29,21 @@ proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf, error "Failed to read keystore", err = err.msg, path = keystorePath return - if conf.secretsDir.isSome: - let passphrasePath = conf.secretsDir.get / keyName - if fileExists(passphrasePath): - let - passphrase = KeyStorePass: - try: readFile(passphrasePath) - except IOError as err: - error "Failed to read passphrase file", err = err.msg, path = passphrasePath - return + let passphrasePath = conf.secretsDir / keyName + if fileExists(passphrasePath): + let + passphrase = KeyStorePass: + try: readFile(passphrasePath) + except IOError as err: + error "Failed to read passphrase file", err = err.msg, path = passphrasePath + return - let res = decryptKeystore(keystoreContents, passphrase) - if res.isOk: - return res.get.some - else: - error "Failed to decrypt keystore", keystorePath, passphrasePath - return + let res = decryptKeystore(keystoreContents, passphrase) + if res.isOk: + return res.get.some + else: + error "Failed to decrypt keystore", keystorePath, passphrasePath + return if conf.nonInteractive: error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir", @@ -76,7 +75,7 @@ iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPriv file = validatorKeyFile.string, err = err.msg quit 1 - let validatorsDir = conf.localValidatorsDir + let validatorsDir = conf.validatorsDir try: for kind, file in walkDir(validatorsDir): if kind == pcDir: diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 382105b03..f7807f824 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -34,7 +34,7 @@ declareCounter beacon_blocks_proposed, logScope: topics = "beacval" proc saveValidatorKey*(keyName, key: string, conf: BeaconNodeConf) = - let validatorsDir = conf.localValidatorsDir + let validatorsDir = conf.validatorsDir let outputFile = validatorsDir / keyName createDir validatorsDir writeFile(outputFile, key) diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index 8e052c8e6..d2f90983a 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -212,7 +212,7 @@ for NUM_NODE in $(seq 0 $((NUM_NODES - 1))); do if [[ $NUM_NODE -lt $NODES_WITH_VALIDATORS ]]; then for VALIDATOR in $(ls ${DEPOSITS_DIR} | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do cp -ar "${DEPOSITS_DIR}/$VALIDATOR" "${NODE_DATA_DIR}/validators/" - cp -a "${SECRETS_DIR}/${VALIDATOR}" "${NODE_DATA_DIR}/secrets" + cp -a "${SECRETS_DIR}/${VALIDATOR}" "${NODE_DATA_DIR}/secrets/" done fi From 37b473954798831183b0ce63c523a017b48b85bf Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 3 Jun 2020 14:59:50 +0300 Subject: [PATCH 14/70] Include hash_tree_root in the SSZ fuzzing tests --- tests/fuzzing/ssz_fuzzing.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/fuzzing/ssz_fuzzing.nim b/tests/fuzzing/ssz_fuzzing.nim index a20c36bfb..80b49ab7d 100644 --- a/tests/fuzzing/ssz_fuzzing.nim +++ b/tests/fuzzing/ssz_fuzzing.nim @@ -19,6 +19,9 @@ template sszFuzzingTest*(T: type) = let reEncoded = SSZ.encode(decoded) + when T isnot SignedBeaconBlock: + let hash = hash_tree_root(decoded) + if payload != reEncoded: when hasSerializationTracing: # Run deserialization again to produce a seriazation trace @@ -30,6 +33,9 @@ template sszFuzzingTest*(T: type) = echo "Re-encoided payload with len = ", reEncoded.len echo reEncoded + when T isnot SignedBeaconBlock: + echo "HTR: ", hash + echo repr(decoded) doAssert false From 6e88a07b510596455454c58f8c5baf9e04d3fba7 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 3 Jun 2020 16:16:39 +0300 Subject: [PATCH 15/70] Fix the validator client build --- beacon_chain/conf.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index c9c5e3950..503e71c8e 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -303,11 +303,11 @@ type abbr: "v" name: "validator" }: seq[ValidatorKeyPath] - validatorsDir* {. + validatorsDirFlag* {. desc: "A directory containing validator keystores." name: "validators-dir" }: Option[InputDir] - secretsDir* {. + secretsDirFlag* {. desc: "A directory containing validator keystore passwords." name: "secrets-dir" }: Option[InputDir] From 76b44c73f3b86ff617f3e8df0786caf22e167c5d Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Thu, 4 Jun 2020 16:36:44 +0300 Subject: [PATCH 16/70] Make it easier to turn off the use of libstacktrace --- config.nims | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/config.nims b/config.nims index a9f5ac544..f17c89a99 100644 --- a/config.nims +++ b/config.nims @@ -43,15 +43,22 @@ else: switch("import", "testutils/moduletests") +const useLibStackTrace = not defined(macosx) and + not (defined(windows) and defined(i386)) and + not defined(disable_libbacktrace) + +when useLibStackTrace: + --define:nimStackTraceOverride + switch("import", "libbacktrace") +else: + --stacktrace:on + --linetrace:on + # the default open files limit is too low on macOS (512), breaking the # "--debugger:native" build. It can be increased with `ulimit -n 1024`. if not defined(macosx): # add debugging symbols and original files and line numbers --debugger:native - if not (defined(windows) and defined(i386)) and not defined(disable_libbacktrace): - # light-weight stack traces using libbacktrace and libunwind - --define:nimStackTraceOverride - switch("import", "libbacktrace") --define:nimOldCaseObjects # https://github.com/status-im/nim-confutils/issues/9 From fdaf419e4143b66417411f5ab8913a217ccca9a7 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Mon, 8 Jun 2020 17:56:56 +0300 Subject: [PATCH 17/70] Address review comments --- beacon_chain/beacon_node.nim | 2 +- ...directories.nim => keystore_management.nim} | 0 beacon_chain/merkle_minimal.nim | 14 ++++++++++---- beacon_chain/spec/crypto.nim | 3 --- beacon_chain/spec/keystore.nim | 18 +++++++++++++----- beacon_chain/validator_client.nim | 2 +- beacon_chain/validator_duties.nim | 2 +- docker/manage_testnet_hosts.nims | 2 +- tests/test_keystore.nim | 5 ++++- 9 files changed, 31 insertions(+), 17 deletions(-) rename beacon_chain/{keystore_directories.nim => keystore_management.nim} (100%) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 72f609970..45d095de5 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -25,7 +25,7 @@ import beacon_node_common, beacon_node_types, block_pools/block_pools_types, nimbus_binary_common, mainchain_monitor, version, ssz/[merkleization], sszdump, - sync_protocol, request_manager, keystore_directories, interop, statusbar, + sync_protocol, request_manager, keystore_management, interop, statusbar, sync_manager, state_transition, validator_duties, validator_api, attestation_aggregation diff --git a/beacon_chain/keystore_directories.nim b/beacon_chain/keystore_management.nim similarity index 100% rename from beacon_chain/keystore_directories.nim rename to beacon_chain/keystore_management.nim diff --git a/beacon_chain/merkle_minimal.nim b/beacon_chain/merkle_minimal.nim index 281bb86a8..a7d00f95d 100644 --- a/beacon_chain/merkle_minimal.nim +++ b/beacon_chain/merkle_minimal.nim @@ -18,6 +18,14 @@ import ../../beacon_chain/spec/[beaconstate, datatypes, digest, helpers], ../../beacon_chain/ssz/merkleization +# TODO +# +# This module currently represents a direct translation of the Python +# code, appearing in the spec. We need to review it to ensure that it +# doesn't duplicate any code defined in ssz.nim already. +# +# All tests need to be moved to the test suite. + func round_step_down*(x: Natural, step: static Natural): int {.inline.} = ## Round the input to the previous multiple of "step" when (step and (step - 1)) == 0: @@ -60,10 +68,8 @@ proc merkleTreeFromLeaves*( h.update zeroHashes[depth-1] result.nnznodes[depth].add nodeHash -proc getMerkleProof*[Depth: static int]( - tree: SparseMerkleTree[Depth], - index: int, - ): array[Depth, Eth2Digest] = +proc getMerkleProof*[Depth: static int](tree: SparseMerkleTree[Depth], + index: int): array[Depth, Eth2Digest] = # Descend down the tree according to the bit representation # of the index: diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index 666cd6ba1..e3a3f83fe 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -84,9 +84,6 @@ template `==`*[N, T](a: BlsValue[N, T], b: T): bool = template `==`*[N, T](a: T, b: BlsValue[N, T]): bool = a == b.blsValue -template `==`*(a, b: ValidatorPrivKey): bool = - blscurve.SecretKey(a) == blscurve.SecretKey(b) - # API # ---------------------------------------------------------------------- # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index ad5fefdfc..76f1c8bba 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -187,13 +187,21 @@ proc generateMnemonic*(words: openarray[cstring], proc deriveChildKey*(parentKey: ValidatorPrivKey, index: Natural): ValidatorPrivKey = - doAssert derive_child_secretKey(SecretKey result, - SecretKey parentKey, - uint32 index) + let success = derive_child_secretKey(SecretKey result, + SecretKey parentKey, + uint32 index) + # TODO `derive_child_secretKey` is reporting pre-condition + # failures with return values. We should turn the checks + # into asserts inside the function. + doAssert success proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey = - doAssert derive_master_secretKey(SecretKey result, - seq[byte] seed) + let success = derive_master_secretKey(SecretKey result, + seq[byte] seed) + # TODO `derive_master_secretKey` is reporting pre-condition + # failures with return values. We should turn the checks + # into asserts inside the function. + doAssert success proc deriveMasterKey*(mnemonic: Mnemonic, password: KeyStorePass): ValidatorPrivKey = diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index d30698e62..bca8e1722 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -21,7 +21,7 @@ import eth2_network, eth2_discovery, validator_pool, beacon_node_types, nimbus_binary_common, version, ssz/merkleization, - sync_manager, keystore_directories, + sync_manager, keystore_management, spec/eth2_apis/validator_callsigs_types, eth2_json_rpc_serialization diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index f7807f824..9adbf2198 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -20,7 +20,7 @@ import # Local modules spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network], conf, time, validator_pool, state_transition, - attestation_pool, block_pool, eth2_network, keystore_directories, + attestation_pool, block_pool, eth2_network, keystore_management, beacon_node_common, beacon_node_types, nimbus_binary_common, mainchain_monitor, version, ssz/merkleization, interop, attestation_aggregation, sync_manager, sszdump diff --git a/docker/manage_testnet_hosts.nims b/docker/manage_testnet_hosts.nims index b6cd4f621..6c4741a25 100644 --- a/docker/manage_testnet_hosts.nims +++ b/docker/manage_testnet_hosts.nims @@ -1,5 +1,5 @@ import - strformat, os, confutils, algorightm + strformat, os, confutils, algorithm type Command = enum diff --git a/tests/test_keystore.nim b/tests/test_keystore.nim index 4013de891..0839fec03 100644 --- a/tests/test_keystore.nim +++ b/tests/test_keystore.nim @@ -9,11 +9,14 @@ import unittest, ./testutil, json, - stew/byteutils, + stew/byteutils, blscurve, ../beacon_chain/spec/[crypto, keystore] from strutils import replace +template `==`*(a, b: ValidatorPrivKey): bool = + blscurve.SecretKey(a) == blscurve.SecretKey(b) + const scryptVector = """{ "crypto": { From 811ba9aacd22437b506d2dafd88bf0fb8256e9d6 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 9 Jun 2020 14:44:40 +0300 Subject: [PATCH 18/70] Fix SSZ issues on 32-bit platforms --- beacon_chain/ssz/bytes_reader.nim | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/beacon_chain/ssz/bytes_reader.nim b/beacon_chain/ssz/bytes_reader.nim index 874b0523f..3e031fe5c 100644 --- a/beacon_chain/ssz/bytes_reader.nim +++ b/beacon_chain/ssz/bytes_reader.nim @@ -85,14 +85,14 @@ template checkForForbiddenBits(ResulType: type, func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = mixin fromSszBytes, toSszType - template readOffsetUnchecked(n: int): int {.used.}= - int fromSszBytes(uint32, input.toOpenArray(n, n + offsetSize - 1)) + template readOffsetUnchecked(n: int): uint32 {.used.}= + fromSszBytes(uint32, input.toOpenArray(n, n + offsetSize - 1)) template readOffset(n: int): int {.used.} = let offset = readOffsetUnchecked(n) - if offset > input.len: + if offset > input.len.uint32: raise newException(MalformedSszError, "SSZ list element offset points past the end of the input") - offset + int(offset) #when result is List: # result.setOutputSize input.len @@ -141,6 +141,7 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = elif val is List|array: type E = type val[0] + when E is byte: val.setOutputSize input.len if input.len > 0: @@ -171,8 +172,8 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = raise newException(MalformedSszError, "SSZ input of insufficient size") var offset = readOffset 0 - trs "GOT OFFSET ", offset + let resultLen = offset div offsetSize trs "LEN ", resultLen @@ -206,8 +207,10 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = copyMem(addr val.bytes[0], unsafeAddr input[0], input.len) elif val is object|tuple: - const minimallyExpectedSize = fixedPortionSize(T) - if input.len < minimallyExpectedSize: + let inputLen = uint32 input.len + const minimallyExpectedSize = uint32 fixedPortionSize(T) + + if inputLen < minimallyExpectedSize: raise newException(MalformedSszError, "SSZ input of insufficient size") enumInstanceSerializedFields(val, fieldName, field): @@ -231,7 +234,7 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = else: let startOffset = readOffsetUnchecked(boundingOffsets[0]) - endOffset = if boundingOffsets[1] == -1: input.len + endOffset = if boundingOffsets[1] == -1: inputLen else: readOffsetUnchecked(boundingOffsets[1]) when boundingOffsets.isFirstOffset: @@ -241,7 +244,7 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = trs "VAR FIELD ", startOffset, "-", endOffset if startOffset > endOffset: raise newException(MalformedSszError, "SSZ field offsets are not monotonically increasing") - elif endOffset > input.len: + elif endOffset > inputLen: raise newException(MalformedSszError, "SSZ field offset points past the end of the input") elif startOffset < minimallyExpectedSize: raise newException(MalformedSszError, "SSZ field offset points outside bounding offsets") @@ -253,14 +256,14 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = # TODO passing in `FieldType` instead of `type(field)` triggers a # bug in the compiler readSszValue( - input.toOpenArray(startOffset, endOffset - 1), + input.toOpenArray(int(startOffset), int(endOffset - 1)), field) trs "READING COMPLETE ", fieldName else: trs "READING FOREIGN ", fieldName, ": ", name(SszType) field = fromSszBytes( type(field), - input.toOpenArray(startOffset, endOffset - 1)) + input.toOpenArray(int(startOffset), int(endOffset - 1))) else: unsupported T From 25821331c4a06f5360e3c9be458c04c2db3cd3b0 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 9 Jun 2020 14:49:58 +0300 Subject: [PATCH 19/70] More greppable code for the onPeerConnected operation --- beacon_chain/eth2_network.nim | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index fe7d59d0a..cef29e9ee 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -124,8 +124,8 @@ type # Private fields: peerStateInitializer*: PeerStateInitializer networkStateInitializer*: NetworkStateInitializer - handshake*: HandshakeStep - disconnectHandler*: DisconnectionHandler + onPeerConnected*: OnPeerConnectedHandler + onPeerDisconnected*: OnPeerDisconnectedHandler ProtocolInfo* = ptr ProtocolInfoObj @@ -136,8 +136,8 @@ type PeerStateInitializer* = proc(peer: Peer): RootRef {.gcsafe.} NetworkStateInitializer* = proc(network: EthereumNode): RootRef {.gcsafe.} - HandshakeStep* = proc(peer: Peer, conn: Connection): Future[void] {.gcsafe.} - DisconnectionHandler* = proc(peer: Peer): Future[void] {.gcsafe.} + OnPeerConnectedHandler* = proc(peer: Peer, conn: Connection): Future[void] {.gcsafe.} + OnPeerDisconnectedHandler* = proc(peer: Peer): Future[void] {.gcsafe.} ThunkProc* = LPProtoHandler MounterProc* = proc(network: Eth2Node) {.gcsafe.} MessageContentPrinter* = proc(msg: pointer): string {.gcsafe.} @@ -504,14 +504,11 @@ template send*[M](r: SingleChunkResponse[M], val: auto): untyped = proc performProtocolHandshakes*(peer: Peer) {.async.} = var subProtocolsHandshakes = newSeqOfCap[Future[void]](allProtocols.len) for protocol in allProtocols: - if protocol.handshake != nil: - subProtocolsHandshakes.add((protocol.handshake)(peer, nil)) + if protocol.onPeerConnected != nil: + subProtocolsHandshakes.add protocol.onPeerConnected(peer, nil) await allFuturesThrowing(subProtocolsHandshakes) -template initializeConnection*(peer: Peer): auto = - performProtocolHandshakes(peer) - proc initProtocol(name: string, peerInit: PeerStateInitializer, networkInit: NetworkStateInitializer): ProtocolInfoObj = @@ -528,10 +525,10 @@ proc registerProtocol(protocol: ProtocolInfo) = gProtocols[i].index = i proc setEventHandlers(p: ProtocolInfo, - handshake: HandshakeStep, - disconnectHandler: DisconnectionHandler) = - p.handshake = handshake - p.disconnectHandler = disconnectHandler + onPeerConnected: OnPeerConnectedHandler, + onPeerDisconnected: OnPeerDisconnectedHandler) = + p.onPeerConnected = onPeerConnected + p.onPeerDisconnected = onPeerDisconnected proc implementSendProcBody(sendProc: SendProc) = let @@ -726,7 +723,7 @@ proc dialPeer*(node: Eth2Node, peerInfo: PeerInfo) {.async.} = #debug "Supported protocols", ls debug "Initializing connection" - await initializeConnection(peer) + await performProtocolHandshakes(peer) inc libp2p_successful_dials debug "Network handshakes completed" From 8da81210fad629d4baa496990149dac1b1461f6f Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 9 Jun 2020 16:49:46 +0300 Subject: [PATCH 20/70] Work-around https://github.com/nim-lang/Nim/issues/14616 --- research/state_sim.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/research/state_sim.nim b/research/state_sim.nim index 8617e7a98..12c0bf280 100644 --- a/research/state_sim.nim +++ b/research/state_sim.nim @@ -106,9 +106,12 @@ cli do(slots = SLOTS_PER_EPOCH * 6, # some variation let target_slot = state[].data.slot + MIN_ATTESTATION_INCLUSION_DELAY - 1 + commitee_count = get_committee_count_at_slot(state[].data, target_slot) + + let scass = withTimerRet(timers[tShuffle]): mapIt( - 0'u64 ..< get_committee_count_at_slot(state[].data, target_slot), + 0 ..< commitee_count.int, get_beacon_committee(state[].data, target_slot, it.CommitteeIndex, cache)) for i, scas in scass: From bea243ae04f9688f26036590a1c2fba27ca04b48 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 10 Jun 2020 10:30:48 +0300 Subject: [PATCH 21/70] Attempt to fix the CI finalization test Since I'm not able to reproduce the finalization failure locally and it does happen only sporadically, one possible explanation is that the introduction of keystores lead to a slower initialization of the beacon nodes which somehow interferes with their behavior during the initial slots. If increasing the start-up delay fixes the problems, the hypothesis will be confirmed. --- scripts/launch_local_testnet.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index d2f90983a..8ef5a1908 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -153,7 +153,7 @@ BOOTSTRAP_IP="127.0.0.1" --output-bootstrap-file="${NETWORK_DIR}/bootstrap_nodes.txt" \ --bootstrap-address=${BOOTSTRAP_IP} \ --bootstrap-port=${BASE_PORT} \ - --genesis-offset=30 # Delay in seconds + --genesis-offset=60 # Delay in seconds ./scripts/make_prometheus_config.sh \ --nodes ${NUM_NODES} \ From ff49932bb9a56672a710bf1068301ddbc99db766 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 10 Jun 2020 18:44:11 +0300 Subject: [PATCH 22/70] Reduce the number of validators to their older values (before this branch) --- scripts/testnet0.env | 4 ++-- scripts/testnet1.env | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/testnet0.env b/scripts/testnet0.env index bac0383ab..2adf10389 100644 --- a/scripts/testnet0.env +++ b/scripts/testnet0.env @@ -1,5 +1,5 @@ CONST_PRESET=minimal -USER_VALIDATORS=10 -TOTAL_VALIDATORS=256 +USER_VALIDATORS=8 +TOTAL_VALIDATORS=128 BOOTSTRAP_PORT=9000 WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a diff --git a/scripts/testnet1.env b/scripts/testnet1.env index 104c83829..1ef9e9a6b 100644 --- a/scripts/testnet1.env +++ b/scripts/testnet1.env @@ -1,6 +1,6 @@ CONST_PRESET=mainnet -USER_VALIDATORS=10 -TOTAL_VALIDATORS=256 +USER_VALIDATORS=8 +TOTAL_VALIDATORS=128 BOOTSTRAP_PORT=9100 WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a From c773e10c1a085a2c60915ee4f8c05ec4489bbbbc Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Wed, 10 Jun 2020 22:36:54 +0300 Subject: [PATCH 23/70] Attempt to reduce the risk of dropped network connections during the loading of KeyStores --- beacon_chain/beacon_node.nim | 22 +++++++++++++--------- beacon_chain/eth2_network.nim | 10 ++++++---- beacon_chain/validator_duties.nim | 6 ------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 45d095de5..c55a75738 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -254,9 +254,6 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async proc connectToNetwork(node: BeaconNode) {.async.} = await node.network.connectToNetwork() - let addressFile = node.config.dataDir / "beacon_node.address" - writeFile(addressFile, node.network.announcedENR.toURI) - proc onAttestation(node: BeaconNode, attestation: Attestation) = # We received an attestation from the network but don't know much about it # yet - in particular, we haven't verified that it belongs to particular chain @@ -538,7 +535,7 @@ proc runForwardSyncLoop(node: BeaconNode) {.async.} = result = node.blockPool.head.blck.slot proc getLocalWallSlot(): Slot {.gcsafe.} = - let epoch = node.beaconClock.now().toSlot().slot.compute_epoch_at_slot() + + let epoch = node.beaconClock.now().slotOrZero.compute_epoch_at_slot() + 1'u64 result = epoch.compute_start_slot_at_epoch() @@ -815,8 +812,6 @@ proc start(node: BeaconNode) = # actually need to make this part of normal application flow - # losing all connections might happen at any time and we should be # prepared to handle it. - waitFor node.connectToNetwork() - let head = node.blockPool.head finalizedHead = node.blockPool.finalizedHead @@ -837,12 +832,21 @@ proc start(node: BeaconNode) = cat = "init", pcs = "start_beacon_node" - let - bs = BlockSlot(blck: head.blck, slot: head.blck.slot) + node.network.startListening() + let addressFile = node.config.dataDir / "beacon_node.address" + writeFile(addressFile, node.network.announcedENR.toURI) + + let bs = BlockSlot(blck: head.blck, slot: head.blck.slot) node.blockPool.withState(node.blockPool.tmpState, bs): - node.addLocalValidators(state) + for validatorKey in node.config.validatorKeys: + node.addLocalValidator state, validatorKey + # Allow some network events to be processed: + waitFor sleepAsync(1) + info "Local validators attached ", count = node.attachedValidators.count + + waitFor node.network.connectToNetwork() node.run() func formatGwei(amount: uint64): string = diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index cef29e9ee..3a0dfdf47 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -821,17 +821,19 @@ proc init*(T: type Eth2Node, conf: BeaconNodeConf, enrForkId: ENRForkID, if msg.protocolMounter != nil: msg.protocolMounter result - for i in 0 ..< ConcurrentConnections: - result.connWorkers.add(connectWorker(result)) - template publicKey*(node: Eth2Node): keys.PublicKey = node.discovery.privKey.toPublicKey.tryGet() template addKnownPeer*(node: Eth2Node, peer: enr.Record) = node.discovery.addNode peer -proc start*(node: Eth2Node) {.async.} = +proc startListening*(node: Eth2Node) = node.discovery.open() + +proc start*(node: Eth2Node) {.async.} = + for i in 0 ..< ConcurrentConnections: + node.connWorkers.add connectWorker(node) + node.discovery.start() node.libp2pTransportLoops = await node.switch.start() node.discoveryLoop = node.runDiscoveryLoop() diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 9adbf2198..f34674010 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -53,12 +53,6 @@ proc addLocalValidator*(node: BeaconNode, node.attachedValidators.addLocalValidator(pubKey, privKey) -proc addLocalValidators*(node: BeaconNode, state: BeaconState) = - for validatorKey in node.config.validatorKeys: - node.addLocalValidator state, validatorKey - - info "Local validators attached ", count = node.attachedValidators.count - func getAttachedValidator*(node: BeaconNode, state: BeaconState, idx: ValidatorIndex): AttachedValidator = From cf6a869e9e18a3f3748528841b68ddf046e77cfe Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Thu, 11 Jun 2020 15:13:12 +0300 Subject: [PATCH 24/70] Address some TODO items; Handle start-up before genesis more properly --- beacon_chain/beacon_node.nim | 46 ++++++++++++++----------------- beacon_chain/eth2_network.nim | 6 ++-- beacon_chain/validator_duties.nim | 21 +++++++++++--- tests/test_peer_connection.nim | 2 +- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index c55a75738..fda717d1a 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -31,6 +31,7 @@ import const genesisFile* = "genesis.ssz" + timeToInitNetworkingBeforeGenesis = chronos.seconds(10) hasPrompt = not defined(withoutPrompt) type @@ -145,7 +146,7 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async if genesisState.isNil: # Didn't work, try creating a genesis state using main chain monitor # TODO Could move this to a separate "GenesisMonitor" process or task - # that would do only this - see + # that would do only this - see Paul's proposal for this. if conf.web3Url.len > 0 and conf.depositContractAddress.len > 0: mainchainMonitor = MainchainMonitor.init( web3Provider(conf.web3Url), @@ -226,8 +227,10 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async topicAggregateAndProofs: topicAggregateAndProofs, ) - # TODO sync is called when a remote peer is connected - is that the right - # time to do so? + traceAsyncErrors res.addLocalValidators() + + # This merely configures the BeaconSync + # The traffic will be started when we join the network. network.initBeaconSync(blockPool, enrForkId.forkDigest, proc(signedBlock: SignedBeaconBlock) = if signedBlock.message.slot.isEpoch: @@ -251,9 +254,6 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async return res -proc connectToNetwork(node: BeaconNode) {.async.} = - await node.network.connectToNetwork() - proc onAttestation(node: BeaconNode, attestation: Attestation) = # We received an attestation from the network but don't know much about it # yet - in particular, we haven't verified that it belongs to particular chain @@ -807,15 +807,25 @@ proc createPidFile(filename: string) = gPidFile = filename addQuitProc proc {.noconv.} = removeFile gPidFile +proc initializeNetworking(node: BeaconNode) {.async.} = + node.network.startListening() + + let addressFile = node.config.dataDir / "beacon_node.address" + writeFile(addressFile, node.network.announcedENR.toURI) + + await node.network.startLookingForPeers() + proc start(node: BeaconNode) = - # TODO: while it's nice to cheat by waiting for connections here, we - # actually need to make this part of normal application flow - - # losing all connections might happen at any time and we should be - # prepared to handle it. let head = node.blockPool.head finalizedHead = node.blockPool.finalizedHead + let genesisTime = node.beaconClock.fromNow(toBeaconTime(Slot 0)) + + if genesisTime.inFuture and genesisTime.offset > timeToInitNetworkingBeforeGenesis: + info "Waiting for the genesis event", genesisIn = genesisTime.offset + waitFor sleepAsync(genesisTime.offset - timeToInitNetworkingBeforeGenesis) + info "Starting beacon node", version = fullVersionStr, timeSinceFinalization = @@ -832,21 +842,7 @@ proc start(node: BeaconNode) = cat = "init", pcs = "start_beacon_node" - node.network.startListening() - let addressFile = node.config.dataDir / "beacon_node.address" - writeFile(addressFile, node.network.announcedENR.toURI) - - let bs = BlockSlot(blck: head.blck, slot: head.blck.slot) - - node.blockPool.withState(node.blockPool.tmpState, bs): - for validatorKey in node.config.validatorKeys: - node.addLocalValidator state, validatorKey - # Allow some network events to be processed: - waitFor sleepAsync(1) - - info "Local validators attached ", count = node.attachedValidators.count - - waitFor node.network.connectToNetwork() + waitFor node.initializeNetworking() node.run() func formatGwei(amount: uint64): string = diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 3a0dfdf47..27f175e57 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -1115,7 +1115,7 @@ proc announcedENR*(node: Eth2Node): enr.Record = proc shortForm*(id: KeyPair): string = $PeerID.init(id.pubkey) -proc connectToNetwork*(node: Eth2Node) {.async.} = +proc startLookingForPeers*(node: Eth2Node) {.async.} = await node.start() proc checkIfConnectedToBootstrapNode {.async.} = @@ -1125,9 +1125,7 @@ proc connectToNetwork*(node: Eth2Node) {.async.} = bootstrapEnrs = node.discovery.bootstrapRecords quit 1 - # TODO: The initial sync forces this to time out. - # Revisit when the new Sync manager is integrated. - # traceAsyncErrors checkIfConnectedToBootstrapNode() + traceAsyncErrors checkIfConnectedToBootstrapNode() func peersCount*(node: Eth2Node): int = len(node.peerPool) diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index f34674010..39d490b6c 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -41,8 +41,8 @@ proc saveValidatorKey*(keyName, key: string, conf: BeaconNodeConf) = info "Imported validator key", file = outputFile proc addLocalValidator*(node: BeaconNode, - state: BeaconState, - privKey: ValidatorPrivKey) = + state: BeaconState, + privKey: ValidatorPrivKey) = let pubKey = privKey.toPubKey() let idx = state.validators.asSeq.findIt(it.pubKey == pubKey) @@ -53,9 +53,22 @@ proc addLocalValidator*(node: BeaconNode, node.attachedValidators.addLocalValidator(pubKey, privKey) +proc addLocalValidators*(node: BeaconNode) {.async.} = + let + head = node.blockPool.head + bs = BlockSlot(blck: head.blck, slot: head.blck.slot) + + node.blockPool.withState(node.blockPool.tmpState, bs): + for validatorKey in node.config.validatorKeys: + node.addLocalValidator state, validatorKey + # Allow some network events to be processed: + await sleepAsync(0.seconds) + + info "Local validators attached ", count = node.attachedValidators.count + func getAttachedValidator*(node: BeaconNode, - state: BeaconState, - idx: ValidatorIndex): AttachedValidator = + state: BeaconState, + idx: ValidatorIndex): AttachedValidator = let validatorKey = state.validators[idx].pubkey node.attachedValidators.getValidator(validatorKey) diff --git a/tests/test_peer_connection.nim b/tests/test_peer_connection.nim index ea35582e8..53ac74330 100644 --- a/tests/test_peer_connection.nim +++ b/tests/test_peer_connection.nim @@ -36,5 +36,5 @@ asyncTest "connect two nodes": c2.nat = "none" var n2 = await createEth2Node(c2, ENRForkID()) - await n2.connectToNetwork(@[n1PersistentAddress]) + await n2.startLookingForPeers(@[n1PersistentAddress]) From 93631690981d7e80f0a53a630d2cddd2a9ed9bed Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Thu, 11 Jun 2020 16:12:20 +0300 Subject: [PATCH 25/70] Cosmetic improvement for the statusbar --- beacon_chain/statusbar.nim | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beacon_chain/statusbar.nim b/beacon_chain/statusbar.nim index 213b15da4..d9a77c0ee 100644 --- a/beacon_chain/statusbar.nim +++ b/beacon_chain/statusbar.nim @@ -75,11 +75,14 @@ func width(cells: seq[StatusBarCell]): int = proc renderCells(cells: seq[StatusBarCell], sep: string) = for i, cell in cells: - if i > 0: stdout.write sep + stdout.setBackgroundColor backgroundColor + stdout.setForegroundColor foregroundColor stdout.setStyle {styleDim} + if i > 0: stdout.write sep stdout.write " ", cell.label, ": " stdout.setStyle {styleBright} stdout.write cell.content, " " + stdout.resetAttributes() proc render*(s: var StatusBarView) = doAssert s.consumedLines == 0 @@ -89,9 +92,8 @@ proc render*(s: var StatusBarView) = allCellsWidth = s.layout.cellsLeft.width + s.layout.cellsRight.width if allCellsWidth > 0: - stdout.setBackgroundColor backgroundColor - stdout.setForegroundColor foregroundColor renderCells(s.layout.cellsLeft, sepLeft) + stdout.setBackgroundColor backgroundColor if termWidth > allCellsWidth: stdout.write spaces(termWidth - allCellsWidth) s.consumedLines = 1 @@ -99,7 +101,6 @@ proc render*(s: var StatusBarView) = stdout.write spaces(max(0, termWidth - s.layout.cellsLeft.width)), "\p" s.consumedLines = 2 renderCells(s.layout.cellsRight, sepRight) - stdout.resetAttributes stdout.flushFile proc erase*(s: var StatusBarView) = From 5807e2a767feea2c4b8b7e30b646169a4d641c7c Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Thu, 11 Jun 2020 16:31:35 +0300 Subject: [PATCH 26/70] Handle the delayed creation of the bootstrap node address file --- scripts/launch_local_testnet.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index 8ef5a1908..d06b1a8c9 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -143,6 +143,8 @@ $MAKE LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${ --out-validators-dir="${DEPOSITS_DIR}" \ --out-secrets-dir="${SECRETS_DIR}" +GENESIS_OFFSET=30 + BOOTSTRAP_IP="127.0.0.1" ./build/beacon_node createTestnet \ --data-dir="${DATA_DIR}/node0" \ @@ -153,7 +155,7 @@ BOOTSTRAP_IP="127.0.0.1" --output-bootstrap-file="${NETWORK_DIR}/bootstrap_nodes.txt" \ --bootstrap-address=${BOOTSTRAP_IP} \ --bootstrap-port=${BASE_PORT} \ - --genesis-offset=60 # Delay in seconds + --genesis-offset=${GENESIS_OFFSET} # Delay in seconds ./scripts/make_prometheus_config.sh \ --nodes ${NUM_NODES} \ @@ -195,7 +197,7 @@ for NUM_NODE in $(seq 0 $((NUM_NODES - 1))); do while [ ! -f "${DATA_DIR}/node0/beacon_node.address" ]; do sleep 0.1 NOW_TIMESTAMP=$(date +%s) - if [[ "$(( NOW_TIMESTAMP - START_TIMESTAMP ))" -ge "$BOOTSTRAP_TIMEOUT" ]]; then + if [[ "$(( NOW_TIMESTAMP - START_TIMESTAMP - GENESIS_OFFSET ))" -ge "$BOOTSTRAP_TIMEOUT" ]]; then echo "Bootstrap node failed to start in ${BOOTSTRAP_TIMEOUT} seconds. Aborting." dump_logs exit 1 From 421378b92d0b760da0aef8931ba5fb3131a80221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Thu, 11 Jun 2020 17:37:27 +0200 Subject: [PATCH 27/70] connect_to_testnet.nims: mkDir dataDir --- scripts/connect_to_testnet.nims | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index 23785b890..ddc427349 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -120,6 +120,7 @@ cli do (skipGoerliKey {. discard cd rootDir + mkDir dataDir # macOS may not have gnu-getopts installed and in the PATH execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes 1 --base-metrics-port 8008 --config-file "{dataDir}/prometheus.yml"""" From f5ec1bc5bbbe7023fe60236e8a5824bea5b87297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Thu, 11 Jun 2020 18:42:58 +0200 Subject: [PATCH 28/70] support multiple local Witti nodes [skip ci] --- Makefile | 10 +++++----- README.md | 17 ++++++++++++++++- scripts/connect_to_testnet.nims | 16 ++++++++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 6eea6ad1b..f0d2d2d87 100644 --- a/Makefile +++ b/Makefile @@ -131,10 +131,10 @@ eth2_network_simulation: | build deps clean_eth2_network_simulation_files + GIT_ROOT="$$PWD" NIMFLAGS="$(NIMFLAGS)" LOG_LEVEL="$(LOG_LEVEL)" tests/simulation/start.sh clean-testnet0: - rm -rf build/data/testnet0 + rm -rf build/data/testnet0* clean-testnet1: - rm -rf build/data/testnet1 + rm -rf build/data/testnet1* # - we're getting the preset from a testnet-specific .env file # - try SCRIPT_PARAMS="--skipGoerliKey" @@ -143,7 +143,7 @@ testnet0 testnet1: | build deps NIM_PARAMS="$(subst ",\",$(NIM_PARAMS))" LOG_LEVEL="$(LOG_LEVEL)" $(ENV_SCRIPT) nim $(NIM_PARAMS) scripts/connect_to_testnet.nims $(SCRIPT_PARAMS) --const-preset=$$CONST_PRESET --dev-build $@ clean-schlesi: - rm -rf build/data/shared_schlesi + rm -rf build/data/shared_schlesi* schlesi: | build deps NIM_PARAMS="$(subst ",\",$(NIM_PARAMS))" LOG_LEVEL="$(LOG_LEVEL)" $(ENV_SCRIPT) nim $(NIM_PARAMS) scripts/connect_to_testnet.nims $(SCRIPT_PARAMS) shared/schlesi @@ -152,7 +152,7 @@ schlesi-dev: | build deps NIM_PARAMS="$(subst ",\",$(NIM_PARAMS))" LOG_LEVEL="DEBUG; TRACE:discv5,networking; REQUIRED:none; DISABLED:none" $(ENV_SCRIPT) nim $(NIM_PARAMS) scripts/connect_to_testnet.nims $(SCRIPT_PARAMS) shared/schlesi clean-witti: - rm -rf build/data/shared_witti + rm -rf build/data/shared_witti* witti: | build deps NIM_PARAMS="$(subst ",\",$(NIM_PARAMS))" LOG_LEVEL="$(LOG_LEVEL)" $(ENV_SCRIPT) nim $(NIM_PARAMS) scripts/connect_to_testnet.nims $(SCRIPT_PARAMS) shared/witti @@ -161,7 +161,7 @@ witti-dev: | build deps NIM_PARAMS="$(subst ",\",$(NIM_PARAMS))" LOG_LEVEL="DEBUG; TRACE:discv5,networking; REQUIRED:none; DISABLED:none" $(ENV_SCRIPT) nim $(NIM_PARAMS) scripts/connect_to_testnet.nims $(SCRIPT_PARAMS) shared/witti clean: | clean-common - rm -rf build/{$(TOOLS_CSV),all_tests,*_node,*ssz*,beacon_node_testnet*,block_sim,state_sim,transition*} + rm -rf build/{$(TOOLS_CSV),all_tests,*_node,*ssz*,beacon_node_*,block_sim,state_sim,transition*} ifneq ($(USE_LIBBACKTRACE), 0) + $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT) endif diff --git a/README.md b/README.md index 06483e11e..59c83f42a 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,20 @@ make witti # This will build Nimbus and all other dependencies # and connect you to Witti ``` +Sometimes, you may want to disable the interactive prompt asking you for a Goerli key in order to become a validator: + +```bash +make SCRIPT_PARAMS="--skipGoerliKey" witti # not a validator +``` + +You can also start multiple local nodes, in different terminal windows/tabs, by specifying their numeric IDs: + +```bash +make SCRIPT_PARAMS="--nodeID=0" witti # the default +make SCRIPT_PARAMS="--nodeID=1" witti +make SCRIPT_PARAMS="--nodeID=2" witti +``` + ### Getting metrics from a local testnet client ```bash @@ -123,7 +137,8 @@ make NIMFLAGS="-d:insecure" witti You can now see the raw metrics on http://127.0.0.1:8008/metrics but they're not very useful like this, so let's feed them to a Prometheus instance: ```bash -prometheus --config.file=build/data/shared_witti/prometheus.yml +prometheus --config.file=build/data/shared_witti_0/prometheus.yml +# when starting multiple nodes at the same time, just use the config file from the one with the highest ID ``` For some pretty pictures, get [Grafana](https://grafana.com/) up and running, then import the dashboard definition in "grafana/beacon\_nodes\_Grafana\_dashboard.json". diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index ddc427349..462ac6204 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -34,6 +34,15 @@ cli do (skipGoerliKey {. desc: "Enables more extensive logging and debugging support" name: "dev-build" .} = false, + nodeID {. + desc: "Node ID" .} = 0.int, + + basePort {. + desc: "Base TCP/UDP port (nodeID will be added to it)" .} = 9000.int, + + baseMetricsPort {. + desc: "Base metrics port (nodeID will be added to it)" .} = 8008.int, + testnetName {.argument .}: string): let nameParts = testnetName.split "/" @@ -84,7 +93,7 @@ cli do (skipGoerliKey {. let dataDirName = testnetName.replace("/", "_") .replace("(", "_") - .replace(")", "_") + .replace(")", "_") & "_" & $nodeID dataDir = buildDir / "data" / dataDirName validatorsDir = dataDir / "validators" secretsDir = dataDir / "secrets" @@ -123,7 +132,7 @@ cli do (skipGoerliKey {. mkDir dataDir # macOS may not have gnu-getopts installed and in the PATH - execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes 1 --base-metrics-port 8008 --config-file "{dataDir}/prometheus.yml"""" + execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes """ & $(1 + nodeID) & &""" --base-metrics-port {baseMetricsPort} --config-file "{dataDir}/prometheus.yml"""" exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{beaconNodeBinary}" beacon_chain/beacon_node.nim""" @@ -159,7 +168,10 @@ cli do (skipGoerliKey {. --data-dir="{dataDir}" --dump --web3-url={web3Url} + --tcp-port=""" & $(basePort + nodeID) & &""" + --udp-port=""" & $(basePort + nodeID) & &""" --metrics + --metrics-port=""" & $(baseMetricsPort + nodeID) & &""" {bootstrapFileOpt} {logLevelOpt} {depositContractOpt} From 68a8b7d969914f246f58173c5179524433ec5f38 Mon Sep 17 00:00:00 2001 From: Kim De Mey Date: Fri, 12 Jun 2020 16:14:18 +0200 Subject: [PATCH 29/70] Filter discovery nodes on forkId (#1162) --- beacon_chain/eth2_network.nim | 8 ++++++-- vendor/nim-eth | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 27f175e57..401691564 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -63,6 +63,7 @@ type connQueue: AsyncQueue[PeerInfo] seenTable: Table[PeerID, SeenItem] connWorkers: seq[Future[void]] + forkId: ENRForkID EthereumNode = Eth2Node # needed for the definitions in p2p_backends_helpers @@ -762,12 +763,14 @@ proc connectWorker(network: Eth2Node) {.async.} = proc runDiscoveryLoop*(node: Eth2Node) {.async.} = debug "Starting discovery loop" + let enrField = ("eth2", SSZ.encode(node.forkId)) while true: let currentPeerCount = node.peerPool.len if currentPeerCount < node.wantedPeers: try: let discoveredPeers = - node.discovery.randomNodes(node.wantedPeers - currentPeerCount) + node.discovery.randomNodes(node.wantedPeers - currentPeerCount, + enrField) for peer in discoveredPeers: try: let peerRecord = peer.record.toTypedRecord @@ -808,9 +811,10 @@ proc init*(T: type Eth2Node, conf: BeaconNodeConf, enrForkId: ENRForkID, result.seenTable = initTable[PeerID, SeenItem]() result.connQueue = newAsyncQueue[PeerInfo](ConcurrentConnections) result.metadata = getPersistentNetMetadata(conf) + result.forkId = enrForkId result.discovery = Eth2DiscoveryProtocol.new( conf, ip, tcpPort, udpPort, privKey.toRaw, - {"eth2": SSZ.encode(enrForkId), "attnets": SSZ.encode(result.metadata.attnets)}) + {"eth2": SSZ.encode(result.forkId), "attnets": SSZ.encode(result.metadata.attnets)}) newSeq result.protocolStates, allProtocols.len for proto in allProtocols: diff --git a/vendor/nim-eth b/vendor/nim-eth index be9a87848..225a9ad41 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit be9a87848e068d68aa8fa1a7bfa07d7c7271eba7 +Subproject commit 225a9ad41cc0f4cd6d42153e64b356bb03f26274 From 33d55fac08c25de3cee5a56fbaf830e10b5ca585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Fri, 12 Jun 2020 16:37:36 +0200 Subject: [PATCH 30/70] eth2_network_simulation: mkdir + cosmetic changes --- tests/simulation/.gitignore | 7 ++++--- tests/simulation/start.sh | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/simulation/.gitignore b/tests/simulation/.gitignore index a35155f0d..72cd4077b 100644 --- a/tests/simulation/.gitignore +++ b/tests/simulation/.gitignore @@ -1,4 +1,5 @@ -data/ -validators/ -prometheus/ +/data +/validators +/prometheus +/secrets diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index a6dece080..22807cc68 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -39,17 +39,26 @@ WAIT_GENESIS="${WAIT_GENESIS:-no}" # Using tmux or multitail is an opt-in USE_MULTITAIL="${USE_MULTITAIL:-no}" -type "$MULTITAIL" &>/dev/null || { echo "${MULTITAIL}" is missing; USE_MULTITAIL="no"; } +if [[ "$USE_MULTITAIL" != "no" ]]; then + type "$MULTITAIL" &>/dev/null || { echo "${MULTITAIL}" is missing; USE_MULTITAIL="no"; } +fi USE_TMUX="${USE_TMUX:-no}" -type "$TMUX" &>/dev/null || { echo "${TMUX}" is missing; USE_TMUX="no"; } +if [[ "$USE_TMUX" != "no" ]]; then + type "$TMUX" &>/dev/null || { echo "${TMUX}" is missing; USE_TMUX="no"; } +fi USE_GANACHE="${USE_GANACHE:-no}" -type "$GANACHE" &>/dev/null || { echo $GANACHE is missing; USE_GANACHE="no"; } +if [[ "$USE_GANACHE" != "no" ]]; then + type "$GANACHE" &>/dev/null || { echo $GANACHE is missing; USE_GANACHE="no"; } +fi -USE_PROMETHEUS="${LAUNCH_PROMETHEUS:-no}" -type "$PROMETHEUS" &>/dev/null || { echo $PROMETHEUS is missing; USE_PROMETHEUS="no"; } +USE_PROMETHEUS="${USE_PROMETHEUS:-no}" +if [[ "$USE_PROMETHEUS" != "no" ]]; then + type "$PROMETHEUS" &>/dev/null || { echo $PROMETHEUS is missing; USE_PROMETHEUS="no"; } +fi +mkdir -p "${METRICS_DIR}" ./scripts/make_prometheus_config.sh \ --nodes ${TOTAL_NODES} \ --base-metrics-port ${BASE_METRICS_PORT} \ From c8f24ae3b812766f85fdb05fc9c37f2980c60c5a Mon Sep 17 00:00:00 2001 From: tersec Date: Fri, 12 Jun 2020 16:03:46 +0000 Subject: [PATCH 31/70] Remove three skipMerkleValidation usages (#1164) * remove three skipMerkleValidation usages * remove a couple obsolete comments/TODOs --- beacon_chain/block_pools/candidate_chains.nim | 2 +- tests/mocking/mock_deposits.nim | 4 ++-- tests/test_interop.nim | 7 +++---- tests/testblockutil.nim | 3 +-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/beacon_chain/block_pools/candidate_chains.nim b/beacon_chain/block_pools/candidate_chains.nim index d859a804e..7b4eef987 100644 --- a/beacon_chain/block_pools/candidate_chains.nim +++ b/beacon_chain/block_pools/candidate_chains.nim @@ -647,7 +647,7 @@ proc updateStateData*(dag: CandidateChains, state: var StateData, bs: BlockSlot) let ok = dag.skipAndUpdateState( state, ancestors[i], - {skipBlsValidation, skipMerkleValidation, skipStateRootValidation}, + {skipBlsValidation, skipStateRootValidation}, false) doAssert ok, "Blocks in database should never fail to apply.." diff --git a/tests/mocking/mock_deposits.nim b/tests/mocking/mock_deposits.nim index 40f5952d0..5113aef4e 100644 --- a/tests/mocking/mock_deposits.nim +++ b/tests/mocking/mock_deposits.nim @@ -104,8 +104,8 @@ template mockGenesisDepositsImpl( ) = # Genesis deposits with varying amounts - # NOTE: this could also apply for skipMerkleValidation, but prefer to er on the - # side of caution and generate a valid Deposit (it can still be skipped later). + # NOTE: prefer to er on the side of caution and generate a valid Deposit + # (it can still be skipped later). if skipBlsValidation in flags: # 1st loop - build deposit data for valIdx in 0 ..< validatorCount.int: diff --git a/tests/test_interop.nim b/tests/test_interop.nim index 3c85de8d6..2a57e5da0 100644 --- a/tests/test_interop.nim +++ b/tests/test_interop.nim @@ -2,7 +2,7 @@ import unittest, stint, ./testutil, stew/byteutils, - ../beacon_chain/[extras, interop, ssz], + ../beacon_chain/[interop, merkle_minimal, ssz], ../beacon_chain/spec/[beaconstate, crypto, datatypes] # Interop test yaml, found here: @@ -146,13 +146,12 @@ suiteReport "Interop": for i in 0..<64: let privKey = makeInteropPrivKey(i) deposits.add makeDeposit(privKey.toPubKey(), privKey) + attachMerkleProofs(deposits) const genesis_time = 1570500000 var - # TODO this currently requires skipMerkleValidation to pass the test - # makeDeposit doesn't appear to produce a proof? initialState = initialize_beacon_state_from_eth1( - eth1BlockHash, genesis_time, deposits, {skipMerkleValidation}) + eth1BlockHash, genesis_time, deposits, {}) # https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start#create-genesis-state initialState.genesis_time = genesis_time diff --git a/tests/testblockutil.nim b/tests/testblockutil.nim index 66c3e6f5d..f9aeb6b7c 100644 --- a/tests/testblockutil.nim +++ b/tests/testblockutil.nim @@ -69,8 +69,7 @@ proc makeInitialDeposits*( # and ideally (but not yet) efficiently only once calculating a Merkle # tree utilizing as much of the shared substructure as feasible, means # attaching proofs all together, as a separate step. - if skipMerkleValidation notin flags: - attachMerkleProofs(result) + attachMerkleProofs(result) func signBlock*( fork: Fork, genesis_validators_root: Eth2Digest, blck: BeaconBlock, From 42832cefa83b2f40eb405f87f2dab5f4994e0d31 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Fri, 12 Jun 2020 18:43:20 +0200 Subject: [PATCH 32/70] Small fixes (#1165) * random fixes * create dump dir on startup * don't crash on failure to write dump * fix a few `uint64` instances being used when indexing arrays - this should be a compile error but isn't due to compiler bugs * fix standalone test_block_pool compilation * add signed block processing in ncli * reuse cache entry instead of allocating a new one * allow for small clock disparities when validating blocks --- beacon_chain/beacon_node.nim | 8 ++- beacon_chain/block_pools/candidate_chains.nim | 70 +++++++++++-------- beacon_chain/block_pools/clearance.nim | 4 +- beacon_chain/spec/beaconstate.nim | 2 +- beacon_chain/spec/state_transition_epoch.nim | 2 +- beacon_chain/sszdump.nim | 32 ++++++--- ncli/ncli_hash_tree_root.nim | 6 +- ncli/ncli_pretty.nim | 1 + tests/test_block_pool.nim | 4 ++ 9 files changed, 83 insertions(+), 46 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index fda717d1a..6321cf4bd 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -303,12 +303,13 @@ proc storeBlock( let blck = node.blockPool.add(blockRoot, signedBlock) if blck.isErr: if blck.error == Invalid and node.config.dumpEnabled: + dump(node.config.dumpDir / "invalid", signedBlock, blockRoot) + let parent = node.blockPool.getRef(signedBlock.message.parent_root) if parent != nil: - node.blockPool.withState( - node.blockPool.tmpState, parent.atSlot(signedBlock.message.slot - 1)): + let parentBs = parent.atSlot(signedBlock.message.slot - 1) + node.blockPool.withState(node.blockPool.tmpState, parentBs): dump(node.config.dumpDir / "invalid", hashedState, parent) - dump(node.config.dumpDir / "invalid", signedBlock, blockRoot) return err(blck.error) @@ -1082,6 +1083,7 @@ programMain: if config.dumpEnabled: createDir(config.dumpDir) createDir(config.dumpDir / "incoming") + createDir(config.dumpDir / "invalid") var node = waitFor BeaconNode.init(config) diff --git a/beacon_chain/block_pools/candidate_chains.nim b/beacon_chain/block_pools/candidate_chains.nim index 7b4eef987..2da450573 100644 --- a/beacon_chain/block_pools/candidate_chains.nim +++ b/beacon_chain/block_pools/candidate_chains.nim @@ -313,12 +313,50 @@ proc getState( func getStateCacheIndex(dag: CandidateChains, blockRoot: Eth2Digest, slot: Slot): int = for i, cachedState in dag.cachedStates: - let (cacheBlockRoot, cacheSlot, state) = cachedState + let (cacheBlockRoot, cacheSlot, _) = cachedState if cacheBlockRoot == blockRoot and cacheSlot == slot: return i -1 +func putStateCache( + dag: CandidateChains, state: HashedBeaconState, blck: BlockRef) = + # Need to be able to efficiently access states for both attestation + # aggregation and to process block proposals going back to the last + # finalized slot. Ideally to avoid potential combinatiorial forking + # storage and/or memory constraints could CoW, up to and including, + # in particular, hash_tree_root() which is expensive to do 30 times + # since the previous epoch, to efficiently state_transition back to + # desired slot. However, none of that's in place, so there are both + # expensive, repeated BeaconState copies as well as computationally + # time-consuming-near-end-of-epoch hash tree roots. The latter are, + # effectively, naïvely O(n^2) in slot number otherwise, so when the + # slots become in the mid-to-high-20s it's spending all its time in + # pointlessly repeated calculations of prefix-state-transitions. An + # intermediate time/memory workaround involves storing only mapping + # between BlockRefs, or BlockSlots, and the BeaconState tree roots, + # but that still involves tens of megabytes worth of copying, along + # with the concomitant memory allocator and GC load. Instead, use a + # more memory-intensive (but more conceptually straightforward, and + # faster) strategy to just store, for the most recent slots. + let stateCacheIndex = dag.getStateCacheIndex(blck.root, state.data.slot) + if stateCacheIndex == -1: + # Could use a deque or similar, but want simpler structure, and the data + # items are small and few. + const MAX_CACHE_SIZE = 32 + + let cacheLen = dag.cachedStates.len + doAssert cacheLen <= MAX_CACHE_SIZE + + let entry = + if dag.cachedStates.len == MAX_CACHE_SIZE: dag.cachedStates.pop().state + else: (ref HashedBeaconState)() + entry[] = state + + insert(dag.cachedStates, (blck.root, state.data.slot, entry)) + trace "CandidateChains.putState(): state cache updated", + cacheLen, root = shortLog(blck.root), slot = state.data.slot + proc putState*(dag: CandidateChains, state: HashedBeaconState, blck: BlockRef) = # TODO we save state at every epoch start but never remove them - we also # potentially save multiple states per slot if reorgs happen, meaning @@ -344,35 +382,7 @@ proc putState*(dag: CandidateChains, state: HashedBeaconState, blck: BlockRef) = if not rootWritten: dag.db.putStateRoot(blck.root, state.data.slot, state.root) - # Need to be able to efficiently access states for both attestation - # aggregation and to process block proposals going back to the last - # finalized slot. Ideally to avoid potential combinatiorial forking - # storage and/or memory constraints could CoW, up to and including, - # in particular, hash_tree_root() which is expensive to do 30 times - # since the previous epoch, to efficiently state_transition back to - # desired slot. However, none of that's in place, so there are both - # expensive, repeated BeaconState copies as well as computationally - # time-consuming-near-end-of-epoch hash tree roots. The latter are, - # effectively, naïvely O(n^2) in slot number otherwise, so when the - # slots become in the mid-to-high-20s it's spending all its time in - # pointlessly repeated calculations of prefix-state-transitions. An - # intermediate time/memory workaround involves storing only mapping - # between BlockRefs, or BlockSlots, and the BeaconState tree roots, - # but that still involves tens of megabytes worth of copying, along - # with the concomitant memory allocator and GC load. Instead, use a - # more memory-intensive (but more conceptually straightforward, and - # faster) strategy to just store, for the most recent slots. - let stateCacheIndex = dag.getStateCacheIndex(blck.root, state.data.slot) - if stateCacheIndex == -1: - # Could use a deque or similar, but want simpler structure, and the data - # items are small and few. - const MAX_CACHE_SIZE = 32 - insert(dag.cachedStates, (blck.root, state.data.slot, newClone(state))) - while dag.cachedStates.len > MAX_CACHE_SIZE: - discard dag.cachedStates.pop() - let cacheLen = dag.cachedStates.len - trace "CandidateChains.putState(): state cache updated", cacheLen - doAssert cacheLen > 0 and cacheLen <= MAX_CACHE_SIZE + putStateCache(dag, state, blck) func getRef*(dag: CandidateChains, root: Eth2Digest): BlockRef = ## Retrieve a resolved block reference, if available diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index 2e7e35645..79d8f083d 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -266,7 +266,9 @@ proc isValidBeaconBlock*( # The block is not from a future slot # TODO allow `MAXIMUM_GOSSIP_CLOCK_DISPARITY` leniency, especially towards # seemingly future slots. - if not (signed_beacon_block.message.slot <= current_slot): + # TODO using +1 here while this is being sorted - should queue these until + # they're within the DISPARITY limit + if not (signed_beacon_block.message.slot <= current_slot + 1): debug "isValidBeaconBlock: block is from a future slot", signed_beacon_block_message_slot = signed_beacon_block.message.slot, current_slot = current_slot diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index dfec56815..cbb722988 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -174,7 +174,7 @@ proc slash_validator*(state: var BeaconState, slashed_index: ValidatorIndex, validator.slashed = true validator.withdrawable_epoch = max(validator.withdrawable_epoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR) - state.slashings[epoch mod EPOCHS_PER_SLASHINGS_VECTOR] += + state.slashings[int(epoch mod EPOCHS_PER_SLASHINGS_VECTOR)] += validator.effective_balance decrease_balance(state, slashed_index, validator.effective_balance div MIN_SLASHING_PENALTY_QUOTIENT) diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index 6bc05e1e2..467872f85 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -566,7 +566,7 @@ func process_final_updates*(state: var BeaconState) {.nbench.}= MAX_EFFECTIVE_BALANCE) # Reset slashings - state.slashings[next_epoch mod EPOCHS_PER_SLASHINGS_VECTOR] = 0.Gwei + state.slashings[int(next_epoch mod EPOCHS_PER_SLASHINGS_VECTOR)] = 0.Gwei # Set randao mix state.randao_mixes[next_epoch mod EPOCHS_PER_HISTORICAL_VECTOR] = diff --git a/beacon_chain/sszdump.nim b/beacon_chain/sszdump.nim index dbd1a7151..1268fd591 100644 --- a/beacon_chain/sszdump.nim +++ b/beacon_chain/sszdump.nim @@ -1,24 +1,38 @@ +{.push raises: [Defect].} + import - os, strformat, + os, strformat, chronicles, ssz/ssz_serialization, beacon_node_types, ./spec/[crypto, datatypes, digest] +# Dump errors are generally not fatal where used currently - the code calling +# these functions, like most code, is not exception safe +template logErrors(body: untyped) = + try: + body + except CatchableError as err: + notice "Failed to write SSZ", dir, msg = err.msg + proc dump*(dir: string, v: AttestationData, validator: ValidatorPubKey) = - SSZ.saveFile(dir / &"att-{v.slot}-{v.index}-{shortLog(validator)}.ssz", v) + logErrors: + SSZ.saveFile(dir / &"att-{v.slot}-{v.index}-{shortLog(validator)}.ssz", v) proc dump*(dir: string, v: SignedBeaconBlock, root: Eth2Digest) = - SSZ.saveFile(dir / &"block-{v.message.slot}-{shortLog(root)}.ssz", v) + logErrors: + SSZ.saveFile(dir / &"block-{v.message.slot}-{shortLog(root)}.ssz", v) proc dump*(dir: string, v: SignedBeaconBlock, blck: BlockRef) = dump(dir, v, blck.root) proc dump*(dir: string, v: HashedBeaconState, blck: BlockRef) = - SSZ.saveFile( - dir / &"state-{v.data.slot}-{shortLog(blck.root)}-{shortLog(v.root)}.ssz", - v.data) + logErrors: + SSZ.saveFile( + dir / &"state-{v.data.slot}-{shortLog(blck.root)}-{shortLog(v.root)}.ssz", + v.data) proc dump*(dir: string, v: HashedBeaconState) = - SSZ.saveFile( - dir / &"state-{v.data.slot}-{shortLog(v.root)}.ssz", - v.data) + logErrors: + SSZ.saveFile( + dir / &"state-{v.data.slot}-{shortLog(v.root)}.ssz", + v.data) diff --git a/ncli/ncli_hash_tree_root.nim b/ncli/ncli_hash_tree_root.nim index 2a91a4074..24260880a 100644 --- a/ncli/ncli_hash_tree_root.nim +++ b/ncli/ncli_hash_tree_root.nim @@ -17,13 +17,17 @@ cli do(kind: string, file: string): echo "Unknown file type: ", ext quit 1 ) - echo hash_tree_root(v[]).data.toHex() + when t is SignedBeaconBlock: + echo hash_tree_root(v.message).data.toHex() + else: + echo hash_tree_root(v[]).data.toHex() let ext = splitFile(file).ext case kind of "attester_slashing": printit(AttesterSlashing) of "attestation": printit(Attestation) + of "signed_block": printit(SignedBeaconBlock) of "block": printit(BeaconBlock) of "block_body": printit(BeaconBlockBody) of "block_header": printit(BeaconBlockHeader) diff --git a/ncli/ncli_pretty.nim b/ncli/ncli_pretty.nim index 80462da11..b6dac732b 100644 --- a/ncli/ncli_pretty.nim +++ b/ncli/ncli_pretty.nim @@ -22,6 +22,7 @@ cli do(kind: string, file: string): case kind of "attester_slashing": printit(AttesterSlashing) of "attestation": printit(Attestation) + of "signed_block": printit(SignedBeaconBlock) of "block": printit(BeaconBlock) of "block_body": printit(BeaconBlockBody) of "block_header": printit(BeaconBlockHeader) diff --git a/tests/test_block_pool.nim b/tests/test_block_pool.nim index 18cd986b7..53f1509be 100644 --- a/tests/test_block_pool.nim +++ b/tests/test_block_pool.nim @@ -13,6 +13,9 @@ import ../beacon_chain/spec/[datatypes, digest, validator], ../beacon_chain/[beacon_node_types, block_pool, state_transition, ssz] +when isMainModule: + import chronicles # or some random compile error happens... + suiteReport "BlockRef and helpers" & preset(): timedTest "isAncestorOf sanity" & preset(): let @@ -367,3 +370,4 @@ when const_preset == "minimal": # These require some minutes in mainnet hash_tree_root(pool.headState.data.data) hash_tree_root(pool2.justifiedState.data.data) == hash_tree_root(pool.justifiedState.data.data) + From 78b767f645f5864b37bd1a49b041f1f8d68bc92f Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Fri, 12 Jun 2020 21:10:22 +0200 Subject: [PATCH 33/70] avoid genericAssign for beacon node types (#1166) * avoid genericAssign for beacon node types ok, I got fed up of this function messing up cpu measurements - it's so ridiculously slow, it's sad. before, while syncing: ``` 40,65% beacon_node_shared_witti_0 [.] genericAssignAux__U5DxFPRpHCCZDKWQzM9adaw 9,02% libc-2.31.so [.] __memmove_avx_unaligned_erms 7,07% beacon_node_shared_witti_0 [.] BIG_384_58_monty 5,19% beacon_node_shared_witti_0 [.] BIG_384_58_mul 2,72% beacon_node_shared_witti_0 [.] memcpy@plt 1,18% [kernel] [k] rb_next 1,17% beacon_node_shared_witti_0 [.] genericReset 1,06% [kernel] [k] map_private_extent_buffer ``` after: ``` 24,88% beacon_node_shared_witti_0 [.] BIG_384_58_monty 20,29% beacon_node_shared_witti_0 [.] BIG_384_58_mul 3,15% beacon_node_shared_witti_0 [.] BIG_384_58_norm 2,93% beacon_node_shared_witti_0 [.] BIG_384_58_add 2,55% beacon_node_shared_witti_0 [.] BIG_384_58_sqr 1,64% beacon_node_shared_witti_0 [.] BIG_384_58_mod 1,63% beacon_node_shared_witti_0 [.] sha256Transform__BJNBQtWr9bJwzqbyfKXd38Q 1,48% beacon_node_shared_witti_0 [.] FP_BLS381_add 1,39% beacon_node_shared_witti_0 [.] BIG_384_58_sub 1,33% beacon_node_shared_witti_0 [.] BIG_384_58_dnorm 1,14% beacon_node_shared_witti_0 [.] FP2_BLS381_mul 1,05% beacon_node_shared_witti_0 [.] BIG_384_58_cmove 1,05% beacon_node_shared_witti_0 [.] get_shuffled_seq__4uncAHNsSG3Pndo5H11U9aQ ``` * better field iteration --- beacon_chain/beacon_chain_db.nim | 2 +- beacon_chain/block_pools/candidate_chains.nim | 8 ++--- beacon_chain/block_pools/clearance.nim | 2 +- beacon_chain/spec/datatypes.nim | 32 ++++++++++++++++++- beacon_chain/validator_duties.nim | 2 +- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/beacon_chain/beacon_chain_db.nim b/beacon_chain/beacon_chain_db.nim index 0bac3b0d8..d00cb97ab 100644 --- a/beacon_chain/beacon_chain_db.nim +++ b/beacon_chain/beacon_chain_db.nim @@ -141,7 +141,7 @@ proc getState*( proc decode(data: openArray[byte]) = try: # TODO can't write to output directly.. - outputAddr[] = SSZ.decode(data, BeaconState) + assign(outputAddr[], SSZ.decode(data, BeaconState)) except SerializationError as e: # If the data can't be deserialized, it could be because it's from a # version of the software that uses a different SSZ encoding diff --git a/beacon_chain/block_pools/candidate_chains.nim b/beacon_chain/block_pools/candidate_chains.nim index 2da450573..bb22ef600 100644 --- a/beacon_chain/block_pools/candidate_chains.nim +++ b/beacon_chain/block_pools/candidate_chains.nim @@ -301,7 +301,7 @@ proc getState( # Nonetheless, this is an ugly workaround that needs to go away doAssert false, "Cannot alias headState" - outputAddr[] = dag.headState + assign(outputAddr[], dag.headState) if not db.getState(stateRoot, output.data.data, restore): return false @@ -351,7 +351,7 @@ func putStateCache( let entry = if dag.cachedStates.len == MAX_CACHE_SIZE: dag.cachedStates.pop().state else: (ref HashedBeaconState)() - entry[] = state + assign(entry[], state) insert(dag.cachedStates, (blck.root, state.data.slot, entry)) trace "CandidateChains.putState(): state cache updated", @@ -529,7 +529,7 @@ proc rewindState(dag: CandidateChains, state: var StateData, bs: BlockSlot): # used in the front-end. let idx = dag.getStateCacheIndex(parBs.blck.root, parBs.slot) if idx >= 0: - state.data = dag.cachedStates[idx].state[] + assign(state.data, dag.cachedStates[idx].state[]) let ancestor = ancestors.pop() state.blck = ancestor.refs @@ -605,7 +605,7 @@ proc getStateDataCached(dag: CandidateChains, state: var StateData, bs: BlockSlo let idx = dag.getStateCacheIndex(bs.blck.root, bs.slot) if idx >= 0: - state.data = dag.cachedStates[idx].state[] + assign(state.data, dag.cachedStates[idx].state[]) state.blck = bs.blck beacon_state_data_cache_hits.inc() return true diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index 79d8f083d..2951c7d16 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -183,7 +183,7 @@ proc add*( # `state_transition` that takes a `StateData` instead and updates # the block as well doAssert v.addr == addr poolPtr.tmpState.data - poolPtr.tmpState = poolPtr.headState + assign(poolPtr.tmpState, poolPtr.headState) var stateCache = getEpochCache(parent, dag.tmpState.data.data) if not state_transition( diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 53c331e6f..bcd2b0b27 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -22,7 +22,7 @@ {.push raises: [Defect].} import - macros, hashes, json, strutils, tables, + macros, hashes, json, strutils, tables, typetraits, stew/[byteutils], chronicles, json_serialization/types as jsonTypes, ../ssz/types as sszTypes, ./crypto, ./digest @@ -649,3 +649,33 @@ chronicles.formatIt Attestation: it.shortLog import json_serialization export json_serialization export writeValue, readValue + +static: + # Sanity checks - these types should be trivial enough to copy with memcpy + doAssert supportsCopyMem(Validator) + doAssert supportsCopyMem(Eth2Digest) + +func assign*[T](tgt: var T, src: T) = + # The default `genericAssignAux` that gets generated for assignments in nim + # is ridiculously slow. When syncing, the application was spending 50%+ CPU + # time in it - `assign`, in the same test, doesn't even show in the perf trace + + when supportsCopyMem(T): + copyMem(addr tgt, unsafeAddr src, sizeof(tgt)) + elif T is object|tuple: + for t, s in fields(tgt, src): + assign(t, s) + elif T is List|BitList: + assign(distinctBase tgt, distinctBase src) + elif T is seq: + tgt.setLen(src.len) + when supportsCopyMem(type(tgt[0])): + if tgt.len > 0: + copyMem(addr tgt[0], unsafeAddr src[0], sizeof(tgt[0]) * tgt.len) + else: + for i in 0.. Date: Fri, 12 Jun 2020 21:37:28 +0200 Subject: [PATCH 34/70] Update 36 spec refs from 0.11.x to 0.12.1 --- beacon_chain/merkle_minimal.nim | 2 +- beacon_chain/spec/beaconstate.nim | 6 +++--- beacon_chain/spec/crypto.nim | 2 +- beacon_chain/spec/datatypes.nim | 14 +++++++------- beacon_chain/spec/digest.nim | 2 +- beacon_chain/spec/helpers.nim | 10 +++++----- beacon_chain/spec/state_transition_block.nim | 6 +++--- beacon_chain/spec/state_transition_epoch.nim | 14 +++++++------- beacon_chain/spec/validator.nim | 4 ++-- beacon_chain/state_transition.nim | 10 +++++----- beacon_chain/validator_pool.nim | 2 +- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/beacon_chain/merkle_minimal.nim b/beacon_chain/merkle_minimal.nim index a7d00f95d..c330c9987 100644 --- a/beacon_chain/merkle_minimal.nim +++ b/beacon_chain/merkle_minimal.nim @@ -5,7 +5,7 @@ # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/tests/core/pyspec/eth2spec/utils/merkle_minimal.py +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/tests/core/pyspec/eth2spec/utils/merkle_minimal.py # Merkle tree helpers # --------------------------------------------------------------- diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index cbb722988..d4300503b 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -111,7 +111,7 @@ proc process_deposit*( true -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_activation_exit_epoch +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_activation_exit_epoch func compute_activation_exit_epoch(epoch: Epoch): Epoch = ## Return the epoch during which validator activations and exits initiated in ## ``epoch`` take effect. @@ -300,7 +300,7 @@ func get_initial_beacon_block*(state: BeaconState): SignedBeaconBlock = # parent_root, randao_reveal, eth1_data, signature, and body automatically # initialized to default values. -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_block_root_at_slot +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_block_root_at_slot func get_block_root_at_slot*(state: BeaconState, slot: Slot): Eth2Digest = # Return the block root at a recent ``slot``. @@ -341,7 +341,7 @@ func is_eligible_for_activation(state: BeaconState, validator: Validator): # Has not yet been activated validator.activation_epoch == FAR_FUTURE_EPOCH -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#registry-updates +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates proc process_registry_updates*(state: var BeaconState, cache: var StateCache) {.nbench.}= ## Process activation eligibility and ejections diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index e3a3f83fe..cb935519e 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -114,7 +114,7 @@ func aggregate*(x: var ValidatorSig, other: ValidatorSig) = ## This assumes that they are real signatures x.blsValue.aggregate(other.blsValue) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#bls-signatures func blsVerify*( pubkey: ValidatorPubKey, message: openArray[byte], signature: ValidatorSig): bool = diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index bcd2b0b27..150892367 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -143,12 +143,12 @@ type Gwei* = uint64 CommitteeIndex* = distinct uint64 - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#proposerslashing + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#proposerslashing ProposerSlashing* = object signed_header_1*: SignedBeaconBlockHeader signed_header_2*: SignedBeaconBlockHeader - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#attesterslashing + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#attesterslashing AttesterSlashing* = object attestation_1*: IndexedAttestation attestation_2*: IndexedAttestation @@ -181,7 +181,7 @@ type epoch*: Epoch root*: Eth2Digest - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#AttestationData + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#AttestationData AttestationData* = object slot*: Slot @@ -196,7 +196,7 @@ type source*: Checkpoint target*: Checkpoint - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#deposit + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#deposit Deposit* = object proof*: array[DEPOSIT_CONTRACT_TREE_DEPTH + 1, Eth2Digest] ##\ ## Merkle path to deposit root @@ -209,7 +209,7 @@ type withdrawal_credentials*: Eth2Digest amount*: Gwei - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#depositdata + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#depositdata DepositData* = object pubkey*: ValidatorPubKey withdrawal_credentials*: Eth2Digest @@ -367,13 +367,13 @@ type epoch*: Epoch ##\ ## Epoch of latest fork - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#eth1data + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1data Eth1Data* = object deposit_root*: Eth2Digest deposit_count*: uint64 block_hash*: Eth2Digest - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#signedvoluntaryexit + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#signedvoluntaryexit SignedVoluntaryExit* = object message*: VoluntaryExit signature*: ValidatorSig diff --git a/beacon_chain/spec/digest.nim b/beacon_chain/spec/digest.nim index e28dcaacd..efda558e5 100644 --- a/beacon_chain/spec/digest.nim +++ b/beacon_chain/spec/digest.nim @@ -7,7 +7,7 @@ # Serenity hash function / digest # -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#hash +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#hash # # In Phase 0 the beacon chain is deployed with SHA256 (SHA2-256). # Note that is is different from Keccak256 (often mistakenly called SHA3-256) diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 8ee8d09f7..5c4d7efad 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -22,7 +22,7 @@ type # (other candidate is nativesockets.Domain) Domain = datatypes.Domain -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#integer_squareroot +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#integer_squareroot func integer_squareroot*(n: SomeInteger): SomeInteger = # Return the largest integer ``x`` such that ``x**2 <= n``. doAssert n >= 0'u64 @@ -35,7 +35,7 @@ func integer_squareroot*(n: SomeInteger): SomeInteger = y = (x + n div x) div 2 x -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_epoch_at_slot +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_epoch_at_slot func compute_epoch_at_slot*(slot: Slot|uint64): Epoch = # Return the epoch number at ``slot``. (slot div SLOTS_PER_EPOCH).Epoch @@ -46,12 +46,12 @@ template epoch*(slot: Slot): Epoch = template isEpoch*(slot: Slot): bool = (slot mod SLOTS_PER_EPOCH) == 0 -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch func compute_start_slot_at_epoch*(epoch: Epoch): Slot = # Return the start slot of ``epoch``. (epoch * SLOTS_PER_EPOCH).Slot -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_active_validator +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_active_validator func is_active_validator*(validator: Validator, epoch: Epoch): bool = ### Check if ``validator`` is active validator.activation_epoch <= epoch and epoch < validator.exit_epoch @@ -152,7 +152,7 @@ func compute_fork_digest*(current_version: Version, compute_fork_data_root( current_version, genesis_validators_root).data.toOpenArray(0, 3) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_domain +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_domain func compute_domain*( domain_type: DomainType, fork_version: Version = Version(GENESIS_FORK_VERSION), diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 34a3e0ad5..98ba0ddcd 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -134,14 +134,14 @@ proc process_randao( true -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#eth1-data +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1-data func process_eth1_data(state: var BeaconState, body: BeaconBlockBody) {.nbench.}= state.eth1_data_votes.add body.eth1_data if state.eth1_data_votes.asSeq.count(body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD.int: state.eth1_data = body.eth1_data -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_slashable_validator +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_slashable_validator func is_slashable_validator(validator: Validator, epoch: Epoch): bool = # Check if ``validator`` is slashable. (not validator.slashed) and @@ -213,7 +213,7 @@ func is_slashable_attestation_data( (data_1.source.epoch < data_2.source.epoch and data_2.target.epoch < data_1.target.epoch) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#attester-slashings +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#attester-slashings proc process_attester_slashing*( state: var BeaconState, attester_slashing: AttesterSlashing, diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index 467872f85..0fc63f182 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -576,7 +576,7 @@ func process_final_updates*(state: var BeaconState) {.nbench.}= if next_epoch mod (SLOTS_PER_HISTORICAL_ROOT div SLOTS_PER_EPOCH).uint64 == 0: # Equivalent to hash_tree_root(foo: HistoricalBatch), but without using # significant additional stack or heap. - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#historicalbatch + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#historicalbatch # In response to https://github.com/status-im/nim-beacon-chain/issues/921 state.historical_roots.add hash_tree_root( [hash_tree_root(state.block_roots), hash_tree_root(state.state_roots)]) @@ -585,14 +585,14 @@ func process_final_updates*(state: var BeaconState) {.nbench.}= state.previous_epoch_attestations = state.current_epoch_attestations state.current_epoch_attestations = default(type state.current_epoch_attestations) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#epoch-processing +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#epoch-processing proc process_epoch*(state: var BeaconState, updateFlags: UpdateFlags, per_epoch_cache: var StateCache) {.nbench.} = let currentEpoch = get_current_epoch(state) trace "process_epoch", current_epoch = currentEpoch - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#justification-and-finalization + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#justification-and-finalization process_justification_and_finalization(state, per_epoch_cache, updateFlags) # state.slot hasn't been incremented yet. @@ -602,16 +602,16 @@ proc process_epoch*(state: var BeaconState, updateFlags: UpdateFlags, # the finalization rules triggered. doAssert state.finalized_checkpoint.epoch + 3 >= currentEpoch - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#rewards-and-penalties-1 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#rewards-and-penalties-1 process_rewards_and_penalties(state, per_epoch_cache) - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#registry-updates + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates process_registry_updates(state, per_epoch_cache) - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#slashings + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#slashings process_slashings(state, per_epoch_cache) - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#final-updates + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#final-updates process_final_updates(state) # Once per epoch metrics diff --git a/beacon_chain/spec/validator.nim b/beacon_chain/spec/validator.nim index 75bad1160..fbccceb54 100644 --- a/beacon_chain/spec/validator.nim +++ b/beacon_chain/spec/validator.nim @@ -90,7 +90,7 @@ func get_shuffled_active_validator_indices*(state: BeaconState, epoch: Epoch): active_validator_indices.len.uint64), active_validator_indices[it]) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_previous_epoch +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_previous_epoch func get_previous_epoch*(state: BeaconState): Epoch = # Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). let current_epoch = get_current_epoch(state) @@ -232,7 +232,7 @@ func get_beacon_proposer_index*(state: BeaconState, cache: var StateCache): Option[ValidatorIndex] = get_beacon_proposer_index(state, cache, state.slot) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/validator.md#validator-assignments +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#validator-assignments func get_committee_assignment*( state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex): Option[tuple[a: seq[ValidatorIndex], b: CommitteeIndex, c: Slot]] {.used.} = diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index 40e769b37..277c4465f 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -62,7 +62,7 @@ func get_epoch_validator_count(state: BeaconState): int64 {.nbench.} = validator.withdrawable_epoch > get_current_epoch(state): result += 1 -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#verify_block_signature +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function proc verify_block_signature*( state: BeaconState, signedBlock: SignedBeaconBlock): bool {.nbench.} = if signedBlock.message.proposer_index >= state.validators.len.uint64: @@ -85,7 +85,7 @@ proc verify_block_signature*( true -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function proc verifyStateRoot(state: BeaconState, blck: BeaconBlock): bool = # This is inlined in state_transition(...) in spec. let state_root = hash_tree_root(state) @@ -108,7 +108,7 @@ type # Hashed-state transition functions # --------------------------------------------------------------- -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function func process_slot*(state: var HashedBeaconState) {.nbench.} = # Cache state root let previous_slot_state_root = state.root @@ -123,7 +123,7 @@ func process_slot*(state: var HashedBeaconState) {.nbench.} = state.data.block_roots[state.data.slot mod SLOTS_PER_HISTORICAL_ROOT] = hash_tree_root(state.data.latest_block_header) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function proc advance_slot*(state: var HashedBeaconState, nextStateRoot: Opt[Eth2Digest], updateFlags: UpdateFlags, epochCache: var StateCache) {.nbench.} = @@ -145,7 +145,7 @@ proc advance_slot*(state: var HashedBeaconState, else: state.root = hash_tree_root(state.data) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function proc process_slots*(state: var HashedBeaconState, slot: Slot, updateFlags: UpdateFlags = {}): bool {.nbench.} = # TODO this function is not _really_ necessary: when replaying states, we diff --git a/beacon_chain/validator_pool.nim b/beacon_chain/validator_pool.nim index d34927358..a36f7b0a0 100644 --- a/beacon_chain/validator_pool.nim +++ b/beacon_chain/validator_pool.nim @@ -80,7 +80,7 @@ proc signAggregateAndProof*(v: AttachedValidator, error "Out of process signAggregateAndProof not implemented" quit 1 -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#randao-reveal +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal func genRandaoReveal*(k: ValidatorPrivKey, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot): ValidatorSig = get_epoch_signature(fork, genesis_validators_root, slot, k) From cd80fab5a979ff4f2f8596c65c8c79ab6a7a7346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Sat, 13 Jun 2020 02:01:19 +0200 Subject: [PATCH 35/70] add link to nimbus-build-system docs --- README.md | 7 +++++-- vendor/nimbus-build-system | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 59c83f42a..fb776e3a9 100644 --- a/README.md +++ b/README.md @@ -243,8 +243,11 @@ The [inspector tool](./beacon_chain/inspector.nim) can help monitor the libp2p n ## For developers -Latest updates happen in the `devel` branch which is merged into `master` every week on Tuesday before deploying a new testnets -The following sections explain how to setup your build environment on your platform. +Latest updates happen in the `devel` branch which is merged into `master` every week on Tuesday before deploying new testnets. + +Interesting Make variables and targets are documented in the [nimbus-build-system](https://github.com/status-im/nimbus-build-system) repo. + +The following sections explain how to set up your build environment on your platform. ### Windows dev environment diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system index 34a884e1c..bb82ba898 160000 --- a/vendor/nimbus-build-system +++ b/vendor/nimbus-build-system @@ -1 +1 @@ -Subproject commit 34a884e1cfa8f4f542db7777085f2024b398eea5 +Subproject commit bb82ba89841e625db9a998be4339f090ff47976d From d9b494dff660cf1c1b57c7017d8d9634d0541a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Sat, 13 Jun 2020 02:50:35 +0200 Subject: [PATCH 36/70] run_node.sh: macOS fix --- tests/simulation/run_node.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/simulation/run_node.sh b/tests/simulation/run_node.sh index 42ef0f06e..39621bc3d 100755 --- a/tests/simulation/run_node.sh +++ b/tests/simulation/run_node.sh @@ -61,7 +61,7 @@ if [[ $NODE_ID -lt $TOTAL_NODES ]]; then pushd "$VALIDATORS_DIR" >/dev/null for VALIDATOR in $(ls | tail -n +$(( ($VALIDATORS_PER_NODE * $NODE_ID) + 1 )) | head -n $ATTACHED_VALIDATORS); do - cp -ar "$VALIDATOR" "$NODE_VALIDATORS_DIR" + cp -a "$VALIDATOR" "$NODE_VALIDATORS_DIR" cp -a "$SECRETS_DIR/$VALIDATOR" "$NODE_SECRETS_DIR" done popd >/dev/null From ed054d601f7df8ff7d9b8c1542aab182ab1b1d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Sun, 14 Jun 2020 04:25:19 +0200 Subject: [PATCH 37/70] Azure: reduce 32-bit Windows job runtime (#1168) * Azure: lighten the load on 32-bit * disable mainnet tests on 32-bit Windows --- Makefile | 7 +++++++ azure-pipelines.yml | 6 +++++- beacon_chain.nimble | 51 +++++++++++++++++++++++++++------------------ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index f0d2d2d87..f772ed9db 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,13 @@ all: | $(TOOLS) libnfuzz.so libnfuzz.a # must be included after the default target -include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk +ifeq ($(OS), Windows_NT) + ifeq ($(ARCH), x86) + # 32-bit Windows is not supported by libbacktrace/libunwind + USE_LIBBACKTRACE := 0 + endif +endif + # "--define:release" implies "--stacktrace:off" and it cannot be added to config.nims ifeq ($(USE_LIBBACKTRACE), 0) NIM_PARAMS := $(NIM_PARAMS) -d:debug -d:disable_libbacktrace diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f6a62c346..b809912c2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -65,10 +65,14 @@ jobs: mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} CI_CACHE=NimBinaries update mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} fetch-dlls mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} LOG_LEVEL=TRACE - mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} LOG_LEVEL=TRACE NIMFLAGS="-d:testnet_servers_image" + if [[ $PLATFORM == "x64" ]]; then + # everything builds more slowly on 32-bit, since there's no libbacktrace support + mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} LOG_LEVEL=TRACE NIMFLAGS="-d:testnet_servers_image" + fi file build/beacon_node # fail fast export NIMTEST_ABORT_ON_ERROR=1 scripts/setup_official_tests.sh jsonTestsCache mingw32-make -j2 ARCH_OVERRIDE=${PLATFORM} DISABLE_TEST_FIXTURES_SCRIPT=1 test displayName: 'build and test' + diff --git a/beacon_chain.nimble b/beacon_chain.nimble index 03f778d36..c88e5d40a 100644 --- a/beacon_chain.nimble +++ b/beacon_chain.nimble @@ -37,7 +37,7 @@ requires "nim >= 0.19.0", "yaml" ### Helper functions -proc buildBinary(name: string, srcDir = "./", params = "", cmdParams = "", lang = "c") = +proc buildAndRunBinary(name: string, srcDir = "./", params = "", cmdParams = "", lang = "c") = if not dirExists "build": mkDir "build" # allow something like "nim test --verbosity:0 --hints:off beacon_chain.nims" @@ -47,7 +47,7 @@ proc buildBinary(name: string, srcDir = "./", params = "", cmdParams = "", lang exec "nim " & lang & " --out:./build/" & name & " -r " & extra_params & " " & srcDir & name & ".nim" & " " & cmdParams task moduleTests, "Run all module tests": - buildBinary "beacon_node", "beacon_chain/", + buildAndRunBinary "beacon_node", "beacon_chain/", "-d:chronicles_log_level=TRACE " & "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\" " & "-d:testutils_test_build" @@ -58,35 +58,46 @@ task test, "Run all tests": # pieces of code get tested regularly. Increased test output verbosity is the # price we pay for that. + # Mainnet tests are not enabled on 32-bit Windows, to avoid a 1h20m timeout on + # Azure Pipelines (there's no libbacktrace/libunwind support there, and the + # performance hit is big). + # Minimal config - buildBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "all_tests", "tests/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - # Mainnet config - buildBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "all_tests", "tests/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "all_tests", "tests/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + when not (defined(windows) and defined(i386)): + # Mainnet config + buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "all_tests", "tests/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # Generic SSZ test, doesn't use consensus objects minimal/mainnet presets - buildBinary "test_fixture_ssz_generic_types", "tests/official/", "-d:chronicles_log_level=TRACE" + buildAndRunBinary "test_fixture_ssz_generic_types", "tests/official/", "-d:chronicles_log_level=TRACE" # Consensus object SSZ tests # 0.11.3 - buildBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" - buildBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" + when not (defined(windows) and defined(i386)): + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" # 0.12.1 - buildBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + when not (defined(windows) and defined(i386)): + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # 0.11.3 - buildBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" - buildBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" + when not (defined(windows) and defined(i386)): + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" # 0.12.1 - buildBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + when not (defined(windows) and defined(i386)): + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # State sim; getting into 4th epoch useful to trigger consensus checks - buildBinary "state_sim", "research/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=32" - buildBinary "state_sim", "research/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=128" + buildAndRunBinary "state_sim", "research/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=32" + when not (defined(windows) and defined(i386)): + buildAndRunBinary "state_sim", "research/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=128" + From 360ebd705fd759481683b51a754c68abf7a9ada8 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Sat, 13 Jun 2020 20:57:07 +0200 Subject: [PATCH 38/70] db: compress with snappy * 3gb vs 12gb for 4000 epochs of witti * 3-4x sync blocks/sec performance improvement on my FDE SSD drive * generate less quirky code for primitive types --- beacon_chain/beacon_chain_db.nim | 42 +++++++++++++++++++++++++------- beacon_chain/spec/datatypes.nim | 10 ++++++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/beacon_chain/beacon_chain_db.nim b/beacon_chain/beacon_chain_db.nim index d00cb97ab..6e56a9818 100644 --- a/beacon_chain/beacon_chain_db.nim +++ b/beacon_chain/beacon_chain_db.nim @@ -1,8 +1,8 @@ {.push raises: [Defect].} import - typetraits, stew/[results, endians2], - serialization, chronicles, + typetraits, stew/[results, objects, endians2], + serialization, chronicles, snappy, eth/db/kvstore, ./spec/[datatypes, digest, crypto], ./ssz/[ssz_serialization, merkleization], ./state_transition @@ -66,14 +66,39 @@ func subkey(root: Eth2Digest, slot: Slot): array[40, byte] = proc init*(T: type BeaconChainDB, backend: KVStoreRef): BeaconChainDB = T(backend: backend) +proc snappyEncode(inp: openArray[byte]): seq[byte] = + try: + snappy.encode(inp) + except CatchableError as err: + raiseAssert err.msg + +proc put(db: BeaconChainDB, key: openArray[byte], v: Eth2Digest) = + db.backend.put(key, v.data).expect("working database") + proc put(db: BeaconChainDB, key: openArray[byte], v: auto) = - db.backend.put(key, SSZ.encode(v)).expect("working database") + db.backend.put(key, snappyEncode(SSZ.encode(v))).expect("working database") + +proc get(db: BeaconChainDB, key: openArray[byte], T: type Eth2Digest): Opt[T] = + var res: Opt[T] + proc decode(data: openArray[byte]) = + if data.len == 32: + res.ok Eth2Digest(data: toArray(32, data)) + else: + # If the data can't be deserialized, it could be because it's from a + # version of the software that uses a different SSZ encoding + warn "Unable to deserialize data, old database?", + typ = name(T), dataLen = data.len + discard + + discard db.backend.get(key, decode).expect("working database") + + res proc get(db: BeaconChainDB, key: openArray[byte], T: typedesc): Opt[T] = var res: Opt[T] proc decode(data: openArray[byte]) = try: - res.ok SSZ.decode(data, T) + res.ok SSZ.decode(snappy.decode(data), T) except SerializationError as e: # If the data can't be deserialized, it could be because it's from a # version of the software that uses a different SSZ encoding @@ -99,8 +124,7 @@ proc putState*(db: BeaconChainDB, value: BeaconState) = proc putStateRoot*(db: BeaconChainDB, root: Eth2Digest, slot: Slot, value: Eth2Digest) = - db.backend.put(subkey(root, slot), value.data).expect( - "working database") + db.put(subkey(root, slot), value) proc putBlock*(db: BeaconChainDB, value: SignedBeaconBlock) = db.putBlock(hash_tree_root(value.message), value) @@ -116,10 +140,10 @@ proc delStateRoot*(db: BeaconChainDB, root: Eth2Digest, slot: Slot) = db.backend.del(subkey(root, slot)).expect("working database") proc putHeadBlock*(db: BeaconChainDB, key: Eth2Digest) = - db.backend.put(subkey(kHeadBlock), key.data).expect("working database") + db.put(subkey(kHeadBlock), key) proc putTailBlock*(db: BeaconChainDB, key: Eth2Digest) = - db.backend.put(subkey(kTailBlock), key.data).expect("working database") + db.put(subkey(kTailBlock), key) proc getBlock*(db: BeaconChainDB, key: Eth2Digest): Opt[SignedBeaconBlock] = db.get(subkey(SignedBeaconBlock, key), SignedBeaconBlock) @@ -141,7 +165,7 @@ proc getState*( proc decode(data: openArray[byte]) = try: # TODO can't write to output directly.. - assign(outputAddr[], SSZ.decode(data, BeaconState)) + assign(outputAddr[], SSZ.decode(snappy.decode(data), BeaconState)) except SerializationError as e: # If the data can't be deserialized, it could be because it's from a # version of the software that uses a different SSZ encoding diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 150892367..ded59fc2a 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -661,10 +661,16 @@ func assign*[T](tgt: var T, src: T) = # time in it - `assign`, in the same test, doesn't even show in the perf trace when supportsCopyMem(T): - copyMem(addr tgt, unsafeAddr src, sizeof(tgt)) + when sizeof(src) <= sizeof(int): + tgt = src + else: + copyMem(addr tgt, unsafeAddr src, sizeof(tgt)) elif T is object|tuple: for t, s in fields(tgt, src): - assign(t, s) + when supportsCopyMem(type s) and sizeof(s) <= sizeof(int) * 2: + t = s # Shortcut + else: + assign(t, s) elif T is List|BitList: assign(distinctBase tgt, distinctBase src) elif T is seq: From 50c5d47250eacc6c7cf3f146c9fe0abccc5847df Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Sun, 14 Jun 2020 12:45:53 +0300 Subject: [PATCH 39/70] Add maximum number of workers (peers used) by SyncManager (default: 10) (#1172) Refactor and simplification of `sync` procedure. Fix aggressive looping on excessive recurring failures. --- beacon_chain/sync_manager.nim | 113 +++++++++++++++++----------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/beacon_chain/sync_manager.nim b/beacon_chain/sync_manager.nim index 099ef129d..88750e914 100644 --- a/beacon_chain/sync_manager.nim +++ b/beacon_chain/sync_manager.nim @@ -85,6 +85,7 @@ type pool: PeerPool[A, B] responseTimeout: chronos.Duration sleepTime: chronos.Duration + maxWorkersCount: int maxStatusAge: uint64 maxHeadAge: uint64 maxRecurringFailures: int @@ -680,6 +681,7 @@ proc newSyncManager*[A, B](pool: PeerPool[A, B], getLocalWallSlotCb: GetSlotCallback, getFSAFECb: GetSlotCallback, updateLocalBlocksCb: UpdateLocalBlocksCallback, + maxWorkers = 10, maxStatusAge = uint64(SLOTS_PER_EPOCH * 4), maxHeadAge = uint64(SLOTS_PER_EPOCH * 1), sleepTime = (int(SLOTS_PER_EPOCH) * @@ -702,6 +704,7 @@ proc newSyncManager*[A, B](pool: PeerPool[A, B], result = SyncManager[A, B]( pool: pool, + maxWorkersCount: maxWorkers, maxStatusAge: maxStatusAge, getLocalHeadSlot: getLocalHeadSlotCb, syncUpdate: syncUpdate, @@ -909,14 +912,14 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = # This procedure manages main loop of SyncManager and in this loop it # performs # 1. It checks for current sync status, "are we synced?". - # 2. If we are in active syncing, it tries to acquire peers from PeerPool and - # spawns new sync-workers. + # 2. If we are in active syncing, it tries to acquire new peers from PeerPool + # and spawns new sync-workers. Number of spawned sync-workers can be + # controlled by `maxWorkersCount` value. # 3. It stops spawning sync-workers when we are "in sync". # 4. It calculates syncing performance. mixin getKey, getScore var pending = newSeq[Future[A]]() var acquireFut: Future[A] - var wallSlot, headSlot: Slot var syncSpeed: float = 0.0 template workersCount(): int = @@ -944,53 +947,24 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = traceAsyncErrors watchTask() while true: - wallSlot = man.getLocalWallSlot() - headSlot = man.getLocalHeadSlot() + let wallSlot = man.getLocalWallSlot() + let headSlot = man.getLocalHeadSlot() - var progress: uint64 - if headSlot <= man.queue.lastSlot: - progress = man.queue.progress() - else: - progress = 100'u64 + let progress = + if headSlot <= man.queue.lastSlot: + man.queue.progress() + else: + 100'u64 - debug "Synchronization loop start tick", wall_head_slot = wallSlot, + debug "Synchronization loop tick", wall_head_slot = wallSlot, local_head_slot = headSlot, queue_status = progress, queue_start_slot = man.queue.startSlot, queue_last_slot = man.queue.lastSlot, - workers_count = workersCount(), topics = "syncman" - - if headAge <= man.maxHeadAge: - debug "Synchronization loop sleeping", wall_head_slot = wallSlot, - local_head_slot = headSlot, workers_count = workersCount(), - difference = (wallSlot - headSlot), - max_head_age = man.maxHeadAge, topics = "syncman" - if len(pending) == 0: - man.inProgress = false - await sleepAsync(man.sleepTime) - else: - var peerFut = one(pending) - # We do not care about result here because we going to check peerFut - # later. - discard await withTimeout(peerFut, man.sleepTime) - else: - man.inProgress = true - - if isNil(acquireFut): - acquireFut = man.pool.acquire() - pending.add(acquireFut) - - debug "Synchronization loop waiting for new peer", - wall_head_slot = wallSlot, local_head_slot = headSlot, - workers_count = workersCount(), topics = "syncman" - var peerFut = one(pending) - # We do not care about result here, because we going to check peerFut - # later. - discard await withTimeout(peerFut, man.sleepTime) + waiting_for_new_peer = $not(isNil(acquireFut)), + sync_speed = syncSpeed, workers_count = workersCount(), + topics = "syncman" var temp = newSeqOfCap[Future[A]](len(pending)) - # Update slots to with more recent data - wallSlot = man.getLocalWallSlot() - headSlot = man.getLocalHeadSlot() for fut in pending: if fut.finished(): if fut == acquireFut: @@ -1001,7 +975,7 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = workers_count = workersCount(), errMsg = acquireFut.readError().msg, topics = "syncman" else: - var peer = acquireFut.read() + let peer = acquireFut.read() if headAge <= man.maxHeadAge: # If we are already in sync, we going to release just acquired # peer and do not acquire peers @@ -1011,20 +985,22 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = man.pool.release(peer) else: if headSlot > man.queue.lastSlot: + debug "Synchronization lost, restoring", + wall_head_slot = wallSlot, local_head_slot = headSlot, + queue_last_slot = man.queue.lastSlot, topics = "syncman" man.queue = SyncQueue.init(A, headSlot, wallSlot, man.chunkSize, man.syncUpdate, man.getFirstSlotAFE, 2) + debug "Synchronization loop starting new worker", peer = peer, wall_head_slot = wallSlot, local_head_slot = headSlot, peer_score = peer.getScore(), topics = "syncman" temp.add(syncWorker(man, peer)) - acquireFut = nil - if headAge > man.maxHeadAge: - acquireFut = man.pool.acquire() - temp.add(acquireFut) + # We will create new `acquireFut` later. + acquireFut = nil else: - # Worker finished its work + # We got worker finished its work if fut.failed(): debug "Synchronization loop got worker finished with an error", wall_head_slot = wallSlot, local_head_slot = headSlot, @@ -1037,6 +1013,7 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = topics = "syncman" else: if fut == acquireFut: + # Task which waits for new peer from PeerPool is not yet finished. if headAge <= man.maxHeadAge: debug "Synchronization loop reached sync barrier", wall_head_slot = wallSlot, local_head_slot = headSlot, @@ -1050,13 +1027,39 @@ proc sync*[A, B](man: SyncManager[A, B]) {.async.} = pending = temp + if headAge <= man.maxHeadAge: + debug "Synchronization loop sleeping", wall_head_slot = wallSlot, + local_head_slot = headSlot, workers_count = workersCount(), + difference = (wallSlot - headSlot), + max_head_age = man.maxHeadAge, topics = "syncman" + if len(pending) == 0: + man.inProgress = false + await sleepAsync(man.sleepTime) + else: + debug "Synchronization loop waiting for workers completion", + workers_count = workersCount() + discard await withTimeout(one(pending), man.sleepTime) + else: + man.inProgress = true + + if isNil(acquireFut) and len(pending) < man.maxWorkersCount: + acquireFut = man.pool.acquire() + pending.add(acquireFut) + debug "Synchronization loop waiting for new peer", + wall_head_slot = wallSlot, local_head_slot = headSlot, + workers_count = workersCount(), topics = "syncman", + sleep_time = $man.sleepTime + else: + debug "Synchronization loop waiting for workers", + wall_head_slot = wallSlot, local_head_slot = headSlot, + workers_count = workersCount(), topics = "syncman", + sleep_time = $man.sleep_time + + discard await withTimeout(one(pending), man.sleepTime) + if len(man.failures) > man.maxRecurringFailures and (workersCount() > 1): debug "Number of recurring failures exceeds limit, reseting queue", workers_count = workers_count(), rec_failures = len(man.failures) + # Cleaning up failures. + man.failures.setLen(0) await man.queue.resetWait(none[Slot]()) - - debug "Synchronization loop end tick", wall_head_slot = wallSlot, - local_head_slot = headSlot, workers_count = workersCount(), - waiting_for_new_peer = $not(isNil(acquireFut)), - sync_speed = syncSpeed, queue_slot = man.queue.outSlot, - topics = "syncman" From 95a8a0e81c027a0191d722c28394d1918fac1a12 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Sun, 14 Jun 2020 13:45:05 +0200 Subject: [PATCH 40/70] bump libp2p (#1173) --- vendor/nim-libp2p | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 92579435b..85b56d0b3 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 92579435b6b5637d573bd2e0b7338791f7a768d4 +Subproject commit 85b56d0b3a53611feaddd9247dd55bb43dc27103 From 8a2c796e838f3280ac8a6f1790cdedb42100ba01 Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Mon, 15 Jun 2020 11:38:05 +0200 Subject: [PATCH 41/70] switch 12 unchanged spec references from 0.11.x to 0.12.1; line-wrap and remove pointless-to-counterproductive "return" --- beacon_chain/mainchain_monitor.nim | 11 ++++++----- beacon_chain/spec/datatypes.nim | 8 ++++---- beacon_chain/spec/state_transition_block.nim | 8 ++++---- beacon_chain/spec/state_transition_epoch.nim | 2 +- beacon_chain/spec/state_transition_helpers.nim | 2 +- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/beacon_chain/mainchain_monitor.nim b/beacon_chain/mainchain_monitor.nim index 70e3fed9b..f2cec4182 100644 --- a/beacon_chain/mainchain_monitor.nim +++ b/beacon_chain/mainchain_monitor.nim @@ -91,14 +91,15 @@ const # module seems broken. Investigate and file this as an issue. {.push warning[LockLevel]: off.} -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#get_eth1_data func compute_time_at_slot(state: BeaconState, slot: Slot): uint64 = - return state.genesis_time + slot * SECONDS_PER_SLOT + state.genesis_time + slot * SECONDS_PER_SLOT -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#get_eth1_data func voting_period_start_time*(state: BeaconState): uint64 = - let eth1_voting_period_start_slot = state.slot - state.slot mod SLOTS_PER_ETH1_VOTING_PERIOD.uint64 - return compute_time_at_slot(state, eth1_voting_period_start_slot) + let eth1_voting_period_start_slot = + state.slot - state.slot mod SLOTS_PER_ETH1_VOTING_PERIOD.uint64 + compute_time_at_slot(state, eth1_voting_period_start_slot) # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data func is_candidate_block(blk: Eth1Block, period_start: uint64): bool = diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index ded59fc2a..be492e861 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -98,7 +98,7 @@ const # TODO: This needs revisiting. # Why was the validator WITHDRAWAL_PERIOD altered in the spec? - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.2/specs/phase0/p2p-interface.md#configuration + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#configuration ATTESTATION_PROPAGATION_SLOT_RANGE* = 32 SLOTS_PER_ETH1_VOTING_PERIOD* = Slot(EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH) @@ -242,7 +242,7 @@ type body*: BeaconBlockBody - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beaconblockheader + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beaconblockheader BeaconBlockHeader* = object slot*: Slot proposer_index*: uint64 @@ -263,7 +263,7 @@ type deposits*: List[Deposit, MAX_DEPOSITS] voluntary_exits*: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#beaconstate + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beaconstate BeaconStateObj* = object # Versioning genesis_time*: uint64 @@ -357,7 +357,7 @@ type block_roots* : array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] state_roots* : array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#fork + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#fork Fork* = object # TODO: Spec introduced an alias for Version = array[4, byte] # and a default parameter to compute_domain diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 98ba0ddcd..184f41080 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -41,7 +41,7 @@ declareGauge beacon_previous_live_validators, "Number of active validators that declareGauge beacon_pending_deposits, "Number of pending deposits (state.eth1_data.deposit_count - state.eth1_deposit_index)" # On block declareGauge beacon_processed_deposits_total, "Number of total deposits included on chain" # On block -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#block-header +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#block-header proc process_block_header*( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.}= @@ -405,7 +405,7 @@ func get_slot_signature*( blsSign(privKey, signing_root.data) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#randao-reveal +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal func get_epoch_signature*( fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, privkey: ValidatorPrivKey): ValidatorSig = @@ -416,7 +416,7 @@ func get_epoch_signature*( blsSign(privKey, signing_root.data) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#signature +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#signature func get_block_signature*( fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, root: Eth2Digest, privkey: ValidatorPrivKey): ValidatorSig = @@ -440,7 +440,7 @@ func get_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth return blsSign(privKey, signing_root.data) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregate-signature +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregate-signature func get_attestation_signature*( fork: Fork, genesis_validators_root: Eth2Digest, attestation: AttestationData, privkey: ValidatorPrivKey): ValidatorSig = diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index 0fc63f182..d7b50b196 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -539,7 +539,7 @@ func process_slashings*(state: var BeaconState, cache: var StateCache) {.nbench. let penalty = penalty_numerator div total_balance * increment decrease_balance(state, index.ValidatorIndex, penalty) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#final-updates +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#final-updates func process_final_updates*(state: var BeaconState) {.nbench.}= let current_epoch = get_current_epoch(state) diff --git a/beacon_chain/spec/state_transition_helpers.nim b/beacon_chain/spec/state_transition_helpers.nim index 12d070abd..5e3935d00 100644 --- a/beacon_chain/spec/state_transition_helpers.nim +++ b/beacon_chain/spec/state_transition_helpers.nim @@ -35,7 +35,7 @@ func get_attesting_indices*( result = result.union(get_attesting_indices( state, a.data, a.aggregation_bits, stateCache)) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#helper-functions-1 +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#helper-functions-1 func get_unslashed_attesting_indices*( state: BeaconState, attestations: openarray[PendingAttestation], stateCache: var StateCache): HashSet[ValidatorIndex] = From f3fd7e17a3fbc06834d677649847831741050560 Mon Sep 17 00:00:00 2001 From: tersec Date: Mon, 15 Jun 2020 13:06:21 +0000 Subject: [PATCH 42/70] Revert "bump libp2p (#1173)" This reverts commit 95a8a0e81c027a0191d722c28394d1918fac1a12. --- vendor/nim-libp2p | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 85b56d0b3..92579435b 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 85b56d0b3a53611feaddd9247dd55bb43dc27103 +Subproject commit 92579435b6b5637d573bd2e0b7338791f7a768d4 From 96f26c447cc67d2797792197ec20252f9a6c1f75 Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Mon, 15 Jun 2020 22:41:26 +0300 Subject: [PATCH 43/70] Replace zero-point rewind with rewind to latest finalized epoch's first slot. (#1176) * Replace zero-point rewind with rewind to latest finalized epoch's first slot. * Fix tests. * Add missing penalty for MissingParent Fix comments. --- beacon_chain/sync_manager.nim | 169 ++++++---------------------------- 1 file changed, 28 insertions(+), 141 deletions(-) diff --git a/beacon_chain/sync_manager.nim b/beacon_chain/sync_manager.nim index 88750e914..ed5535d1f 100644 --- a/beacon_chain/sync_manager.nim +++ b/beacon_chain/sync_manager.nim @@ -25,7 +25,7 @@ const ## Peer's response contains incorrect blocks. PeerScoreBadResponse* = -1000 ## Peer's response is not in requested range. - PeerScoreJokeBlocks* = -200 + PeerScoreMissingBlocks* = -200 ## Peer response contains too many empty blocks. type @@ -78,7 +78,6 @@ type debtsQueue: HeapQueue[SyncRequest[T]] debtsCount: uint64 readyQueue: HeapQueue[SyncResult[T]] - zeroPoint: Option[Slot] suspects: seq[SyncResult[T]] SyncManager*[A, B] = ref object @@ -260,34 +259,8 @@ proc init*[T](t1: typedesc[SyncQueue], t2: typedesc[T], # missing" error. Lets call such peers "jokers", because they are joking # with responses. # - # To fix "joker" problem i'm going to introduce "zero-point" which will - # represent first non-empty slot in gap at the end of requested chunk. - # If SyncQueue receives chunk of blocks with gap at the end and this chunk - # will be successfully processed by `block_pool` it will set `zero_point` to - # the first uncertain (empty) slot. For example: - # - # Case 1 - # X X X X X - - # 3 4 5 6 7 8 - # - # Case2 - # X X - - - - - # 3 4 5 6 7 8 - # - # In Case 1 `zero-point` will be equal to 8, in Case 2 `zero-point` will be - # set to 5. - # - # When `zero-point` is set and the next received chunk of blocks will be - # empty, then peer produced this chunk of blocks will be added to suspect - # list. - # - # If the next chunk of blocks has at least one non-empty block and this chunk - # will be successfully processed by `block_pool`, then `zero-point` will be - # reset and suspect list will be cleared. - # - # If the `block_pool` failed to process next chunk of blocks, SyncQueue will - # perform rollback to `zero-point` and penalize all the peers in suspect list. - + # To fix "joker" problem we going to perform rollback to the latest finalized + # epoch's first slot. doAssert(chunkSize > 0'u64, "Chunk size should not be zero") result = SyncQueue[T]( startSlot: start, @@ -372,10 +345,10 @@ proc resetWait*[T](sq: SyncQueue[T], toSlot: Option[Slot]) {.async.} = # without missing any blocks. There 3 sources: # 1. Debts queue. # 2. Processing queue (`inpSlot`, `outSlot`). - # 3. Requested slot `toSlot` (which can be `zero-point` slot). + # 3. Requested slot `toSlot`. # # Queue's `outSlot` is the lowest slot we added to `block_pool`, but - # `zero-point` slot can be less then `outSlot`. `debtsQueue` holds only not + # `toSlot` slot can be less then `outSlot`. `debtsQueue` holds only not # added slot requests, so it can't be bigger then `outSlot` value. var minSlot = sq.outSlot if toSlot.isSome(): @@ -441,8 +414,7 @@ proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], if res: continue else: - # SyncQueue reset happens (it can't be `zero-point` reset, or continous - # failure reset). We are exiting to wake up sync-worker. + # SyncQueue reset happens. We are exiting to wake up sync-worker. exitNow = true break let syncres = SyncResult[T](request: sr, data: data) @@ -460,55 +432,6 @@ proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], let item = sq.readyQueue.pop() let res = sq.syncUpdate(item.request, item.data) if res.isOk: - if sq.zeroPoint.isSome(): - if item.isEmpty(): - # If the `zeropoint` is set and response is empty, we will add this - # request to suspect list. - debug "Adding peer to suspect list", peer = item.request.item, - request_slot = item.request.slot, - request_count = item.request.count, - request_step = item.request.step, - response_count = len(item.data), topics = "syncman" - sq.suspects.add(item) - else: - # If the `zeropoint` is set and response is not empty, we will clean - # suspect list and reset `zeropoint`. - sq.suspects.setLen(0) - sq.zeroPoint = none[Slot]() - # At this point `zeropoint` is unset, but received response can have - # gap at the end. - if item.hasEndGap(): - debug "Zero-point reset and new zero-point found", - peer = item.request.item, request_slot = item.request.slot, - request_count = item.request.count, - request_step = item.request.step, - response_count = len(item.data), - blocks_map = getShortMap(item.request, item.data), - topics = "syncman" - sq.suspects.add(item) - sq.zeroPoint = some(item.getLastNonEmptySlot()) - else: - debug "Zero-point reset", peer = item.request.item, - request_slot = item.request.slot, - request_count = item.request.count, - request_step = item.request.step, - response_count = len(item.data), - blocks_map = getShortMap(item.request, item.data), - topics = "syncman" - else: - # If the `zeropoint` is not set and response has gap at the end, we - # will add first suspect to the suspect list and set `zeropoint`. - if item.hasEndGap(): - debug "New zero-point found", peer = item.request.item, - request_slot = item.request.slot, - request_count = item.request.count, - request_step = item.request.step, - response_count = len(item.data), - blocks_map = getShortMap(item.request, item.data), - topics = "syncman" - sq.suspects.add(item) - sq.zeroPoint = some(item.getLastNonEmptySlot()) - sq.outSlot = sq.outSlot + item.request.count sq.wakeupWaiters() else: @@ -522,63 +445,26 @@ proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], var resetSlot: Option[Slot] if res.error == BlockError.MissingParent: - if sq.zeroPoint.isSome(): - # If the `zeropoint` is set and we are unable to store response in - # `block_pool` we are going to revert suspicious responses list. - - # If `zeropoint` is set, suspicious list should not be empty. - var req: SyncRequest[T] - if isEmpty(sq.suspects[0]): - # If initial suspicious response is an empty list, then previous - # chunk of blocks did not have a gap at the end. So we are going to - # request suspicious response one more time without any changes. - req = sq.suspects[0].request - else: - # If initial suspicious response is not an empty list, we are going - # to request only gap at the end of the suspicious response. - let startSlot = sq.suspects[0].getLastNonEmptySlot() + 1'u64 - let lastSlot = sq.suspects[0].request.lastSlot() - req = SyncRequest.init(T, startSlot, lastSlot) - - debug "Resolve joker's problem", request_slot = req.slot, - request_count = req.count, - request_step = req.step, - suspects_count = (len(sq.suspects) - 1) - - sq.suspects[0].request.item.updateScore(PeerScoreJokeBlocks) - - sq.toDebtsQueue(req) - # We move all left suspicious responses to the debts queue. - if len(sq.suspects) > 1: - for i in 1 ..< len(sq.suspects): - sq.toDebtsQueue(sq.suspects[i].request) - sq.suspects[i].request.item.updateScore(PeerScoreJokeBlocks) - - # Reset state to the `zeropoint`. - sq.suspects.setLen(0) - resetSlot = sq.zeroPoint - sq.zeroPoint = none[Slot]() + # If we got `BlockError.MissingParent` it means that peer returns chain + # of blocks with holes or `block_pool` is in incomplete state. We going + # to rewind to the first slot at latest finalized epoch. + let req = item.request + let finalizedSlot = sq.getFirstSlotAFE() + if finalizedSlot < req.slot: + warn "Unexpected missing parent, rewind happens", + peer = req.item, rewind_to_slot = finalizedSlot, + request_slot = req.slot, request_count = req.count, + request_step = req.step, blocks_count = len(item.data), + blocks_map = getShortMap(req, item.data) + resetSlot = some(finalizedSlot) + req.item.updateScore(PeerScoreMissingBlocks) else: - # If we got `BlockError.MissingParent` and `zero-point` is not set - # it means that peer returns chain of blocks with holes or block_pool - # in incorrect state. We going to rewind to the latest finalized - # epoch. - let req = item.request - let finalizedSlot = sq.getFirstSlotAFE() - if finalizedSlot < req.slot: - warn "Unexpected missing parent, rewind to latest finalized epoch slot", - peer = req.item, to_slot = finalizedSlot, - request_slot = req.slot, request_count = req.count, - request_step = req.step, blocks_count = len(item.data), - blocks_map = getShortMap(req, item.data) - await sq.resetWait(some(finalizedSlot)) - else: - error "Unexpected missing parent at finalized epoch slot", - peer = req.item, to_slot = finalizedSlot, - request_slot = req.slot, request_count = req.count, - request_step = req.step, blocks_count = len(item.data), - blocks_map = getShortMap(req, item.data) - req.item.updateScore(PeerScoreBadBlocks) + error "Unexpected missing parent at finalized epoch slot", + peer = req.item, to_slot = finalizedSlot, + request_slot = req.slot, request_count = req.count, + request_step = req.step, blocks_count = len(item.data), + blocks_map = getShortMap(req, item.data) + req.item.updateScore(PeerScoreBadBlocks) elif res.error == BlockError.Invalid: let req = item.request warn "Received invalid sequence of blocks", peer = req.item, @@ -598,8 +484,9 @@ proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], sq.toDebtsQueue(item.request) if resetSlot.isSome(): await sq.resetWait(resetSlot) - debug "Zero-point reset happens", queue_input_slot = sq.inpSlot, - queue_output_slot = sq.outSlot + debug "Rewind to slot was happened", reset_slot = reset_slot.get(), + queue_input_slot = sq.inpSlot, + queue_output_slot = sq.outSlot break proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T]) = From 090f06614a1688205690a8a749b156b5664874ae Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Tue, 16 Jun 2020 00:40:16 +0200 Subject: [PATCH 44/70] Dual headed fork choice (#1163) * Dual headed fork choice * fix finalizedEpoch not moving * reduce fork choice verbosity --- .gitignore | 2 +- beacon_chain/attestation_pool.nim | 105 ++++++++++++++++++++++- beacon_chain/beacon_node.nim | 5 +- beacon_chain/beacon_node_common.nim | 6 +- beacon_chain/beacon_node_types.nim | 6 +- beacon_chain/fork_choice/fork_choice.nim | 8 +- tests/test_attestation_pool.nim | 10 +-- 7 files changed, 125 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index c73c8da16..da6ab1c8f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,9 +32,9 @@ build/ *.sqlite3 /local_testnet_data*/ +/local_testnet*_data*/ # Prometheus db /data # Grafana dashboards /docker/*.json - diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index a809e6a97..5b2f44694 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -11,7 +11,8 @@ import deques, sequtils, tables, options, chronicles, stew/[byteutils], json_serialization/std/sets, ./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], - ./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types + ./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types, + ./fork_choice/[fork_choice_types, fork_choice] logScope: topics = "attpool" @@ -22,10 +23,23 @@ func init*(T: type AttestationPool, blockPool: BlockPool): T = # TODO blockPool is only used when resolving orphaned attestations - it should # probably be removed as a dependency of AttestationPool (or some other # smart refactoring) + + # TODO: In tests, on blockpool.init the finalized root + # from the `headState` and `justifiedState` is zero + let forkChoice = initForkChoice( + finalized_block_slot = default(Slot), # This is unnecessary for fork choice but may help external components + finalized_block_state_root = default(Eth2Digest), # This is unnecessary for fork choice but may help external components + justified_epoch = blockPool.headState.data.data.current_justified_checkpoint.epoch, + finalized_epoch = blockPool.headState.data.data.finalized_checkpoint.epoch, + # finalized_root = blockPool.headState.data.data.finalized_checkpoint.root + finalized_root = blockPool.finalizedHead.blck.root + ).get() + T( mapSlotsToAttestations: initDeque[AttestationsSeen](), blockPool: blockPool, unresolved: initTable[Eth2Digest, UnresolvedAttestation](), + forkChoice_v2: forkChoice ) proc combine*(tgt: var Attestation, src: Attestation, flags: UpdateFlags) = @@ -107,13 +121,21 @@ proc slotIndex( func updateLatestVotes( pool: var AttestationPool, state: BeaconState, attestationSlot: Slot, participants: seq[ValidatorIndex], blck: BlockRef) = + + # ForkChoice v2 + let target_epoch = compute_epoch_at_slot(attestationSlot) + for validator in participants: + # ForkChoice v1 let pubKey = state.validators[validator].pubkey current = pool.latestAttestations.getOrDefault(pubKey) if current.isNil or current.slot < attestationSlot: pool.latestAttestations[pubKey] = blck + # ForkChoice v2 + pool.forkChoice_v2.process_attestation(validator, blck.root, target_epoch) + func get_attesting_indices_seq(state: BeaconState, attestation_data: AttestationData, bits: CommitteeValidatorsBits, @@ -261,6 +283,34 @@ proc add*(pool: var AttestationPool, attestation: Attestation) = pool.addResolved(blck, attestation) +proc addForkChoice_v2*(pool: var AttestationPool, blck: BlockRef) = + ## Add a verified block to the fork choice context + ## The current justifiedState of the block pool is used as reference + + # TODO: add(BlockPool, blockRoot: Eth2Digest, SignedBeaconBlock): BlockRef + # should ideally return the justified_epoch and finalized_epoch + # so that we can pass them directly to this proc without having to + # redo "updateStateData" + # + # In any case, `updateStateData` should shortcut + # to `getStateDataCached` + + updateStateData( + pool.blockPool, + pool.blockPool.tmpState, + BlockSlot(blck: blck, slot: blck.slot) + ) + + let blockData = pool.blockPool.get(blck) + pool.forkChoice_v2.process_block( + slot = blck.slot, + block_root = blck.root, + parent_root = if not blck.parent.isNil: blck.parent.root else: default(Eth2Digest), + state_root = default(Eth2Digest), # This is unnecessary for fork choice but may help external components + justified_epoch = pool.blockPool.tmpState.data.data.current_justified_checkpoint.epoch, + finalized_epoch = pool.blockPool.tmpState.data.data.finalized_checkpoint.epoch, + ).get() + proc getAttestationsForSlot*(pool: AttestationPool, newBlockSlot: Slot): Option[AttestationsSeen] = if newBlockSlot < (GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY): @@ -395,7 +445,10 @@ proc resolve*(pool: var AttestationPool) = for a in resolved: pool.addResolved(a.blck, a.attestation) -func latestAttestation*( +# Fork choice v1 +# --------------------------------------------------------------- + +func latestAttestation( pool: AttestationPool, pubKey: ValidatorPubKey): BlockRef = pool.latestAttestations.getOrDefault(pubKey) @@ -403,7 +456,7 @@ func latestAttestation*( # The structure of this code differs from the spec since we use a different # strategy for storing states and justification points - it should nonetheless # be close in terms of functionality. -func lmdGhost*( +func lmdGhost( pool: AttestationPool, start_state: BeaconState, start_block: BlockRef): BlockRef = # TODO: a Fenwick Tree datastructure to keep track of cumulated votes @@ -454,7 +507,7 @@ func lmdGhost*( winCount = candCount head = winner -proc selectHead*(pool: AttestationPool): BlockRef = +proc selectHead_v1(pool: AttestationPool): BlockRef = let justifiedHead = pool.blockPool.latestJustifiedBlock() @@ -462,3 +515,47 @@ proc selectHead*(pool: AttestationPool): BlockRef = lmdGhost(pool, pool.blockPool.justifiedState.data.data, justifiedHead.blck) newHead + +# Fork choice v2 +# --------------------------------------------------------------- + +func getAttesterBalances(state: StateData): seq[Gwei] {.noInit.}= + ## Get the balances from a state + result.newSeq(state.data.data.validators.len) # zero-init + + let epoch = state.data.data.slot.compute_epoch_at_slot() + + for i in 0 ..< result.len: + # All non-active validators have a 0 balance + template validator: Validator = state.data.data.validators[i] + if validator.is_active_validator(epoch): + result[i] = validator.effective_balance + +proc selectHead_v2(pool: var AttestationPool): BlockRef = + let attesterBalances = pool.blockPool.justifiedState.getAttesterBalances() + + let newHead = pool.forkChoice_v2.find_head( + justified_epoch = pool.blockPool.justifiedState.data.data.slot.compute_epoch_at_slot(), + justified_root = pool.blockPool.head.justified.blck.root, + finalized_epoch = pool.blockPool.headState.data.data.finalized_checkpoint.epoch, + justified_state_balances = attesterBalances + ).get() + + pool.blockPool.getRef(newHead) + +proc pruneBefore*(pool: var AttestationPool, finalizedhead: BlockSlot) = + pool.forkChoice_v2.maybe_prune(finalizedHead.blck.root).get() + +# Dual-Headed Fork choice +# --------------------------------------------------------------- + +proc selectHead*(pool: var AttestationPool): BlockRef = + let head_v1 = pool.selectHead_v1() + let head_v2 = pool.selectHead_v2() + + if head_v1 != head_v2: + error "Fork choice engines in disagreement, using block from v1.", + v1_block = shortlog(head_v1), + v2_block = shortlog(head_v2) + + return head_v1 diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 6321cf4bd..b64ed47ce 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -313,6 +313,10 @@ proc storeBlock( return err(blck.error) + # Still here? This means we received a valid block and we need to add it + # to the fork choice + node.attestationPool.addForkChoice_v2(blck.get()) + # The block we received contains attestations, and we might not yet know about # all of them. Let's add them to the attestation pool - in case the block # is not yet resolved, neither will the attestations be! @@ -1134,4 +1138,3 @@ programMain: config.depositContractAddress, config.depositPrivateKey, delayGenerator) - diff --git a/beacon_chain/beacon_node_common.nim b/beacon_chain/beacon_node_common.nim index f999c740d..007dad4d6 100644 --- a/beacon_chain/beacon_node_common.nim +++ b/beacon_chain/beacon_node_common.nim @@ -20,7 +20,8 @@ import conf, time, beacon_chain_db, attestation_pool, block_pool, eth2_network, beacon_node_types, mainchain_monitor, request_manager, - sync_manager + sync_manager, + fork_choice/fork_choice # This removes an invalid Nim warning that the digest module is unused here # It's currently used for `shortLog(head.blck.root)` @@ -68,6 +69,9 @@ proc updateHead*(node: BeaconNode): BlockRef = node.blockPool.updateHead(newHead) beacon_head_root.set newHead.root.toGaugeValue + # Cleanup the fork choice v2 if we have a finalized head + node.attestationPool.pruneBefore(node.blockPool.finalizedHead) + newHead template findIt*(s: openarray, predicate: untyped): int64 = diff --git a/beacon_chain/beacon_node_types.nim b/beacon_chain/beacon_node_types.nim index 4015c3d60..03cf4767d 100644 --- a/beacon_chain/beacon_node_types.nim +++ b/beacon_chain/beacon_node_types.nim @@ -5,7 +5,8 @@ import stew/endians2, spec/[datatypes, crypto, digest], block_pools/block_pools_types, - block_pool # TODO: refactoring compat shim + block_pool, # TODO: refactoring compat shim + fork_choice/fork_choice_types export block_pools_types @@ -74,6 +75,9 @@ type latestAttestations*: Table[ValidatorPubKey, BlockRef] ##\ ## Map that keeps track of the most recent vote of each attester - see ## fork_choice + forkChoice_v2*: ForkChoice ##\ + ## The alternative fork choice "proto_array" that will ultimately + ## replace the original one # ############################################# # diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index c9fc6db77..7b4f828bb 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -117,7 +117,7 @@ func process_attestation*( vote.next_epoch = target_epoch {.noSideEffect.}: - info "Integrating vote in fork choice", + trace "Integrating vote in fork choice", validator_index = $validator_index, new_vote = shortlog(vote) else: @@ -129,7 +129,7 @@ func process_attestation*( ignored_block_root = shortlog(block_root), ignored_target_epoch = $target_epoch else: - info "Ignoring double-vote for fork choice", + trace "Ignoring double-vote for fork choice", validator_index = $validator_index, current_vote = shortlog(vote), ignored_block_root = shortlog(block_root), @@ -159,7 +159,7 @@ func process_block*( return err("process_block_error: " & $err) {.noSideEffect.}: - info "Integrating block in fork choice", + trace "Integrating block in fork choice", block_root = $shortlog(block_root), parent_root = $shortlog(parent_root), justified_epoch = $justified_epoch, @@ -205,7 +205,7 @@ func find_head*( return err("find_head failed: " & $ghost_err) {.noSideEffect.}: - info "Fork choice requested", + debug "Fork choice requested", justified_epoch = $justified_epoch, justified_root = shortlog(justified_root), finalized_epoch = $finalized_epoch, diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index 36af8a135..cddac4713 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -33,7 +33,7 @@ suiteReport "Attestation pool processing" & preset(): check: process_slots(state.data, state.data.data.slot + 1) - # pool[].add(blockPool[].tail) # Make the tail known to fork choice + pool[].addForkChoice_v2(blockPool[].tail) # Make the tail known to fork choice timedTest "Can add and retrieve simple attestation" & preset(): var cache = get_empty_per_epoch_cache() @@ -161,7 +161,7 @@ suiteReport "Attestation pool processing" & preset(): b1Root = hash_tree_root(b1.message) b1Add = blockpool[].add(b1Root, b1)[] - # pool[].add(b1Add) - make a block known to the future fork choice + pool[].addForkChoice_v2(b1Add) let head = pool[].selectHead() check: @@ -172,7 +172,7 @@ suiteReport "Attestation pool processing" & preset(): b2Root = hash_tree_root(b2.message) b2Add = blockpool[].add(b2Root, b2)[] - # pool[].add(b2Add) - make a block known to the future fork choice + pool[].addForkChoice_v2(b2Add) let head2 = pool[].selectHead() check: @@ -185,7 +185,7 @@ suiteReport "Attestation pool processing" & preset(): b10Root = hash_tree_root(b10.message) b10Add = blockpool[].add(b10Root, b10)[] - # pool[].add(b10Add) - make a block known to the future fork choice + pool[].addForkChoice_v2(b10Add) let head = pool[].selectHead() check: @@ -202,7 +202,7 @@ suiteReport "Attestation pool processing" & preset(): state.data.data, state.data.data.slot, 1.CommitteeIndex, cache) attestation0 = makeAttestation(state.data.data, b10Root, bc1[0], cache) - # pool[].add(b11Add) - make a block known to the future fork choice + pool[].addForkChoice_v2(b11Add) pool[].add(attestation0) let head2 = pool[].selectHead() From 9335533503302582694fee0619a12aeef1151b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Tue, 16 Jun 2020 04:42:47 +0200 Subject: [PATCH 45/70] bump submodules --- beacon_chain/attestation_pool.nim | 2 +- beacon_chain/beacon_node_common.nim | 3 +-- vendor/nim-eth | 2 +- vendor/nim-faststreams | 2 +- vendor/nim-json-rpc | 2 +- vendor/nim-libp2p | 2 +- vendor/nim-testutils | 2 +- vendor/nimbus-build-system | 2 +- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index 5b2f44694..f305299b0 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -12,7 +12,7 @@ import chronicles, stew/[byteutils], json_serialization/std/sets, ./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], ./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types, - ./fork_choice/[fork_choice_types, fork_choice] + ./fork_choice/fork_choice logScope: topics = "attpool" diff --git a/beacon_chain/beacon_node_common.nim b/beacon_chain/beacon_node_common.nim index 007dad4d6..e356d50ff 100644 --- a/beacon_chain/beacon_node_common.nim +++ b/beacon_chain/beacon_node_common.nim @@ -20,8 +20,7 @@ import conf, time, beacon_chain_db, attestation_pool, block_pool, eth2_network, beacon_node_types, mainchain_monitor, request_manager, - sync_manager, - fork_choice/fork_choice + sync_manager # This removes an invalid Nim warning that the digest module is unused here # It's currently used for `shortLog(head.blck.root)` diff --git a/vendor/nim-eth b/vendor/nim-eth index 225a9ad41..4d0a7a46b 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 225a9ad41cc0f4cd6d42153e64b356bb03f26274 +Subproject commit 4d0a7a46ba38947b8daecb1b5ae817c82c8e16c5 diff --git a/vendor/nim-faststreams b/vendor/nim-faststreams index 81c24860e..5df69fc69 160000 --- a/vendor/nim-faststreams +++ b/vendor/nim-faststreams @@ -1 +1 @@ -Subproject commit 81c24860e2622a15e05c81d15e3d1cc02c460870 +Subproject commit 5df69fc6961e58205189cd92ae2477769fa8c4c0 diff --git a/vendor/nim-json-rpc b/vendor/nim-json-rpc index 271512c16..8c1a8ef8d 160000 --- a/vendor/nim-json-rpc +++ b/vendor/nim-json-rpc @@ -1 +1 @@ -Subproject commit 271512c161706e08533690fdc8bbbbc54f1dc0ed +Subproject commit 8c1a8ef8d9fd1705d4e8640b4c30df2caee76881 diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 92579435b..9d9f793b4 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 92579435b6b5637d573bd2e0b7338791f7a768d4 +Subproject commit 9d9f793b4f4674b95b524e175509ea6402744f68 diff --git a/vendor/nim-testutils b/vendor/nim-testutils index 1601894ec..622607e98 160000 --- a/vendor/nim-testutils +++ b/vendor/nim-testutils @@ -1 +1 @@ -Subproject commit 1601894ec1fd1c7095d405eb0c846cac212fb18f +Subproject commit 622607e98ee54ab600af5d43d32244333a643e2a diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system index bb82ba898..46b6f7880 160000 --- a/vendor/nimbus-build-system +++ b/vendor/nimbus-build-system @@ -1 +1 @@ -Subproject commit bb82ba89841e625db9a998be4339f090ff47976d +Subproject commit 46b6f78806026b37e4710eabf8bd047969d2d23c From 89e4819ce9a4e1c6b804d556cede3d942af45214 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 16 Jun 2020 07:45:04 +0200 Subject: [PATCH 46/70] collect signature production and verificaiton in one place (#1179) * collect signature production and verificaiton in one place Signatures are made over data and domain - here we collect all such activities in one place. Also: * security: fix cast-before-range-check * log block/attestation verification consistently * run block verification based on `getProposer` in its own history * clean up some unused stuff * import * missing raises --- beacon_chain/attestation_aggregation.nim | 32 +++-- beacon_chain/block_pools/clearance.nim | 65 +++++---- beacon_chain/interop.nim | 7 +- beacon_chain/spec/beaconstate.nim | 39 +++--- beacon_chain/spec/crypto.nim | 30 ++-- beacon_chain/spec/datatypes.nim | 10 +- beacon_chain/spec/keystore.nim | 7 +- beacon_chain/spec/signatures.nim | 137 +++++++++++++++++++ beacon_chain/spec/state_transition_block.nim | 94 +++---------- beacon_chain/state_transition.nim | 25 ++-- beacon_chain/validator_pool.nim | 5 +- research/block_sim.nim | 3 +- tests/mocking/mock_attestations.nim | 2 +- tests/mocking/mock_blocks.nim | 5 +- tests/mocking/mock_deposits.nim | 94 ++----------- tests/testblockutil.nim | 6 +- 16 files changed, 285 insertions(+), 276 deletions(-) create mode 100644 beacon_chain/spec/signatures.nim diff --git a/beacon_chain/attestation_aggregation.nim b/beacon_chain/attestation_aggregation.nim index 5c2e9ac9b..67c1e0a5f 100644 --- a/beacon_chain/attestation_aggregation.nim +++ b/beacon_chain/attestation_aggregation.nim @@ -9,10 +9,13 @@ import options, chronicles, - ./spec/[beaconstate, datatypes, crypto, digest, helpers, validator, - state_transition_block], + ./spec/[ + beaconstate, datatypes, crypto, digest, helpers, validator, signatures], ./block_pool, ./attestation_pool, ./beacon_node_types, ./ssz +logScope: + topics = "att_aggr" + # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregation-selection func is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, slot_signature: ValidatorSig): bool = @@ -75,17 +78,20 @@ proc aggregate_attestations*( proc isValidAttestation*( pool: AttestationPool, attestation: Attestation, current_slot: Slot, topicCommitteeIndex: uint64): bool = + logScope: + topics = "att_aggr valid_att" + received_attestation = shortLog(attestation) + # The attestation's committee index (attestation.data.index) is for the # correct subnet. if attestation.data.index != topicCommitteeIndex: - debug "isValidAttestation: attestation's committee index not for the correct subnet", - topicCommitteeIndex = topicCommitteeIndex, - attestation_data_index = attestation.data.index + debug "attestation's committee index not for the correct subnet", + topicCommitteeIndex = topicCommitteeIndex return false if not (attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot and current_slot >= attestation.data.slot): - debug "isValidAttestation: attestation.data.slot not within ATTESTATION_PROPAGATION_SLOT_RANGE" + debug "attestation.data.slot not within ATTESTATION_PROPAGATION_SLOT_RANGE" return false # The attestation is unaggregated -- that is, it has exactly one @@ -100,11 +106,10 @@ proc isValidAttestation*( continue onesCount += 1 if onesCount > 1: - debug "isValidAttestation: attestation has too many aggregation bits", - aggregation_bits = attestation.aggregation_bits + debug "attestation has too many aggregation bits" return false if onesCount != 1: - debug "isValidAttestation: attestation has too few aggregation bits" + debug "attestation has too few aggregation bits" return false # The attestation is the first valid attestation received for the @@ -117,9 +122,7 @@ proc isValidAttestation*( # Attestations might be aggregated eagerly or lazily; allow for both. for validation in attestationEntry.validations: if attestation.aggregation_bits.isSubsetOf(validation.aggregation_bits): - debug "isValidAttestation: attestation already exists at slot", - attestation_data_slot = attestation.data.slot, - attestation_aggregation_bits = attestation.aggregation_bits, + debug "attestation already exists at slot", attestation_pool_validation = validation.aggregation_bits return false @@ -131,8 +134,7 @@ proc isValidAttestation*( # propagated - i.e. imagine that attestations are smaller than blocks and # therefore propagate faster, thus reordering their arrival in some nodes if pool.blockPool.get(attestation.data.beacon_block_root).isNone(): - debug "isValidAttestation: block doesn't exist in block pool", - attestation_data_beacon_block_root = attestation.data.beacon_block_root + debug "block doesn't exist in block pool" return false # The signature of attestation is valid. @@ -143,7 +145,7 @@ proc isValidAttestation*( pool.blockPool.headState.data.data, get_indexed_attestation( pool.blockPool.headState.data.data, attestation, cache), {}): - debug "isValidAttestation: signature verification failed" + debug "signature verification failed" return false true diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index 2951c7d16..a8f058319 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -5,12 +5,13 @@ # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. +{.push raises: [Defect].} + import chronicles, sequtils, tables, metrics, stew/results, ../ssz/merkleization, ../state_transition, ../extras, - ../spec/[crypto, datatypes, digest, helpers], - + ../spec/[crypto, datatypes, digest, helpers, signatures], block_pools_types, candidate_chains export results @@ -22,8 +23,8 @@ export results # "quarantined" network blocks # pass the firewall and be stored in the blockpool -logScope: topics = "clearblk" -{.push raises: [Defect].} +logScope: + topics = "clearance" func getOrResolve*(dag: CandidateChains, quarantine: var Quarantine, root: Eth2Digest): BlockRef = ## Fetch a block ref, or nil if not found (will be added to list of @@ -259,6 +260,10 @@ proc isValidBeaconBlock*( dag: CandidateChains, quarantine: var Quarantine, signed_beacon_block: SignedBeaconBlock, current_slot: Slot, flags: UpdateFlags): bool = + logScope: + topics = "clearance valid_blck" + received_block = shortLog(signed_beacon_block.message) + # In general, checks are ordered from cheap to expensive. Especially, crypto # verification could be quite a bit more expensive than the rest. This is an # externally easy-to-invoke function by tossing network packets at the node. @@ -269,9 +274,8 @@ proc isValidBeaconBlock*( # TODO using +1 here while this is being sorted - should queue these until # they're within the DISPARITY limit if not (signed_beacon_block.message.slot <= current_slot + 1): - debug "isValidBeaconBlock: block is from a future slot", - signed_beacon_block_message_slot = signed_beacon_block.message.slot, - current_slot = current_slot + debug "block is from a future slot", + current_slot return false # The block is from a slot greater than the latest finalized slot (with a @@ -279,7 +283,7 @@ proc isValidBeaconBlock*( # signed_beacon_block.message.slot > # compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) if not (signed_beacon_block.message.slot > dag.finalizedHead.slot): - debug "isValidBeaconBlock: block is not from a slot greater than the latest finalized slot" + debug "block is not from a slot greater than the latest finalized slot" return false # The block is the first block with valid signature received for the proposer @@ -317,11 +321,9 @@ proc isValidBeaconBlock*( signed_beacon_block.message.proposer_index and blck.message.slot == signed_beacon_block.message.slot and blck.signature.toRaw() != signed_beacon_block.signature.toRaw(): - debug "isValidBeaconBlock: block isn't first block with valid signature received for the proposer", - signed_beacon_block_message_slot = signed_beacon_block.message.slot, + debug "block isn't first block with valid signature received for the proposer", blckRef = slotBlockRef, - received_block = shortLog(signed_beacon_block.message), - existing_block = shortLog(dag.get(slotBlockRef).data.message) + existing_block = shortLog(blck.message) return false # If this block doesn't have a parent we know about, we can't/don't really @@ -342,27 +344,36 @@ proc isValidBeaconBlock*( # CandidateChains.add(...) directly, with no additional validity checks. TODO, # not specific to this, but by the pending dag keying on the htr of the # BeaconBlock, not SignedBeaconBlock, opens up certain spoofing attacks. + debug "parent unknown, putting block in quarantine" quarantine.pending[hash_tree_root(signed_beacon_block.message)] = signed_beacon_block return false # The proposer signature, signed_beacon_block.signature, is valid with # respect to the proposer_index pubkey. - let bs = - BlockSlot(blck: parent_ref, slot: dag.get(parent_ref).data.message.slot) - dag.withState(dag.tmpState, bs): - let - blockRoot = hash_tree_root(signed_beacon_block.message) - domain = get_domain(dag.headState.data.data, DOMAIN_BEACON_PROPOSER, - compute_epoch_at_slot(signed_beacon_block.message.slot)) - signing_root = compute_signing_root(blockRoot, domain) - proposer_index = signed_beacon_block.message.proposer_index + let + proposer = getProposer(dag, parent_ref, signed_beacon_block.message.slot) - if proposer_index >= dag.headState.data.data.validators.len.uint64: - return false - if not blsVerify(dag.headState.data.data.validators[proposer_index].pubkey, - signing_root.data, signed_beacon_block.signature): - debug "isValidBeaconBlock: block failed signature verification" - return false + if proposer.isNone: + notice "cannot compute proposer for message" + return false + + if proposer.get()[0] != + ValidatorIndex(signed_beacon_block.message.proposer_index): + debug "block had unexpected proposer", + expected_proposer = proposer.get()[0] + return false + + if not verify_block_signature( + dag.headState.data.data.fork, + dag.headState.data.data.genesis_validators_root, + signed_beacon_block.message.slot, + signed_beacon_block.message, + proposer.get()[1], + signed_beacon_block.signature): + debug "block failed signature verification", + signature = shortLog(signed_beacon_block.signature) + + return false true diff --git a/beacon_chain/interop.nim b/beacon_chain/interop.nim index 41e5c362f..9d23e7b01 100644 --- a/beacon_chain/interop.nim +++ b/beacon_chain/interop.nim @@ -3,7 +3,7 @@ import stew/endians2, stint, ./extras, ./ssz/merkleization, - spec/[crypto, datatypes, digest, helpers, keystore] + spec/[crypto, datatypes, digest, keystore, signatures] func get_eth1data_stub*(deposit_count: uint64, current_epoch: Epoch): Eth1Data = # https://github.com/ethereum/eth2.0-pm/blob/e596c70a19e22c7def4fd3519e20ae4022349390/interop/mocked_eth1data/README.md @@ -47,9 +47,6 @@ func makeDeposit*( withdrawal_credentials: makeWithdrawalCredentials(pubkey))) if skipBLSValidation notin flags: - let domain = compute_domain(DOMAIN_DEPOSIT) - let signing_root = compute_signing_root(ret.getDepositMessage, domain) - - ret.data.signature = bls_sign(privkey, signing_root.data) + ret.data.signature = get_deposit_signature(ret.data, privkey) ret diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index d4300503b..857c16a27 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -11,7 +11,7 @@ import tables, algorithm, math, sequtils, options, json_serialization/std/sets, chronicles, ../extras, ../ssz/merkleization, - ./crypto, ./datatypes, ./digest, ./helpers, ./validator, + ./crypto, ./datatypes, ./digest, ./helpers, ./signatures, ./validator, ../../nbench/bench_lab # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_valid_merkle_branch @@ -79,19 +79,13 @@ proc process_deposit*( if index == -1: # Verify the deposit signature (proof of possession) which is not checked # by the deposit contract - - # Fork-agnostic domain since deposits are valid across forks - let domain = compute_domain(DOMAIN_DEPOSIT) - - let signing_root = compute_signing_root(deposit.getDepositMessage, domain) - if skipBLSValidation notin flags and not bls_verify( - pubkey, signing_root.data, - deposit.data.signature): - # It's ok that deposits fail - they get included in blocks regardless - # TODO spec test? - debug "Skipping deposit with invalid signature", - pubkey, signing_root, signature = deposit.data.signature - return true + if skipBLSValidation notin flags: + if not verify_deposit_signature(deposit.data): + # It's ok that deposits fail - they get included in blocks regardless + # TODO spec test? + debug "Skipping deposit with invalid signature", + deposit = shortLog(deposit.data) + return true # Add validator and balance entries state.validators.add(Validator( @@ -418,15 +412,14 @@ proc is_valid_indexed_attestation*( return false # Verify aggregate signature - let pubkeys = mapIt(indices, state.validators[it.int].pubkey) # TODO: fuse loops with blsFastAggregateVerify - let domain = state.get_domain(DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) - let signing_root = compute_signing_root(indexed_attestation.data, domain) - if skipBLSValidation notin flags and - not blsFastAggregateVerify( - pubkeys, signing_root.data, indexed_attestation.signature - ): - notice "indexed attestation: signature verification failure" - return false + if skipBLSValidation notin flags: + # TODO: fuse loops with blsFastAggregateVerify + let pubkeys = mapIt(indices, state.validators[it.int].pubkey) + if not verify_attestation_signature( + state.fork, state.genesis_validators_root, indexed_attestation.data, + pubkeys, indexed_attestation.signature): + notice "indexed attestation: signature verification failure" + return false true diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index cb935519e..ca7172147 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -99,16 +99,6 @@ func toPubKey*(privkey: ValidatorPrivKey): ValidatorPubKey = else: privkey.getKey -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures -func aggregate*[T](values: openarray[ValidatorSig]): ValidatorSig = - ## Aggregate arrays of sequences of Validator Signatures - ## This assumes that they are real signatures - - result = BlsValue[T](kind: Real, blsValue: values[0].BlsValue) - - for i in 1 ..< values.len: - result.blsValue.aggregate(values[i].blsValue) - func aggregate*(x: var ValidatorSig, other: ValidatorSig) = ## Aggregate 2 Validator Signatures ## This assumes that they are real signatures @@ -143,13 +133,13 @@ func blsVerify*( # return true pubkey.blsValue.verify(message, signature.blsValue) -func blsSign*(privkey: ValidatorPrivKey, message: openarray[byte]): ValidatorSig = +func blsSign*(privkey: ValidatorPrivKey, message: openArray[byte]): ValidatorSig = ## Computes a signature from a secret key and a message ValidatorSig(kind: Real, blsValue: SecretKey(privkey).sign(message)) -func blsFastAggregateVerify*[T: byte|char]( - publicKeys: openarray[ValidatorPubKey], - message: openarray[T], +func blsFastAggregateVerify*( + publicKeys: openArray[ValidatorPubKey], + message: openArray[byte], signature: ValidatorSig ): bool = ## Verify the aggregate of multiple signatures on the same message @@ -177,7 +167,8 @@ func blsFastAggregateVerify*[T: byte|char]( if pubkey.kind != Real: return false unwrapped.add pubkey.blsValue - return fastAggregateVerify(unwrapped, message, signature.blsValue) + + fastAggregateVerify(unwrapped, message, signature.blsValue) proc newKeyPair*(): BlsResult[tuple[pub: ValidatorPubKey, priv: ValidatorPrivKey]] = ## Generates a new public-private keypair @@ -230,14 +221,14 @@ func toRaw*(x: BlsValue): auto = func toHex*(x: BlsCurveType): string = toHex(toRaw(x)) -func fromRaw*(T: type ValidatorPrivKey, bytes: openarray[byte]): BlsResult[T] = +func fromRaw*(T: type ValidatorPrivKey, bytes: openArray[byte]): BlsResult[T] = var val: SecretKey if val.fromBytes(bytes): ok ValidatorPrivKey(val) else: err "bls: invalid private key" -func fromRaw*[N, T](BT: type BlsValue[N, T], bytes: openarray[byte]): BlsResult[BT] = +func fromRaw*[N, T](BT: type BlsValue[N, T], bytes: openArray[byte]): BlsResult[BT] = # This is a workaround, so that we can deserialize the serialization of a # default-initialized BlsValue without raising an exception when defined(ssz_testing): @@ -294,7 +285,7 @@ proc readValue*(reader: var JsonReader, value: var ValidatorPrivKey) {. inline, raises: [Exception].} = value = ValidatorPrivKey.fromHex(reader.readValue(string)).tryGet() -template fromSszBytes*(T: type BlsValue, bytes: openarray[byte]): auto = +template fromSszBytes*(T: type BlsValue, bytes: openArray[byte]): auto = let v = fromRaw(T, bytes) if v.isErr: raise newException(MalformedSszError, $v.error) @@ -350,10 +341,9 @@ proc getRandomBytes*(n: Natural): seq[byte] if randomBytes(result) != result.len: raise newException(RandomSourceDepleted, "Failed to generate random bytes") -proc getRandomBytesOrPanic*(output: var openarray[byte]) = +proc getRandomBytesOrPanic*(output: var openArray[byte]) = doAssert randomBytes(output) == output.len proc getRandomBytesOrPanic*(n: Natural): seq[byte] = result = newSeq[byte](n) getRandomBytesOrPanic(result) - diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index be492e861..c2ef685c7 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -213,7 +213,7 @@ type DepositData* = object pubkey*: ValidatorPubKey withdrawal_credentials*: Eth2Digest - amount*: uint64 + amount*: Gwei signature*: ValidatorSig # Signing over DepositMessage # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#voluntaryexit @@ -622,6 +622,14 @@ func shortLog*(v: SignedBeaconBlock): auto = signature: shortLog(v.signature) ) +func shortLog*(v: DepositData): auto = + ( + pubkey: shortLog(v.pubkey), + withdrawal_credentials: shortlog(v.withdrawal_credentials), + amount: v.amount, + signature: shortLog(v.signature) + ) + func shortLog*(v: AttestationData): auto = ( slot: shortLog(v.slot), diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 76f1c8bba..167fd4867 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -10,7 +10,7 @@ import stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros, eth/keyfile/uuid, blscurve, nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], - datatypes, crypto, digest, helpers + ./datatypes, ./crypto, ./digest, ./signatures export results @@ -396,9 +396,6 @@ proc prepareDeposit*(credentials: Credentials, pubkey: signingPubKey, withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))) - let domain = compute_domain(DOMAIN_DEPOSIT) - let signing_root = compute_signing_root(ret.getDepositMessage, domain) + ret.data.signature = get_deposit_signature(ret.data, credentials.signingKey) - ret.data.signature = bls_sign(credentials.signingKey, signing_root.data) ret - diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim new file mode 100644 index 000000000..7443825ea --- /dev/null +++ b/beacon_chain/spec/signatures.nim @@ -0,0 +1,137 @@ +# beacon_chain +# Copyright (c) 2018-2020 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [Defect].} + +import + ./crypto, ./digest, ./datatypes, ./helpers, ../ssz/merkleization + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregation-selection +func get_slot_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, + privkey: ValidatorPrivKey): ValidatorSig = + let + epoch = compute_epoch_at_slot(slot) + domain = get_domain( + fork, DOMAIN_SELECTION_PROOF, epoch, genesis_validators_root) + signing_root = compute_signing_root(slot, domain) + + blsSign(privKey, signing_root.data) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal +func get_epoch_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, epoch: Epoch, + privkey: ValidatorPrivKey): ValidatorSig = + let + domain = get_domain(fork, DOMAIN_RANDAO, epoch, genesis_validators_root) + signing_root = compute_signing_root(epoch, domain) + + blsSign(privKey, signing_root.data) + +func verify_epoch_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, epoch: Epoch, + pubkey: ValidatorPubKey, signature: ValidatorSig): bool = + let + domain = get_domain(fork, DOMAIN_RANDAO, epoch, genesis_validators_root) + signing_root = compute_signing_root(epoch, domain) + + blsVerify(pubkey, signing_root.data, signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#signature +func get_block_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, + root: Eth2Digest, privkey: ValidatorPrivKey): ValidatorSig = + let + epoch = compute_epoch_at_slot(slot) + domain = get_domain( + fork, DOMAIN_BEACON_PROPOSER, epoch, genesis_validators_root) + signing_root = compute_signing_root(root, domain) + + blsSign(privKey, signing_root.data) + +func verify_block_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, + blck: Eth2Digest | BeaconBlock | BeaconBlockHeader, pubkey: ValidatorPubKey, + signature: ValidatorSig): bool = + let + epoch = compute_epoch_at_slot(slot) + domain = get_domain( + fork, DOMAIN_BEACON_PROPOSER, epoch, genesis_validators_root) + signing_root = compute_signing_root(blck, domain) + + blsVerify(pubKey, signing_root.data, signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-aggregate +func get_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth2Digest, + aggregate_and_proof: AggregateAndProof, + privKey: ValidatorPrivKey): ValidatorSig = + let + epoch = compute_epoch_at_slot(aggregate_and_proof.aggregate.data.slot) + domain = get_domain( + fork, DOMAIN_AGGREGATE_AND_PROOF, epoch, genesis_validators_root) + signing_root = compute_signing_root(aggregate_and_proof, domain) + + blsSign(privKey, signing_root.data) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregate-signature +func get_attestation_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, + attestation_data: AttestationData, + privkey: ValidatorPrivKey): ValidatorSig = + let + epoch = attestation_data.target.epoch + domain = get_domain( + fork, DOMAIN_BEACON_ATTESTER, epoch, genesis_validators_root) + signing_root = compute_signing_root(attestation_data, domain) + + blsSign(privKey, signing_root.data) + +func verify_attestation_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, + attestation_data: AttestationData, + pubkeys: openArray[ValidatorPubKey], + signature: ValidatorSig): bool = + let + epoch = attestation_data.target.epoch + domain = get_domain( + fork, DOMAIN_BEACON_ATTESTER, epoch, genesis_validators_root) + signing_root = compute_signing_root(attestation_data, domain) + + blsFastAggregateVerify(pubkeys, signing_root.data, signature) + +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#deposits +func get_deposit_signature*( + deposit: DepositData, + privkey: ValidatorPrivKey): ValidatorSig = + + let + deposit_message = deposit.getDepositMessage() + # Fork-agnostic domain since deposits are valid across forks + domain = compute_domain(DOMAIN_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + + blsSign(privKey, signing_root.data) + +func verify_deposit_signature*(deposit: DepositData): bool = + let + deposit_message = deposit.getDepositMessage() + # Fork-agnostic domain since deposits are valid across forks + domain = compute_domain(DOMAIN_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + + blsVerify(deposit.pubkey, signing_root.data, deposit.signature) + +func verify_voluntary_exit_signature*( + fork: Fork, genesis_validators_root: Eth2Digest, + voluntary_exit: VoluntaryExit, + pubkey: ValidatorPubKey, signature: ValidatorSig): bool = + let + domain = get_domain( + fork, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch, genesis_validators_root) + signing_root = compute_signing_root(voluntary_exit, domain) + + blsVerify(pubkey, signing_root.data, signature) diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 184f41080..9455bece1 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -32,7 +32,8 @@ import algorithm, collections/sets, chronicles, options, sequtils, sets, ../extras, ../ssz/merkleization, metrics, - beaconstate, crypto, datatypes, digest, helpers, validator, + ./beaconstate, ./crypto, ./datatypes, ./digest, ./helpers, ./validator, + ./signatures, ../../nbench/bench_lab # https://github.com/ethereum/eth2.0-metrics/blob/master/metrics.md#additional-metrics @@ -104,7 +105,6 @@ proc process_randao( state: var BeaconState, body: BeaconBlockBody, flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.}= let - epoch = state.get_current_epoch() proposer_index = get_beacon_proposer_index(state, stateCache) if proposer_index.isNone: @@ -112,14 +112,17 @@ proc process_randao( return false # Verify RANDAO reveal - let proposer = addr state.validators[proposer_index.get] + let + epoch = state.get_current_epoch() - let signing_root = compute_signing_root( - epoch, get_domain(state, DOMAIN_RANDAO, get_current_epoch(state))) if skipBLSValidation notin flags: - if not blsVerify(proposer.pubkey, signing_root.data, body.randao_reveal): - notice "Randao mismatch", proposer_pubkey = shortLog(proposer.pubkey), - message = epoch, + let proposer_pubkey = state.validators[proposer_index.get].pubkey + + if not verify_epoch_signature( + state.fork, state.genesis_validators_root, epoch, proposer_pubkey, + body.randao_reveal): + notice "Randao mismatch", proposer_pubkey = shortLog(proposer_pubkey), + epoch, signature = shortLog(body.randao_reveal), slot = state.slot return false @@ -187,12 +190,9 @@ proc process_proposer_slashing*( if skipBlsValidation notin flags: for i, signed_header in [proposer_slashing.signed_header_1, proposer_slashing.signed_header_2]: - let domain = get_domain( - state, DOMAIN_BEACON_PROPOSER, - compute_epoch_at_slot(signed_header.message.slot) - ) - let signing_root = compute_signing_root(signed_header.message, domain) - if not blsVerify(proposer.pubkey, signing_root.data, signed_header.signature): + if not verify_block_signature( + state.fork, state.genesis_validators_root, signed_header.message.slot, + signed_header.message, proposer.pubkey, signed_header.signature): notice "Proposer slashing: invalid signature", signature_index = i return false @@ -260,7 +260,7 @@ proc process_voluntary_exit*( let voluntary_exit = signed_voluntary_exit.message # Not in spec. Check that validator_index is in range - if voluntary_exit.validator_index.int >= state.validators.len: + if voluntary_exit.validator_index >= state.validators.len.uint64: notice "Exit: invalid validator index", index = voluntary_exit.validator_index, num_validators = state.validators.len @@ -298,9 +298,9 @@ proc process_voluntary_exit*( # Verify signature if skipBlsValidation notin flags: - let domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) - let signing_root = compute_signing_root(voluntary_exit, domain) - if not bls_verify(validator.pubkey, signing_root.data, signed_voluntary_exit.signature): + if not verify_voluntary_exit_signature( + state.fork, state.genesis_validators_root, voluntary_exit, + validator.pubkey, signed_voluntary_exit.signature): notice "Exit: invalid signature" return false @@ -393,61 +393,3 @@ proc process_block*( return false true - -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregation-selection -func get_slot_signature*( - fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, - privkey: ValidatorPrivKey): ValidatorSig = - let - domain = get_domain(fork, DOMAIN_SELECTION_PROOF, - compute_epoch_at_slot(slot), genesis_validators_root) - signing_root = compute_signing_root(slot, domain) - - blsSign(privKey, signing_root.data) - -# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal -func get_epoch_signature*( - fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, - privkey: ValidatorPrivKey): ValidatorSig = - let - domain = get_domain(fork, DOMAIN_RANDAO, compute_epoch_at_slot(slot), - genesis_validators_root) - signing_root = compute_signing_root(compute_epoch_at_slot(slot), domain) - - blsSign(privKey, signing_root.data) - -# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#signature -func get_block_signature*( - fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, - root: Eth2Digest, privkey: ValidatorPrivKey): ValidatorSig = - let - domain = get_domain(fork, DOMAIN_BEACON_PROPOSER, - compute_epoch_at_slot(slot), genesis_validators_root) - signing_root = compute_signing_root(root, domain) - - blsSign(privKey, signing_root.data) - -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#broadcast-aggregate -func get_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth2Digest, - aggregate_and_proof: AggregateAndProof, - privKey: ValidatorPrivKey): ValidatorSig = - let - aggregate = aggregate_and_proof.aggregate - domain = get_domain(fork, DOMAIN_AGGREGATE_AND_PROOF, - compute_epoch_at_slot(aggregate.data.slot), - genesis_validators_root) - signing_root = compute_signing_root(aggregate_and_proof, domain) - - return blsSign(privKey, signing_root.data) - -# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregate-signature -func get_attestation_signature*( - fork: Fork, genesis_validators_root: Eth2Digest, attestation: AttestationData, - privkey: ValidatorPrivKey): ValidatorSig = - let - attestationRoot = hash_tree_root(attestation) - domain = get_domain(fork, DOMAIN_BEACON_ATTESTER, - attestation.target.epoch, genesis_validators_root) - signing_root = compute_signing_root(attestationRoot, domain) - - blsSign(privKey, signing_root.data) diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index 277c4465f..91016791f 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -32,7 +32,7 @@ import chronicles, stew/results, ./extras, ./ssz/merkleization, metrics, - ./spec/[datatypes, crypto, digest, helpers, validator], + ./spec/[datatypes, crypto, digest, helpers, signatures, validator], ./spec/[state_transition_block, state_transition_epoch], ../nbench/bench_lab @@ -64,23 +64,20 @@ func get_epoch_validator_count(state: BeaconState): int64 {.nbench.} = # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#beacon-chain-state-transition-function proc verify_block_signature*( - state: BeaconState, signedBlock: SignedBeaconBlock): bool {.nbench.} = - if signedBlock.message.proposer_index >= state.validators.len.uint64: + state: BeaconState, signed_block: SignedBeaconBlock): bool {.nbench.} = + let + proposer_index = signed_block.message.proposer_index + if proposer_index >= state.validators.len.uint64: notice "Invalid proposer index in block", - blck = shortLog(signedBlock.message) + blck = shortLog(signed_block.message) return false - let - proposer = state.validators[signedBlock.message.proposer_index] - domain = get_domain( - state, DOMAIN_BEACON_PROPOSER, - compute_epoch_at_slot(signedBlock.message.slot)) - signing_root = compute_signing_root(signedBlock.message, domain) - - if not bls_verify(proposer.pubKey, signing_root.data, signedBlock.signature): + if not verify_block_signature( + state.fork, state.genesis_validators_root, signed_block.message.slot, + signed_block.message, state.validators[proposer_index].pubkey, + signed_block.signature): notice "Block: signature verification failed", - blck = shortLog(signedBlock), - signingRoot = shortLog(signing_root) + blck = shortLog(signedBlock) return false true diff --git a/beacon_chain/validator_pool.nim b/beacon_chain/validator_pool.nim index a36f7b0a0..43ece331c 100644 --- a/beacon_chain/validator_pool.nim +++ b/beacon_chain/validator_pool.nim @@ -1,7 +1,7 @@ import tables, chronos, chronicles, - spec/[datatypes, crypto, digest, state_transition_block], + spec/[datatypes, crypto, digest, signatures, helpers], beacon_node_types func init*(T: type ValidatorPool): T = @@ -83,7 +83,8 @@ proc signAggregateAndProof*(v: AttachedValidator, # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal func genRandaoReveal*(k: ValidatorPrivKey, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot): ValidatorSig = - get_epoch_signature(fork, genesis_validators_root, slot, k) + get_epoch_signature( + fork, genesis_validators_root, slot.compute_epoch_at_slot, k) func genRandaoReveal*(v: AttachedValidator, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot): ValidatorSig = diff --git a/research/block_sim.nim b/research/block_sim.nim index 51219c5f8..d69cc2288 100644 --- a/research/block_sim.nim +++ b/research/block_sim.nim @@ -20,8 +20,7 @@ import options, random, tables, ../tests/[testblockutil], ../beacon_chain/spec/[ - beaconstate, crypto, datatypes, digest, helpers, validator, - state_transition_block], + beaconstate, crypto, datatypes, digest, helpers, validator, signatures], ../beacon_chain/[ attestation_pool, block_pool, beacon_node_types, beacon_chain_db, interop, state_transition, validator_pool], diff --git a/tests/mocking/mock_attestations.nim b/tests/mocking/mock_attestations.nim index 18c13b653..44e337106 100644 --- a/tests/mocking/mock_attestations.nim +++ b/tests/mocking/mock_attestations.nim @@ -13,7 +13,7 @@ import sets, # Specs ../../beacon_chain/spec/[datatypes, beaconstate, helpers, validator, crypto, - state_transition_block], + signatures], # Internals ../../beacon_chain/[ssz, extras, state_transition], # Mocking procs diff --git a/tests/mocking/mock_blocks.nim b/tests/mocking/mock_blocks.nim index aa92d8bf2..3cdb48d2c 100644 --- a/tests/mocking/mock_blocks.nim +++ b/tests/mocking/mock_blocks.nim @@ -8,7 +8,7 @@ import options, # Specs - ../../beacon_chain/spec/[datatypes, validator, state_transition_block], + ../../beacon_chain/spec/[datatypes, helpers, signatures, validator], # Internals ../../beacon_chain/[ssz, extras], # Mock helpers @@ -27,7 +27,8 @@ proc signMockBlockImpl( let privkey = MockPrivKeys[signedBlock.message.proposer_index] signedBlock.message.body.randao_reveal = get_epoch_signature( - state.fork, state.genesis_validators_root, block_slot, privkey) + state.fork, state.genesis_validators_root, block_slot.compute_epoch_at_slot, + privkey) signedBlock.signature = get_block_signature( state.fork, state.genesis_validators_root, block_slot, hash_tree_root(signedBlock.message), privkey) diff --git a/tests/mocking/mock_deposits.nim b/tests/mocking/mock_deposits.nim index 5113aef4e..b6889dec9 100644 --- a/tests/mocking/mock_deposits.nim +++ b/tests/mocking/mock_deposits.nim @@ -12,88 +12,34 @@ import # Standard library math, random, # Specs - ../../beacon_chain/spec/[datatypes, crypto, helpers, digest], + ../../beacon_chain/spec/[datatypes, crypto, digest, keystore, signatures], # Internals ../../beacon_chain/[ssz, extras, merkle_minimal], # Mocking procs ./mock_validator_keys -func signMockDepositData( - deposit_data: var DepositData, - privkey: ValidatorPrivKey - ) = - # No state --> Genesis - let domain = compute_domain( - DOMAIN_DEPOSIT, - Version(GENESIS_FORK_VERSION) - ) - let signing_root = compute_signing_root( - deposit_data.getDepositMessage(), - domain - ) - deposit_data.signature = blsSign( - privkey, - signing_root.data - ) - -func signMockDepositData( - deposit_data: var DepositData, - privkey: ValidatorPrivKey, - state: BeaconState - ) = - let domain = compute_domain( - DOMAIN_DEPOSIT, - Version(GENESIS_FORK_VERSION) - ) - let signing_root = compute_signing_root( - deposit_data.getDepositMessage(), - domain - ) - deposit_data.signature = blsSign( - privkey, - signing_root.data - ) - func mockDepositData( - deposit_data: var DepositData, pubkey: ValidatorPubKey, amount: uint64, - # withdrawal_credentials: Eth2Digest - ) = - deposit_data.pubkey = pubkey - deposit_data.amount = amount - + ): DepositData = # Insecurely use pubkey as withdrawal key - deposit_data.withdrawal_credentials.data[0] = byte BLS_WITHDRAWAL_PREFIX - deposit_data.withdrawal_credentials.data[1..^1] = pubkey.toRaw() - .eth2hash() - .data - .toOpenArray(1, 31) + DepositData( + pubkey: pubkey, + withdrawal_credentials: makeWithdrawalCredentials(pubkey), + amount: amount, + ) func mockDepositData( - deposit_data: var DepositData, pubkey: ValidatorPubKey, privkey: ValidatorPrivKey, amount: uint64, # withdrawal_credentials: Eth2Digest, flags: UpdateFlags = {} - ) = - mockDepositData(deposit_data, pubkey, amount) + ): DepositData = + var ret = mockDepositData(pubkey, amount) if skipBlsValidation notin flags: - signMockDepositData(deposit_data, privkey) - -func mockDepositData( - deposit_data: var DepositData, - pubkey: ValidatorPubKey, - privkey: ValidatorPrivKey, - amount: uint64, - # withdrawal_credentials: Eth2Digest, - state: BeaconState, - flags: UpdateFlags = {} - ) = - mockDepositData(deposit_data, pubkey, amount) - if skipBlsValidation notin flags: - signMockDepositData(deposit_data, privkey, state) + ret.signature = get_deposit_signature(ret, privkey) + ret template mockGenesisDepositsImpl( result: seq[Deposit], @@ -115,11 +61,7 @@ template mockGenesisDepositsImpl( updateAmount # DepositData - mockDepositData( - result[valIdx].data, - MockPubKeys[valIdx], - amount - ) + result[valIdx].data = mockDepositData(MockPubKeys[valIdx], amount) else: # With signing var depositsDataHash: seq[Eth2Digest] var depositsData: seq[DepositData] @@ -132,13 +74,8 @@ template mockGenesisDepositsImpl( updateAmount # DepositData - mockDepositData( - result[valIdx].data, - MockPubKeys[valIdx], - MockPrivKeys[valIdx], - amount, - flags - ) + result[valIdx].data = mockDepositData( + MockPubKeys[valIdx], MockPrivKeys[valIdx], amount, flags) depositsData.add result[valIdx].data depositsDataHash.add hash_tree_root(result[valIdx].data) @@ -193,8 +130,7 @@ proc mockUpdateStateForNewDeposit*( # TODO withdrawal credentials - mockDepositData( - result.data, + result.data = mockDepositData( MockPubKeys[validator_index], MockPrivKeys[validator_index], amount, diff --git a/tests/testblockutil.nim b/tests/testblockutil.nim index f9aeb6b7c..a32e83dee 100644 --- a/tests/testblockutil.nim +++ b/tests/testblockutil.nim @@ -12,7 +12,7 @@ import ../beacon_chain/ssz/merkleization, state_transition, validator_pool], ../beacon_chain/spec/[beaconstate, crypto, datatypes, digest, - helpers, validator, state_transition_block] + helpers, validator, signatures] func makeFakeValidatorPrivKey(i: int): ValidatorPrivKey = # 0 is not a valid BLS private key - 1000 helps interop with rust BLS library, @@ -44,7 +44,6 @@ func makeDeposit(i: int, flags: UpdateFlags): Deposit = privkey = makeFakeValidatorPrivKey(i) pubkey = privkey.toPubKey() withdrawal_credentials = makeFakeHash(i) - domain = compute_domain(DOMAIN_DEPOSIT, Version(GENESIS_FORK_VERSION)) result = Deposit( data: DepositData( @@ -55,8 +54,7 @@ func makeDeposit(i: int, flags: UpdateFlags): Deposit = ) if skipBLSValidation notin flags: - let signing_root = compute_signing_root(result.getDepositMessage, domain) - result.data.signature = bls_sign(privkey, signing_root.data) + result.data.signature = get_deposit_signature(result.data, privkey) proc makeInitialDeposits*( n = SLOTS_PER_EPOCH, flags: UpdateFlags = {}): seq[Deposit] = From 60176b8cc1c5aebb2376f008c4de1f3d1e27a086 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 16 Jun 2020 09:46:00 +0200 Subject: [PATCH 47/70] Revert "Dual headed fork choice (#1163)" (#1181) This reverts commit 090f06614a1688205690a8a749b156b5664874ae. --- .gitignore | 2 +- beacon_chain/attestation_pool.nim | 102 +---------------------- beacon_chain/beacon_node.nim | 5 +- beacon_chain/beacon_node_common.nim | 3 - beacon_chain/beacon_node_types.nim | 6 +- beacon_chain/fork_choice/fork_choice.nim | 8 +- tests/test_attestation_pool.nim | 10 +-- 7 files changed, 15 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index da6ab1c8f..c73c8da16 100644 --- a/.gitignore +++ b/.gitignore @@ -32,9 +32,9 @@ build/ *.sqlite3 /local_testnet_data*/ -/local_testnet*_data*/ # Prometheus db /data # Grafana dashboards /docker/*.json + diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index f305299b0..aff0841fe 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -23,23 +23,10 @@ func init*(T: type AttestationPool, blockPool: BlockPool): T = # TODO blockPool is only used when resolving orphaned attestations - it should # probably be removed as a dependency of AttestationPool (or some other # smart refactoring) - - # TODO: In tests, on blockpool.init the finalized root - # from the `headState` and `justifiedState` is zero - let forkChoice = initForkChoice( - finalized_block_slot = default(Slot), # This is unnecessary for fork choice but may help external components - finalized_block_state_root = default(Eth2Digest), # This is unnecessary for fork choice but may help external components - justified_epoch = blockPool.headState.data.data.current_justified_checkpoint.epoch, - finalized_epoch = blockPool.headState.data.data.finalized_checkpoint.epoch, - # finalized_root = blockPool.headState.data.data.finalized_checkpoint.root - finalized_root = blockPool.finalizedHead.blck.root - ).get() - T( mapSlotsToAttestations: initDeque[AttestationsSeen](), blockPool: blockPool, unresolved: initTable[Eth2Digest, UnresolvedAttestation](), - forkChoice_v2: forkChoice ) proc combine*(tgt: var Attestation, src: Attestation, flags: UpdateFlags) = @@ -121,21 +108,13 @@ proc slotIndex( func updateLatestVotes( pool: var AttestationPool, state: BeaconState, attestationSlot: Slot, participants: seq[ValidatorIndex], blck: BlockRef) = - - # ForkChoice v2 - let target_epoch = compute_epoch_at_slot(attestationSlot) - for validator in participants: - # ForkChoice v1 let pubKey = state.validators[validator].pubkey current = pool.latestAttestations.getOrDefault(pubKey) if current.isNil or current.slot < attestationSlot: pool.latestAttestations[pubKey] = blck - # ForkChoice v2 - pool.forkChoice_v2.process_attestation(validator, blck.root, target_epoch) - func get_attesting_indices_seq(state: BeaconState, attestation_data: AttestationData, bits: CommitteeValidatorsBits, @@ -283,34 +262,6 @@ proc add*(pool: var AttestationPool, attestation: Attestation) = pool.addResolved(blck, attestation) -proc addForkChoice_v2*(pool: var AttestationPool, blck: BlockRef) = - ## Add a verified block to the fork choice context - ## The current justifiedState of the block pool is used as reference - - # TODO: add(BlockPool, blockRoot: Eth2Digest, SignedBeaconBlock): BlockRef - # should ideally return the justified_epoch and finalized_epoch - # so that we can pass them directly to this proc without having to - # redo "updateStateData" - # - # In any case, `updateStateData` should shortcut - # to `getStateDataCached` - - updateStateData( - pool.blockPool, - pool.blockPool.tmpState, - BlockSlot(blck: blck, slot: blck.slot) - ) - - let blockData = pool.blockPool.get(blck) - pool.forkChoice_v2.process_block( - slot = blck.slot, - block_root = blck.root, - parent_root = if not blck.parent.isNil: blck.parent.root else: default(Eth2Digest), - state_root = default(Eth2Digest), # This is unnecessary for fork choice but may help external components - justified_epoch = pool.blockPool.tmpState.data.data.current_justified_checkpoint.epoch, - finalized_epoch = pool.blockPool.tmpState.data.data.finalized_checkpoint.epoch, - ).get() - proc getAttestationsForSlot*(pool: AttestationPool, newBlockSlot: Slot): Option[AttestationsSeen] = if newBlockSlot < (GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY): @@ -445,10 +396,7 @@ proc resolve*(pool: var AttestationPool) = for a in resolved: pool.addResolved(a.blck, a.attestation) -# Fork choice v1 -# --------------------------------------------------------------- - -func latestAttestation( +func latestAttestation*( pool: AttestationPool, pubKey: ValidatorPubKey): BlockRef = pool.latestAttestations.getOrDefault(pubKey) @@ -456,7 +404,7 @@ func latestAttestation( # The structure of this code differs from the spec since we use a different # strategy for storing states and justification points - it should nonetheless # be close in terms of functionality. -func lmdGhost( +func lmdGhost*( pool: AttestationPool, start_state: BeaconState, start_block: BlockRef): BlockRef = # TODO: a Fenwick Tree datastructure to keep track of cumulated votes @@ -507,7 +455,7 @@ func lmdGhost( winCount = candCount head = winner -proc selectHead_v1(pool: AttestationPool): BlockRef = +proc selectHead*(pool: AttestationPool): BlockRef = let justifiedHead = pool.blockPool.latestJustifiedBlock() @@ -515,47 +463,3 @@ proc selectHead_v1(pool: AttestationPool): BlockRef = lmdGhost(pool, pool.blockPool.justifiedState.data.data, justifiedHead.blck) newHead - -# Fork choice v2 -# --------------------------------------------------------------- - -func getAttesterBalances(state: StateData): seq[Gwei] {.noInit.}= - ## Get the balances from a state - result.newSeq(state.data.data.validators.len) # zero-init - - let epoch = state.data.data.slot.compute_epoch_at_slot() - - for i in 0 ..< result.len: - # All non-active validators have a 0 balance - template validator: Validator = state.data.data.validators[i] - if validator.is_active_validator(epoch): - result[i] = validator.effective_balance - -proc selectHead_v2(pool: var AttestationPool): BlockRef = - let attesterBalances = pool.blockPool.justifiedState.getAttesterBalances() - - let newHead = pool.forkChoice_v2.find_head( - justified_epoch = pool.blockPool.justifiedState.data.data.slot.compute_epoch_at_slot(), - justified_root = pool.blockPool.head.justified.blck.root, - finalized_epoch = pool.blockPool.headState.data.data.finalized_checkpoint.epoch, - justified_state_balances = attesterBalances - ).get() - - pool.blockPool.getRef(newHead) - -proc pruneBefore*(pool: var AttestationPool, finalizedhead: BlockSlot) = - pool.forkChoice_v2.maybe_prune(finalizedHead.blck.root).get() - -# Dual-Headed Fork choice -# --------------------------------------------------------------- - -proc selectHead*(pool: var AttestationPool): BlockRef = - let head_v1 = pool.selectHead_v1() - let head_v2 = pool.selectHead_v2() - - if head_v1 != head_v2: - error "Fork choice engines in disagreement, using block from v1.", - v1_block = shortlog(head_v1), - v2_block = shortlog(head_v2) - - return head_v1 diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index b64ed47ce..6321cf4bd 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -313,10 +313,6 @@ proc storeBlock( return err(blck.error) - # Still here? This means we received a valid block and we need to add it - # to the fork choice - node.attestationPool.addForkChoice_v2(blck.get()) - # The block we received contains attestations, and we might not yet know about # all of them. Let's add them to the attestation pool - in case the block # is not yet resolved, neither will the attestations be! @@ -1138,3 +1134,4 @@ programMain: config.depositContractAddress, config.depositPrivateKey, delayGenerator) + diff --git a/beacon_chain/beacon_node_common.nim b/beacon_chain/beacon_node_common.nim index e356d50ff..f999c740d 100644 --- a/beacon_chain/beacon_node_common.nim +++ b/beacon_chain/beacon_node_common.nim @@ -68,9 +68,6 @@ proc updateHead*(node: BeaconNode): BlockRef = node.blockPool.updateHead(newHead) beacon_head_root.set newHead.root.toGaugeValue - # Cleanup the fork choice v2 if we have a finalized head - node.attestationPool.pruneBefore(node.blockPool.finalizedHead) - newHead template findIt*(s: openarray, predicate: untyped): int64 = diff --git a/beacon_chain/beacon_node_types.nim b/beacon_chain/beacon_node_types.nim index 03cf4767d..4015c3d60 100644 --- a/beacon_chain/beacon_node_types.nim +++ b/beacon_chain/beacon_node_types.nim @@ -5,8 +5,7 @@ import stew/endians2, spec/[datatypes, crypto, digest], block_pools/block_pools_types, - block_pool, # TODO: refactoring compat shim - fork_choice/fork_choice_types + block_pool # TODO: refactoring compat shim export block_pools_types @@ -75,9 +74,6 @@ type latestAttestations*: Table[ValidatorPubKey, BlockRef] ##\ ## Map that keeps track of the most recent vote of each attester - see ## fork_choice - forkChoice_v2*: ForkChoice ##\ - ## The alternative fork choice "proto_array" that will ultimately - ## replace the original one # ############################################# # diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index 7b4f828bb..c9fc6db77 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -117,7 +117,7 @@ func process_attestation*( vote.next_epoch = target_epoch {.noSideEffect.}: - trace "Integrating vote in fork choice", + info "Integrating vote in fork choice", validator_index = $validator_index, new_vote = shortlog(vote) else: @@ -129,7 +129,7 @@ func process_attestation*( ignored_block_root = shortlog(block_root), ignored_target_epoch = $target_epoch else: - trace "Ignoring double-vote for fork choice", + info "Ignoring double-vote for fork choice", validator_index = $validator_index, current_vote = shortlog(vote), ignored_block_root = shortlog(block_root), @@ -159,7 +159,7 @@ func process_block*( return err("process_block_error: " & $err) {.noSideEffect.}: - trace "Integrating block in fork choice", + info "Integrating block in fork choice", block_root = $shortlog(block_root), parent_root = $shortlog(parent_root), justified_epoch = $justified_epoch, @@ -205,7 +205,7 @@ func find_head*( return err("find_head failed: " & $ghost_err) {.noSideEffect.}: - debug "Fork choice requested", + info "Fork choice requested", justified_epoch = $justified_epoch, justified_root = shortlog(justified_root), finalized_epoch = $finalized_epoch, diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index cddac4713..36af8a135 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -33,7 +33,7 @@ suiteReport "Attestation pool processing" & preset(): check: process_slots(state.data, state.data.data.slot + 1) - pool[].addForkChoice_v2(blockPool[].tail) # Make the tail known to fork choice + # pool[].add(blockPool[].tail) # Make the tail known to fork choice timedTest "Can add and retrieve simple attestation" & preset(): var cache = get_empty_per_epoch_cache() @@ -161,7 +161,7 @@ suiteReport "Attestation pool processing" & preset(): b1Root = hash_tree_root(b1.message) b1Add = blockpool[].add(b1Root, b1)[] - pool[].addForkChoice_v2(b1Add) + # pool[].add(b1Add) - make a block known to the future fork choice let head = pool[].selectHead() check: @@ -172,7 +172,7 @@ suiteReport "Attestation pool processing" & preset(): b2Root = hash_tree_root(b2.message) b2Add = blockpool[].add(b2Root, b2)[] - pool[].addForkChoice_v2(b2Add) + # pool[].add(b2Add) - make a block known to the future fork choice let head2 = pool[].selectHead() check: @@ -185,7 +185,7 @@ suiteReport "Attestation pool processing" & preset(): b10Root = hash_tree_root(b10.message) b10Add = blockpool[].add(b10Root, b10)[] - pool[].addForkChoice_v2(b10Add) + # pool[].add(b10Add) - make a block known to the future fork choice let head = pool[].selectHead() check: @@ -202,7 +202,7 @@ suiteReport "Attestation pool processing" & preset(): state.data.data, state.data.data.slot, 1.CommitteeIndex, cache) attestation0 = makeAttestation(state.data.data, b10Root, bc1[0], cache) - pool[].addForkChoice_v2(b11Add) + # pool[].add(b11Add) - make a block known to the future fork choice pool[].add(attestation0) let head2 = pool[].selectHead() From 5c25d23ef1b5bf5b42ad7c2c331ea64577963903 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 16 Jun 2020 14:16:43 +0200 Subject: [PATCH 48/70] eth2hash -> eth2digest hash in nim is the insecure hash-map helper - might as well use `digest` consistently to mark the difference --- beacon_chain/attestation_aggregation.nim | 2 +- beacon_chain/interop.nim | 2 +- beacon_chain/spec/beaconstate.nim | 2 +- beacon_chain/spec/digest.nim | 7 +++---- beacon_chain/spec/helpers.nim | 2 +- beacon_chain/spec/keystore.nim | 2 +- beacon_chain/spec/state_transition_block.nim | 2 +- beacon_chain/spec/validator.nim | 8 ++++---- 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/beacon_chain/attestation_aggregation.nim b/beacon_chain/attestation_aggregation.nim index 67c1e0a5f..cceb30992 100644 --- a/beacon_chain/attestation_aggregation.nim +++ b/beacon_chain/attestation_aggregation.nim @@ -24,7 +24,7 @@ func is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, let committee = get_beacon_committee(state, slot, index, cache) modulo = max(1, len(committee) div TARGET_AGGREGATORS_PER_COMMITTEE).uint64 - bytes_to_int(eth2hash(slot_signature.toRaw()).data[0..7]) mod modulo == 0 + bytes_to_int(eth2digest(slot_signature.toRaw()).data[0..7]) mod modulo == 0 proc aggregate_attestations*( pool: AttestationPool, state: BeaconState, index: CommitteeIndex, diff --git a/beacon_chain/interop.nim b/beacon_chain/interop.nim index 9d23e7b01..ad7b6838c 100644 --- a/beacon_chain/interop.nim +++ b/beacon_chain/interop.nim @@ -25,7 +25,7 @@ func makeInteropPrivKey*(i: int): ValidatorPrivKey = curveOrder = "52435875175126190479447740508185965837690552500527637822603658699938581184513".parse(UInt256) - privkeyBytes = eth2hash(bytes) + privkeyBytes = eth2digest(bytes) key = (UInt256.fromBytesLE(privkeyBytes.data) mod curveOrder).toBytesBE() ValidatorPrivKey.fromRaw(key).get diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 857c16a27..4b8e66300 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -29,7 +29,7 @@ func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openarray[Eth2Digest], de else: buf[0..31] = value.data buf[32..63] = branch[i.int].data - value = eth2hash(buf) + value = eth2digest(buf) value == root # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#increase_balance diff --git a/beacon_chain/spec/digest.nim b/beacon_chain/spec/digest.nim index efda558e5..67787e6db 100644 --- a/beacon_chain/spec/digest.nim +++ b/beacon_chain/spec/digest.nim @@ -17,7 +17,7 @@ # # In our code base, to enable a smooth transition # (already did Blake2b --> Keccak256 --> SHA2-256), -# we call this function `eth2hash`, and it outputs a `Eth2Digest`. Easy to sed :) +# we call this function `eth2digest`, and it outputs a `Eth2Digest`. Easy to sed :) {.push raises: [Defect].} @@ -44,7 +44,7 @@ chronicles.formatIt Eth2Digest: # TODO: expose an in-place digest function # when hashing in loop or into a buffer # See: https://github.com/cheatfate/nimcrypto/blob/b90ba3abd/nimcrypto/sha2.nim#L570 -func eth2hash*(v: openArray[byte]): Eth2Digest {.inline.} = +func eth2digest*(v: openArray[byte]): Eth2Digest {.inline.} = # We use the init-update-finish interface to avoid # the expensive burning/clearing memory (20~30% perf) # TODO: security implication? @@ -63,8 +63,7 @@ template withEth2Hash*(body: untyped): Eth2Digest = var h {.inject.}: sha256 init(h) body - var res = finish(h) - res + finish(h) func hash*(x: Eth2Digest): Hash = ## Hash for digests for Nim hash tables diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 5c4d7efad..879fdd46e 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -212,4 +212,4 @@ func get_seed*(state: BeaconState, epoch: Epoch, domain_type: DomainType): Eth2D seed_input[12..43] = get_randao_mix(state, # Avoid underflow epoch + EPOCHS_PER_HISTORICAL_VECTOR - MIN_SEED_LOOKAHEAD - 1).data - eth2hash(seed_input) + eth2digest(seed_input) diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 167fd4867..65472c6ca 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -379,7 +379,7 @@ proc generateCredentials*(entropy: openarray[byte] = @[], # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest = - var bytes = eth2hash(k.toRaw()) + var bytes = eth2digest(k.toRaw()) bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8 bytes diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 9455bece1..f7c06cc20 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -130,7 +130,7 @@ proc process_randao( # Mix it in let mix = get_randao_mix(state, epoch) - rr = eth2hash(body.randao_reveal.toRaw()).data + rr = eth2digest(body.randao_reveal.toRaw()).data state.randao_mixes[epoch mod EPOCHS_PER_HISTORICAL_VECTOR].data = mix.data xor rr diff --git a/beacon_chain/spec/validator.nim b/beacon_chain/spec/validator.nim index fbccceb54..a39803791 100644 --- a/beacon_chain/spec/validator.nim +++ b/beacon_chain/spec/validator.nim @@ -52,14 +52,14 @@ func get_shuffled_seq*(seed: Eth2Digest, source_buffer[32] = round_bytes1 # Only one pivot per round. - let pivot = bytes_to_int(eth2hash(pivot_buffer).data.toOpenArray(0, 7)) mod list_size + let pivot = bytes_to_int(eth2digest(pivot_buffer).data.toOpenArray(0, 7)) mod list_size ## Only need to run, per round, position div 256 hashes, so precalculate ## them. This consumes memory, but for low-memory devices, it's possible ## to mitigate by some light LRU caching and similar. for reduced_position in 0 ..< sources.len: source_buffer[33..36] = int_to_bytes4(reduced_position.uint64) - sources[reduced_position] = eth2hash(source_buffer) + sources[reduced_position] = eth2digest(source_buffer) ## Iterate over all the indices. This was in get_permuted_index, but large ## efficiency gains exist in caching and re-using data. @@ -185,7 +185,7 @@ func compute_proposer_index(state: BeaconState, indices: seq[ValidatorIndex], buffer[32..39] = int_to_bytes8(i.uint64 div 32) let candidate_index = shuffled_seq[(i.uint64 mod seq_len).int] - random_byte = (eth2hash(buffer).data)[i mod 32] + random_byte = (eth2digest(buffer).data)[i mod 32] effective_balance = state.validators[candidate_index].effective_balance if effective_balance * MAX_RANDOM_BYTE >= @@ -217,7 +217,7 @@ func get_beacon_proposer_index*(state: BeaconState, cache: var StateCache, slot: try: let - seed = eth2hash(buffer) + seed = eth2digest(buffer) indices = sorted(cache.shuffled_active_validator_indices[epoch], system.cmp) From a25bc025d1824bfe184e4c714038cf7a7488c880 Mon Sep 17 00:00:00 2001 From: kdeme Date: Tue, 16 Jun 2020 13:46:19 +0200 Subject: [PATCH 49/70] Start discovery after starting libp2p switch --- beacon_chain/eth2_network.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 401691564..cbfd9e3d0 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -838,8 +838,8 @@ proc start*(node: Eth2Node) {.async.} = for i in 0 ..< ConcurrentConnections: node.connWorkers.add connectWorker(node) - node.discovery.start() node.libp2pTransportLoops = await node.switch.start() + node.discovery.start() node.discoveryLoop = node.runDiscoveryLoop() traceAsyncErrors node.discoveryLoop From 49e9167b2806b7d7c9498978058f44d4e9a62608 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 16 Jun 2020 10:49:32 +0200 Subject: [PATCH 50/70] clean up dump feature * don't write blocks that get added to database * don't write states * write to folders * add state dumping feature to `ncli_db` to get any known state from the database --- beacon_chain/beacon_node.nim | 47 ++++++++++++++------------ beacon_chain/block_pool.nim | 9 ++--- beacon_chain/block_pools/clearance.nim | 18 +++++----- beacon_chain/conf.nim | 19 +++++++++++ beacon_chain/validator_duties.nim | 7 ++-- ncli/ncli_db.nim | 34 +++++++++++++++++++ 6 files changed, 95 insertions(+), 39 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 6321cf4bd..40eb827b8 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -287,6 +287,22 @@ proc onAttestation(node: BeaconNode, attestation: Attestation) = node.attestationPool.add(attestation) +proc dumpBlock[T]( + node: BeaconNode, signedBlock: SignedBeaconBlock, + res: Result[T, BlockError]) = + if node.config.dumpEnabled and res.isErr: + case res.error + of Invalid: + dump( + node.config.dumpDirInvalid, signedBlock, + hash_tree_root(signedBlock.message)) + of MissingParent: + dump( + node.config.dumpDirIncoming, signedBlock, + hash_tree_root(signedBlock.message)) + else: + discard + proc storeBlock( node: BeaconNode, signedBlock: SignedBeaconBlock): Result[void, BlockError] = let blockRoot = hash_tree_root(signedBlock.message) @@ -296,28 +312,16 @@ proc storeBlock( cat = "block_listener", pcs = "receive_block" - if node.config.dumpEnabled: - dump(node.config.dumpDir / "incoming", signedBlock, blockRoot) - beacon_blocks_received.inc() let blck = node.blockPool.add(blockRoot, signedBlock) + + node.dumpBlock(signedBlock, blck) + if blck.isErr: - if blck.error == Invalid and node.config.dumpEnabled: - dump(node.config.dumpDir / "invalid", signedBlock, blockRoot) - - let parent = node.blockPool.getRef(signedBlock.message.parent_root) - if parent != nil: - let parentBs = parent.atSlot(signedBlock.message.slot - 1) - node.blockPool.withState(node.blockPool.tmpState, parentBs): - dump(node.config.dumpDir / "invalid", hashedState, parent) - return err(blck.error) # The block we received contains attestations, and we might not yet know about - # all of them. Let's add them to the attestation pool - in case the block - # is not yet resolved, neither will the attestations be! - # But please note that we only care about recent attestations. - # TODO shouldn't add attestations if the block turns out to be invalid.. + # all of them. Let's add them to the attestation pool. let currentSlot = node.beaconClock.now.toSlot if currentSlot.afterGenesis and signedBlock.message.slot.epoch + 1 >= currentSlot.slot.epoch: @@ -770,7 +774,11 @@ proc run*(node: BeaconNode) = let (afterGenesis, slot) = node.beaconClock.now.toSlot() if not afterGenesis: return false - node.blockPool.isValidBeaconBlock(signedBlock, slot, {}) + + let blck = node.blockPool.isValidBeaconBlock(signedBlock, slot, {}) + node.dumpBlock(signedBlock, blck) + + blck.isOk installAttestationHandlers(node) @@ -1080,10 +1088,7 @@ programMain: createPidFile(config.dataDir.string / "beacon_node.pid") - if config.dumpEnabled: - createDir(config.dumpDir) - createDir(config.dumpDir / "incoming") - createDir(config.dumpDir / "invalid") + config.createDumpDirs() var node = waitFor BeaconNode.init(config) diff --git a/beacon_chain/block_pool.nim b/beacon_chain/block_pool.nim index 1368d5589..987e01857 100644 --- a/beacon_chain/block_pool.nim +++ b/beacon_chain/block_pool.nim @@ -171,7 +171,8 @@ proc updateStateData*(pool: BlockPool, state: var StateData, bs: BlockSlot) = proc loadTailState*(pool: BlockPool): StateData = loadTailState(pool.dag) -proc isValidBeaconBlock*(pool: var BlockPool, - signed_beacon_block: SignedBeaconBlock, - current_slot: Slot, flags: UpdateFlags): bool = - isValidBeaconBlock(pool.dag, pool.quarantine, signed_beacon_block, current_slot, flags) +proc isValidBeaconBlock*( + pool: var BlockPool, signed_beacon_block: SignedBeaconBlock, + current_slot: Slot, flags: UpdateFlags): Result[void, BlockError] = + isValidBeaconBlock( + pool.dag, pool.quarantine, signed_beacon_block, current_slot, flags) diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index a8f058319..364135989 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -259,7 +259,7 @@ proc add*( proc isValidBeaconBlock*( dag: CandidateChains, quarantine: var Quarantine, signed_beacon_block: SignedBeaconBlock, current_slot: Slot, - flags: UpdateFlags): bool = + flags: UpdateFlags): Result[void, BlockError] = logScope: topics = "clearance valid_blck" received_block = shortLog(signed_beacon_block.message) @@ -276,7 +276,7 @@ proc isValidBeaconBlock*( if not (signed_beacon_block.message.slot <= current_slot + 1): debug "block is from a future slot", current_slot - return false + return err(Invalid) # The block is from a slot greater than the latest finalized slot (with a # MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that @@ -284,7 +284,7 @@ proc isValidBeaconBlock*( # compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) if not (signed_beacon_block.message.slot > dag.finalizedHead.slot): debug "block is not from a slot greater than the latest finalized slot" - return false + return err(Invalid) # The block is the first block with valid signature received for the proposer # for the slot, signed_beacon_block.message.slot. @@ -324,7 +324,7 @@ proc isValidBeaconBlock*( debug "block isn't first block with valid signature received for the proposer", blckRef = slotBlockRef, existing_block = shortLog(blck.message) - return false + return err(Invalid) # If this block doesn't have a parent we know about, we can't/don't really # trace it back to a known-good state/checkpoint to verify its prevenance; @@ -347,7 +347,7 @@ proc isValidBeaconBlock*( debug "parent unknown, putting block in quarantine" quarantine.pending[hash_tree_root(signed_beacon_block.message)] = signed_beacon_block - return false + return err(MissingParent) # The proposer signature, signed_beacon_block.signature, is valid with # respect to the proposer_index pubkey. @@ -356,13 +356,13 @@ proc isValidBeaconBlock*( if proposer.isNone: notice "cannot compute proposer for message" - return false + return err(Invalid) if proposer.get()[0] != ValidatorIndex(signed_beacon_block.message.proposer_index): debug "block had unexpected proposer", expected_proposer = proposer.get()[0] - return false + return err(Invalid) if not verify_block_signature( dag.headState.data.data.fork, @@ -374,6 +374,6 @@ proc isValidBeaconBlock*( debug "block failed signature verification", signature = shortLog(signed_beacon_block.signature) - return false + return err(Invalid) - true + ok() diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 503e71c8e..cdc5a748e 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -324,6 +324,25 @@ proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string = func dumpDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "dump" +func dumpDirInvalid*(conf: BeaconNodeConf|ValidatorClientConf): string = + conf.dumpDir / "invalid" # things that failed validation + +func dumpDirIncoming*(conf: BeaconNodeConf|ValidatorClientConf): string = + conf.dumpDir / "incoming" # things that couldn't be validated (missingparent etc) + +func dumpDirOutgoing*(conf: BeaconNodeConf|ValidatorClientConf): string = + conf.dumpDir / "outgoing" # things we produced + +proc createDumpDirs*(conf: BeaconNodeConf) = + if conf.dumpEnabled: + try: + createDir(conf.dumpDirInvalid) + createDir(conf.dumpDirIncoming) + createDir(conf.dumpDirOutgoing) + except CatchableError as err: + # Dumping is mainly a debugging feature, so ignore these.. + warn "Cannot create dump directories", msg = err.msg + func validatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = string conf.validatorsDirFlag.get(InputDir(conf.dataDir / "validators")) diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 1b4a7e89b..592ffc1b1 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -122,7 +122,7 @@ proc createAndSendAttestation(node: BeaconNode, node.sendAttestation(attestation) if node.config.dumpEnabled: - dump(node.config.dumpDir, attestation.data, validator.pubKey) + dump(node.config.dumpDirOutgoing, attestation.data, validator.pubKey) info "Attestation sent", attestation = shortLog(attestation), @@ -221,10 +221,7 @@ proc proposeSignedBlock*(node: BeaconNode, cat = "consensus" if node.config.dumpEnabled: - dump(node.config.dumpDir, newBlock, newBlockRef[]) - node.blockPool.withState( - node.blockPool.tmpState, newBlockRef[].atSlot(newBlockRef[].slot)): - dump(node.config.dumpDir, hashedState, newBlockRef[]) + dump(node.config.dumpDirOutgoing, newBlock, newBlockRef[]) node.network.broadcast(node.topicBeaconBlocks, newBlock) diff --git a/ncli/ncli_db.nim b/ncli/ncli_db.nim index 37817ef8d..2a1e77386 100644 --- a/ncli/ncli_db.nim +++ b/ncli/ncli_db.nim @@ -18,6 +18,7 @@ type DbCmd* = enum bench dumpState + rewindState DbConf = object databaseDir* {. @@ -40,6 +41,15 @@ type argument desc: "State roots to save".}: seq[string] + of rewindState: + blockRoot* {. + argument + desc: "Block root".}: string + + slot* {. + argument + desc: "Slot".}: uint64 + proc cmdBench(conf: DbConf) = var timers: array[Timers, RunningStat] @@ -104,6 +114,28 @@ proc cmdDumpState(conf: DbConf) = except CatchableError as e: echo "Couldn't load ", stateRoot, ": ", e.msg +proc cmdRewindState(conf: DbConf) = + echo "Opening database..." + let + db = BeaconChainDB.init( + kvStore SqStoreRef.init(conf.databaseDir.string, "nbc").tryGet()) + + if not BlockPool.isInitialized(db): + echo "Database not initialized" + quit 1 + + echo "Initializing block pool..." + let pool = BlockPool.init(db, {}) + + let blckRef = pool.getRef(fromHex(Eth2Digest, conf.blockRoot)) + if blckRef == nil: + echo "Block not found in database" + return + + pool.withState(pool.tmpState, blckRef.atSlot(Slot(conf.slot))): + echo "Writing state..." + dump("./", hashedState, blck) + when isMainModule: let conf = DbConf.load() @@ -113,3 +145,5 @@ when isMainModule: cmdBench(conf) of dumpState: cmdDumpState(conf) + of rewindState: + cmdRewindState(conf) From 044ced53be540f3e9fffa11aa70c51557b89944a Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Tue, 16 Jun 2020 17:12:59 +0200 Subject: [PATCH 51/70] ensure SPEC_VERSION stays synchronized with actual spec tested --- tests/official/fixtures_utils.nim | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/official/fixtures_utils.nim b/tests/official/fixtures_utils.nim index ba6c2cd33..07a293c09 100644 --- a/tests/official/fixtures_utils.nim +++ b/tests/official/fixtures_utils.nim @@ -40,12 +40,9 @@ type TestSizeError* = object of ValueError const - FixturesDir* = currentSourcePath.rsplit(DirSep, 1)[0] / ".." / ".." / "vendor" / "nim-eth2-scenarios" - SszTestsDir* = - when ETH2_SPEC == "v0.12.1": - FixturesDir/"tests-v0.12.1" - else: - FixturesDir/"tests-v0.11.3" + FixturesDir* = + currentSourcePath.rsplit(DirSep, 1)[0] / ".." / ".." / "vendor" / "nim-eth2-scenarios" + SszTestsDir* = FixturesDir / "tests-v" & SPEC_VERSION proc parseTest*(path: string, Format: typedesc[Json or SSZ], T: typedesc): T = try: From f7731b1be2edec4742610a6d478f2d5fcef223df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Tue, 16 Jun 2020 22:34:34 +0200 Subject: [PATCH 52/70] state/block_sim: spec version in filename (#1171) * genesim_{const_preset}_{validators}_{SPEC_VERSION}.ssz --- research/simutils.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/research/simutils.nim b/research/simutils.nim index 5bb827718..833197026 100644 --- a/research/simutils.nim +++ b/research/simutils.nim @@ -41,7 +41,7 @@ func verifyConsensus*(state: BeaconState, attesterRatio: auto) = doAssert state.finalized_checkpoint.epoch + 2 >= current_epoch proc loadGenesis*(validators: int, validate: bool): ref HashedBeaconState = - let fn = &"genesim_{const_preset}_{validators}.ssz" + let fn = &"genesim_{const_preset}_{validators}_{SPEC_VERSION}.ssz" let res = (ref HashedBeaconState)() if fileExists(fn): res.data = SSZ.loadFile(fn, BeaconState) @@ -72,6 +72,7 @@ proc loadGenesis*(validators: int, validate: bool): ref HashedBeaconState = echo &"Saving to {fn}..." SSZ.saveFile(fn, res.data) + res proc printTimers*[Timers: enum]( From 00537b391c94570bd5b6f229d46d75d6bcbd2522 Mon Sep 17 00:00:00 2001 From: Viktor Kirilov Date: Wed, 17 Jun 2020 11:15:55 +0300 Subject: [PATCH 53/70] updated news - includes a fix for the test suite of nim-web3 --- vendor/news | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/news b/vendor/news index 1caa232d6..55d3214c5 160000 --- a/vendor/news +++ b/vendor/news @@ -1 +1 @@ -Subproject commit 1caa232d63f4607f90d5a9eb428fbe772e010d21 +Subproject commit 55d3214c57a880d31ac7542364820e07f8c8abe5 From ec85a8bdbf51e38720c77a2fb06e97ec554c367e Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Wed, 17 Jun 2020 10:25:07 +0200 Subject: [PATCH 54/70] update a dozen unchanged spec refs to v0.12.1 --- beacon_chain/spec/datatypes.nim | 4 ++-- beacon_chain/spec/presets/minimal.nim | 14 +++++++------- beacon_chain/spec/state_transition_block.nim | 2 +- beacon_chain/spec/state_transition_epoch.nim | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index c2ef685c7..610ee56f0 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -162,7 +162,7 @@ type CommitteeValidatorsBits* = BitList[MAX_VALIDATORS_PER_COMMITTEE] - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#attestation + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#attestation Attestation* = object aggregation_bits*: CommitteeValidatorsBits data*: AttestationData @@ -388,7 +388,7 @@ type message*: BeaconBlockHeader signature*: ValidatorSig - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.2/specs/phase0/validator.md#aggregateandproof + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregateandproof AggregateAndProof* = object aggregator_index*: uint64 aggregate*: Attestation diff --git a/beacon_chain/spec/presets/minimal.nim b/beacon_chain/spec/presets/minimal.nim index a6237cb98..3c6b2bf63 100644 --- a/beacon_chain/spec/presets/minimal.nim +++ b/beacon_chain/spec/presets/minimal.nim @@ -43,7 +43,7 @@ const # Gwei values # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L58 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L58 # Unchanged MIN_DEPOSIT_AMOUNT* = 2'u64^0 * 10'u64^9 @@ -53,14 +53,14 @@ const # Initial values # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L70 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L70 GENESIS_FORK_VERSION* = [0'u8, 0'u8, 0'u8, 1'u8] BLS_WITHDRAWAL_PREFIX* = 0'u8 # Time parameters # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L77 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L77 # Changed: Faster to spin up testnets, but does not give validator # reasonable warning time for genesis GENESIS_DELAY* = 300 @@ -71,8 +71,6 @@ const # Unchanged MIN_ATTESTATION_INCLUSION_DELAY* = 1 - SHARD_COMMITTEE_PERIOD* = 64 # epochs - # Changed SLOTS_PER_EPOCH* {.intdefine.} = 8 @@ -87,6 +85,8 @@ const # Unchanged MIN_VALIDATOR_WITHDRAWABILITY_DELAY* = 2'u64^8 + SHARD_COMMITTEE_PERIOD* = 64 # epochs + # Unchanged MAX_EPOCHS_PER_CROSSLINK* = 4 @@ -117,7 +117,7 @@ const # Max operations per block # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L131 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L131 MAX_PROPOSER_SLASHINGS* = 2^4 MAX_ATTESTER_SLASHINGS* = 2^1 @@ -178,7 +178,7 @@ const MAX_REVEAL_LATENESS_DECREMENT* = 128 # Max operations - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L214 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L214 MAX_CUSTODY_KEY_REVEALS* = 256 MAX_EARLY_DERIVED_SECRET_REVEALS* = 1 MAX_CUSTODY_SLASHINGS* = 1 diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index f7c06cc20..5acabbfc4 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -151,7 +151,7 @@ func is_slashable_validator(validator: Validator, epoch: Epoch): bool = (validator.activation_epoch <= epoch) and (epoch < validator.withdrawable_epoch) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#proposer-slashings +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#proposer-slashings proc process_proposer_slashing*( state: var BeaconState, proposer_slashing: ProposerSlashing, flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.}= diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index d7b50b196..b1c6b6ea4 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -53,7 +53,7 @@ declareGauge beacon_current_epoch, "Current epoch" # Spec # -------------------------------------------------------- -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_total_active_balance +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_total_active_balance func get_total_active_balance*(state: BeaconState, cache: var StateCache): Gwei = # Return the combined effective balance of the active validators. # Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei @@ -145,11 +145,11 @@ proc process_justification_and_finalization*(state: var BeaconState, ## matter -- in the next epoch, they'll be 2 epochs old, when BeaconState ## tracks current_epoch_attestations and previous_epoch_attestations only ## per - ## https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#attestations + ## https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#attestations ## and `get_matching_source_attestations(...)` via - ## https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#helper-functions-1 + ## https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#helper-functions-1 ## and - ## https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#final-updates + ## https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#final-updates ## after which the state.previous_epoch_attestations is replaced. let total_active_balance = get_total_active_balance(state, stateCache) trace "Non-attesting indices in previous epoch", From 5b6ade043c5f3d64c764338faa546e76275a18a8 Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Wed, 17 Jun 2020 10:44:11 +0200 Subject: [PATCH 55/70] remove unused import --- beacon_chain/attestation_pool.nim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index aff0841fe..a809e6a97 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -11,8 +11,7 @@ import deques, sequtils, tables, options, chronicles, stew/[byteutils], json_serialization/std/sets, ./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], - ./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types, - ./fork_choice/fork_choice + ./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types logScope: topics = "attpool" From 8fbbd59885717f53a9b01c22968ebd44603dd94b Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Wed, 17 Jun 2020 13:04:24 +0200 Subject: [PATCH 56/70] metric names (#1188) * fix metric names to not clash with native libp2p metrics * run testnet node with rpc enabled by default --- beacon_chain/beacon_node.nim | 2 +- beacon_chain/eth2_network.nim | 32 ++++++++++++++++---------------- scripts/connect_to_testnet.nims | 5 +++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 40eb827b8..88fad5cb5 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -604,7 +604,7 @@ proc currentSlot(node: BeaconNode): Slot = node.beaconClock.now.slotOrZero proc connectedPeersCount(node: BeaconNode): int = - libp2p_peers.value.int + nbc_peers.value.int proc fromJson(n: JsonNode; argName: string; result: var Slot) = var i: int diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index cbfd9e3d0..26d60e666 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -219,22 +219,22 @@ template neterr(kindParam: Eth2NetworkingErrorKind): auto = err(type(result), Eth2NetworkingError(kind: kindParam)) # Metrics for tracking attestation and beacon block loss -declareCounter gossip_messages_sent, +declareCounter nbc_gossip_messages_sent, "Number of gossip messages sent by this peer" -declareCounter gossip_messages_received, +declareCounter nbc_gossip_messages_received, "Number of gossip messages received by this peer" -declarePublicGauge libp2p_successful_dials, +declarePublicCounter nbc_successful_dials, "Number of successfully dialed peers" -declarePublicGauge libp2p_failed_dials, +declarePublicCounter nbc_failed_dials, "Number of dialing attempts that failed" -declarePublicGauge libp2p_timeout_dials, +declarePublicCounter nbc_timeout_dials, "Number of dialing attempts that exceeded timeout" -declarePublicGauge libp2p_peers, +declarePublicGauge nbc_peers, "Number of active libp2p peers" proc safeClose(conn: Connection) {.async.} = @@ -653,7 +653,7 @@ proc handleOutgoingPeer*(peer: Peer): Future[bool] {.async.} = proc onPeerClosed(udata: pointer) {.gcsafe.} = debug "Peer (outgoing) lost", peer - libp2p_peers.set int64(len(network.peerPool)) + nbc_peers.set int64(len(network.peerPool)) let res = await network.peerPool.addOutgoingPeer(peer) if res: @@ -662,14 +662,14 @@ proc handleOutgoingPeer*(peer: Peer): Future[bool] {.async.} = peer.getFuture().addCallback(onPeerClosed) result = true - libp2p_peers.set int64(len(network.peerPool)) + nbc_peers.set int64(len(network.peerPool)) proc handleIncomingPeer*(peer: Peer): Future[bool] {.async.} = let network = peer.network proc onPeerClosed(udata: pointer) {.gcsafe.} = debug "Peer (incoming) lost", peer - libp2p_peers.set int64(len(network.peerPool)) + nbc_peers.set int64(len(network.peerPool)) let res = await network.peerPool.addIncomingPeer(peer) if res: @@ -678,7 +678,7 @@ proc handleIncomingPeer*(peer: Peer): Future[bool] {.async.} = peer.getFuture().addCallback(onPeerClosed) result = true - libp2p_peers.set int64(len(network.peerPool)) + nbc_peers.set int64(len(network.peerPool)) proc toPeerInfo*(r: enr.TypedRecord): PeerInfo = if r.secp256k1.isSome: @@ -726,7 +726,7 @@ proc dialPeer*(node: Eth2Node, peerInfo: PeerInfo) {.async.} = debug "Initializing connection" await performProtocolHandshakes(peer) - inc libp2p_successful_dials + inc nbc_successful_dials debug "Network handshakes completed" proc connectWorker(network: Eth2Node) {.async.} = @@ -749,11 +749,11 @@ proc connectWorker(network: Eth2Node) {.async.} = if fut.failed() and not(fut.cancelled()): debug "Unable to establish connection with peer", peer = pi.id, errMsg = fut.readError().msg - inc libp2p_failed_dials + inc nbc_failed_dials network.addSeen(pi, SeenTableTimeDeadPeer) continue debug "Connection to remote peer timed out", peer = pi.id - inc libp2p_timeout_dials + inc nbc_timeout_dials network.addSeen(pi, SeenTableTimeTimeout) else: trace "Peer is already connected or already seen", peer = pi.id, @@ -1124,7 +1124,7 @@ proc startLookingForPeers*(node: Eth2Node) {.async.} = proc checkIfConnectedToBootstrapNode {.async.} = await sleepAsync(30.seconds) - if node.discovery.bootstrapRecords.len > 0 and libp2p_successful_dials.value == 0: + if node.discovery.bootstrapRecords.len > 0 and nbc_successful_dials.value == 0: fatal "Failed to connect to any bootstrap node. Quitting", bootstrapEnrs = node.discovery.bootstrapRecords quit 1 @@ -1139,7 +1139,7 @@ proc subscribe*[MsgType](node: Eth2Node, msgHandler: proc(msg: MsgType) {.gcsafe.}, msgValidator: proc(msg: MsgType): bool {.gcsafe.} ) {.async, gcsafe.} = template execMsgHandler(peerExpr, gossipBytes, gossipTopic, useSnappy) = - inc gossip_messages_received + inc nbc_gossip_messages_received trace "Incoming pubsub message received", peer = peerExpr, len = gossipBytes.len, topic = gossipTopic, message_id = `$`(sha256.digest(gossipBytes)) @@ -1191,7 +1191,7 @@ proc traceMessage(fut: FutureBase, digest: MDigest[256]) = trace "Outgoing pubsub message sent", message_id = `$`(digest) proc broadcast*(node: Eth2Node, topic: string, msg: auto) = - inc gossip_messages_sent + inc nbc_gossip_messages_sent let broadcastBytes = SSZ.encode(msg) var fut = node.switch.publish(topic, broadcastBytes) traceMessage(fut, sha256.digest(broadcastBytes)) diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index 462ac6204..a3d8d0d1f 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -43,6 +43,9 @@ cli do (skipGoerliKey {. baseMetricsPort {. desc: "Base metrics port (nodeID will be added to it)" .} = 8008.int, + baseRpcPort {. + desc: "Base rpc port (nodeID will be added to it)" .} = 9090.int, + testnetName {.argument .}: string): let nameParts = testnetName.split "/" @@ -172,6 +175,8 @@ cli do (skipGoerliKey {. --udp-port=""" & $(basePort + nodeID) & &""" --metrics --metrics-port=""" & $(baseMetricsPort + nodeID) & &""" + --rpc + --rpc-port=""" & $(baseRpcPort + nodeID) & &""" {bootstrapFileOpt} {logLevelOpt} {depositContractOpt} From ffca27b45ff0cd8841c1f0e64732b3679be765a1 Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Wed, 17 Jun 2020 13:59:02 +0200 Subject: [PATCH 57/70] update 24 v0.11.x spec refs to v0.12.1 --- beacon_chain/block_pools/clearance.nim | 2 +- beacon_chain/mainchain_monitor.nim | 2 +- beacon_chain/spec/datatypes.nim | 18 +++++++++--------- beacon_chain/spec/keystore.nim | 2 +- beacon_chain/spec/presets/minimal.nim | 12 ++++++------ beacon_chain/spec/state_transition_block.nim | 4 ++-- beacon_chain/time.nim | 2 +- beacon_chain/validator_client.nim | 2 +- beacon_chain/validator_duties.nim | 4 ++-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index 364135989..f2798401b 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -290,7 +290,7 @@ proc isValidBeaconBlock*( # for the slot, signed_beacon_block.message.slot. # # While this condition is similar to the proposer slashing condition at - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#proposer-slashing + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#proposer-slashing # it's not identical, and this check does not address slashing: # # (1) The beacon blocks must be conflicting, i.e. different, for the same diff --git a/beacon_chain/mainchain_monitor.nim b/beacon_chain/mainchain_monitor.nim index f2cec4182..c8fc7f9d2 100644 --- a/beacon_chain/mainchain_monitor.nim +++ b/beacon_chain/mainchain_monitor.nim @@ -101,7 +101,7 @@ func voting_period_start_time*(state: BeaconState): uint64 = state.slot - state.slot mod SLOTS_PER_ETH1_VOTING_PERIOD.uint64 compute_time_at_slot(state, eth1_voting_period_start_slot) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#get_eth1_data +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#get_eth1_data func is_candidate_block(blk: Eth1Block, period_start: uint64): bool = (blk.timestamp + SECONDS_PER_ETH1_BLOCK.uint64 * ETH1_FOLLOW_DISTANCE.uint64 <= period_start) and (blk.timestamp + SECONDS_PER_ETH1_BLOCK.uint64 * ETH1_FOLLOW_DISTANCE.uint64 * 2 >= period_start) diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 610ee56f0..424ae3d0c 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -111,7 +111,7 @@ template maxSize*(n: int) {.pragma.} type # Domains # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#domain-types + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#domain-types DomainType* = enum DOMAIN_BEACON_PROPOSER = 0 DOMAIN_BEACON_ATTESTER = 1 @@ -121,12 +121,12 @@ type DOMAIN_SELECTION_PROOF = 5 DOMAIN_AGGREGATE_AND_PROOF = 6 # Phase 1 - Sharding - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.2/specs/phase1/beacon-chain.md#misc + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase1/beacon-chain.md#misc DOMAIN_SHARD_PROPOSAL = 128 DOMAIN_SHARD_COMMITTEE = 129 DOMAIN_LIGHT_CLIENT = 130 # Phase 1 - Custody game - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.2/specs/phase1/custody-game.md#signature-domain-types + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase1/custody-game.md#signature-domain-types DOMAIN_CUSTODY_BIT_SLASHING = 0x83 # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#custom-types @@ -153,7 +153,7 @@ type attestation_1*: IndexedAttestation attestation_2*: IndexedAttestation - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#indexedattestation + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#indexedattestation IndexedAttestation* = object # TODO ValidatorIndex, but that doesn't serialize properly attesting_indices*: List[uint64, MAX_VALIDATORS_PER_COMMITTEE] @@ -176,7 +176,7 @@ type current_version*: Version genesis_validators_root*: Eth2Digest - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#checkpoint + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#checkpoint Checkpoint* = object epoch*: Epoch root*: Eth2Digest @@ -216,7 +216,7 @@ type amount*: Gwei signature*: ValidatorSig # Signing over DepositMessage - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#voluntaryexit + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#voluntaryexit VoluntaryExit* = object epoch*: Epoch ##\ ## Earliest epoch when voluntary exit can be processed @@ -342,7 +342,7 @@ type withdrawable_epoch*: Epoch ##\ ## When validator can withdraw or transfer funds - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#pendingattestation + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#pendingattestation PendingAttestation* = object aggregation_bits*: CommitteeValidatorsBits data*: AttestationData @@ -352,7 +352,7 @@ type proposer_index*: uint64 - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#historicalbatch + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#historicalbatch HistoricalBatch* = object block_roots* : array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] state_roots* : array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] @@ -394,7 +394,7 @@ type aggregate*: Attestation selection_proof*: ValidatorSig - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.2/specs/phase0/validator.md#signedaggregateandproof + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#signedaggregateandproof SignedAggregateAndProof* = object message*: AggregateAndProof signature*: ValidatorSig diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 65472c6ca..4de9a3052 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -377,7 +377,7 @@ proc generateCredentials*(entropy: openarray[byte] = @[], let mnemonic = generateMnemonic(englishWords, entropy) restoreCredentials(mnemonic, password) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/deposit-contract.md#withdrawal-credentials proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest = var bytes = eth2digest(k.toRaw()) bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8 diff --git a/beacon_chain/spec/presets/minimal.nim b/beacon_chain/spec/presets/minimal.nim index 3c6b2bf63..4dbb29e9c 100644 --- a/beacon_chain/spec/presets/minimal.nim +++ b/beacon_chain/spec/presets/minimal.nim @@ -107,7 +107,7 @@ const # Reward and penalty quotients # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L117 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L117 BASE_REWARD_FACTOR* = 2'u64^6 WHISTLEBLOWER_REWARD_QUOTIENT* = 2'u64^9 @@ -134,7 +134,7 @@ const # Validators # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L38 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L38 # Changed ETH1_FOLLOW_DISTANCE* = 16 # blocks @@ -147,14 +147,14 @@ const # Phase 1: Upgrade from Phase 0 # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L161 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L161 PHASE_1_FORK_VERSION* = 16777217 PHASE_1_GENESIS_SLOT* = 8 INITIAL_ACTIVE_SHARDS* = 4 # Phase 1: General # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L169 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L171 MAX_SHARDS* = 8 ONLINE_PERIOD* = 8 # epochs ~ 51 minutes LIGHT_CLIENT_COMMITTEE_SIZE* = 128 @@ -170,7 +170,7 @@ const # Phase 1 - Custody game # --------------------------------------------------------------- # Time parameters - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L202 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L202 RANDAO_PENALTY_EPOCHS* = 2 EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS* = 4096 # epochs EPOCHS_PER_CUSTODY_PERIOD* = 2048 @@ -184,6 +184,6 @@ const MAX_CUSTODY_SLASHINGS* = 1 # Reward and penalty quotients - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L220 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L220 EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE* = 2 MINOR_REWARD_QUOTIENT* = 256 diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 5acabbfc4..3a60ad2f3 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -320,7 +320,7 @@ proc process_voluntary_exit*( true -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#operations +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#operations proc process_operations(state: var BeaconState, body: BeaconBlockBody, flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.} = # Verify that outstanding deposits are processed up to the maximum number of @@ -355,7 +355,7 @@ proc process_operations(state: var BeaconState, body: BeaconBlockBody, true -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#block-processing +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#block-processing proc process_block*( state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags, stateCache: var StateCache): bool {.nbench.}= diff --git a/beacon_chain/time.nim b/beacon_chain/time.nim index 8e4932e10..857ac420c 100644 --- a/beacon_chain/time.nim +++ b/beacon_chain/time.nim @@ -16,7 +16,7 @@ type ## which blocks are valid - in particular, blocks are not valid if they ## come from the future as seen from the local clock. ## - ## https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/fork-choice.md#fork-choice + ## https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#fork-choice ## # TODO replace time in chronos with a proper unit type, then this code can # follow: diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index bca8e1722..273aa9b63 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -111,7 +111,7 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a discard await vc.client.post_v1_beacon_blocks(newBlock) - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attesting + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting # A validator should create and broadcast the attestation to the associated # attestation subnet when either (a) the validator has received a valid # block from the expected block proposer for the assigned slot or diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 592ffc1b1..b29b43d2e 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -454,7 +454,7 @@ proc handleValidatorDuties*( # with any clock discrepancies once only, at the start of slot timer # processing.. - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attesting + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting # A validator should create and broadcast the attestation to the associated # attestation subnet when either (a) the validator has received a valid # block from the expected block proposer for the assigned slot or @@ -470,7 +470,7 @@ proc handleValidatorDuties*( handleAttestations(node, head, slot) - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#broadcast-aggregate + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-aggregate # If the validator is selected to aggregate (is_aggregator), then they # broadcast their best aggregate as a SignedAggregateAndProof to the global # aggregate channel (beacon_aggregate_and_proof) two-thirds of the way From e072997e971644a7bf74f6c1e19251e40002830b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 3 Jun 2020 14:49:32 +0200 Subject: [PATCH 58/70] Nim-1.2.2 --- azure-pipelines.yml | 2 +- config.nims | 7 +++++++ vendor/nimbus-build-system | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b809912c2..47228b5fe 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,7 +16,7 @@ jobs: - task: CacheBeta@1 displayName: 'cache Nim binaries' inputs: - key: NimBinaries | $(Agent.OS) | $(PLATFORM) | "$(Build.SourceBranchName)" | "v4" + key: NimBinaries | $(Agent.OS) | $(PLATFORM) | "$(Build.SourceBranchName)" | "v7" path: NimBinaries - task: CacheBeta@1 diff --git a/config.nims b/config.nims index f17c89a99..944142ccc 100644 --- a/config.nims +++ b/config.nims @@ -65,3 +65,10 @@ if not defined(macosx): # `switch("warning[CaseTransition]", "off")` fails with "Error: invalid command line option: '--warning[CaseTransition]'" switch("warning", "CaseTransition:off") +# The compiler doth protest too much, methinks, about all these cases where it can't +# do its (N)RVO pass: https://github.com/nim-lang/RFCs/issues/230 +switch("warning", "ObservableStores:off") + +# Too many false positives for "Warning: method has lock level , but another method has 0 [LockLevel]" +switch("warning", "LockLevel:off") + diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system index 46b6f7880..ae49e03af 160000 --- a/vendor/nimbus-build-system +++ b/vendor/nimbus-build-system @@ -1 +1 @@ -Subproject commit 46b6f78806026b37e4710eabf8bd047969d2d23c +Subproject commit ae49e03af6f36393eb7e0fc02c1c47df42efd2de From 073046f8abd7648d08336def921ea19eb75cd6d3 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Wed, 17 Jun 2020 13:30:45 +0200 Subject: [PATCH 59/70] bump libp2p --- vendor/nim-libp2p | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index 9d9f793b4..fe828d87d 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit 9d9f793b4f4674b95b524e175509ea6402744f68 +Subproject commit fe828d87d8a1eb4c018400fe525e127c2c0401c3 From 673eeb6a65494479d96caa56c7f166599457cad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 17 Jun 2020 16:03:34 +0200 Subject: [PATCH 60/70] connect_to_testnet.nims changes [skip ci] - remove `--dev-build` option - unconditionally write the "nbc.log" file, but do it after a chdir to dataDir because Chronicles doesn't seem to support proper paths for "file(...)" in sink definitions - change base RPC port (9090 -> 9190) because 9090 is the default Prometheus daemon listening port --- scripts/connect_to_testnet.nims | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index a3d8d0d1f..bbb4ae712 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -30,10 +30,6 @@ cli do (skipGoerliKey {. desc: "The Ethereum 2.0 const preset of the network (optional)" name: "const-preset" .} = "", - devBuild {. - desc: "Enables more extensive logging and debugging support" - name: "dev-build" .} = false, - nodeID {. desc: "Node ID" .} = 0.int, @@ -44,7 +40,7 @@ cli do (skipGoerliKey {. desc: "Base metrics port (nodeID will be added to it)" .} = 8008.int, baseRpcPort {. - desc: "Base rpc port (nodeID will be added to it)" .} = 9090.int, + desc: "Base rpc port (nodeID will be added to it)" .} = 9190.int, testnetName {.argument .}: string): let @@ -104,8 +100,8 @@ cli do (skipGoerliKey {. var nimFlags = "-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS") - if devBuild: - nimFlags.add """ -d:"chronicles_sinks=textlines,json[file(nbc.log)]" """ + # write the logs to a file + nimFlags.add """ -d:"chronicles_sinks=textlines,json[file(nbc.log,truncate)]" """ let depositContractFile = testnetDir / depositContractFileName if system.fileExists(depositContractFile): @@ -167,6 +163,7 @@ cli do (skipGoerliKey {. logLevelOpt = &"""--log-level="{logLevel}" """ mode = Verbose + cd dataDir execIgnoringExitCode replace(&"""{beaconNodeBinary} --data-dir="{dataDir}" --dump From 3a3d3f9bdecfcca878902d1da605502d7c60483d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 17 Jun 2020 16:44:48 +0200 Subject: [PATCH 61/70] timestamps in log files --- scripts/connect_to_testnet.nims | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index bbb4ae712..d494bd463 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -101,7 +101,7 @@ cli do (skipGoerliKey {. nimFlags = "-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS") # write the logs to a file - nimFlags.add """ -d:"chronicles_sinks=textlines,json[file(nbc.log,truncate)]" """ + nimFlags.add """ -d:"chronicles_sinks=textlines,json[file(nbc""" & staticExec("date +\"%Y%m%d%H%M%S\"") & """.log)]" """ let depositContractFile = testnetDir / depositContractFileName if system.fileExists(depositContractFile): From 6fc68bbee5aa98e72a47ed4d7dd15ae67c0da179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Wed, 17 Jun 2020 19:41:59 +0200 Subject: [PATCH 62/70] disable "minimal" preset tests --- beacon_chain.nimble | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/beacon_chain.nimble b/beacon_chain.nimble index c88e5d40a..359917f80 100644 --- a/beacon_chain.nimble +++ b/beacon_chain.nimble @@ -58,46 +58,27 @@ task test, "Run all tests": # pieces of code get tested regularly. Increased test output verbosity is the # price we pay for that. - # Mainnet tests are not enabled on 32-bit Windows, to avoid a 1h20m timeout on - # Azure Pipelines (there's no libbacktrace/libunwind support there, and the - # performance hit is big). - - # Minimal config - buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildAndRunBinary "all_tests", "tests/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - when not (defined(windows) and defined(i386)): - # Mainnet config - buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - buildAndRunBinary "all_tests", "tests/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + # Mainnet config + buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "all_tests", "tests/", "-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # Generic SSZ test, doesn't use consensus objects minimal/mainnet presets buildAndRunBinary "test_fixture_ssz_generic_types", "tests/official/", "-d:chronicles_log_level=TRACE" # Consensus object SSZ tests # 0.11.3 - buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" - when not (defined(windows) and defined(i386)): - buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" # 0.12.1 - buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - when not (defined(windows) and defined(i386)): - buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # 0.11.3 - buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.11.3\"" - when not (defined(windows) and defined(i386)): - buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:ETH2_SPEC=\"v0.11.3\"" # 0.12.1 - buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" - when not (defined(windows) and defined(i386)): - buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" + buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", "-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"" # State sim; getting into 4th epoch useful to trigger consensus checks - buildAndRunBinary "state_sim", "research/", "-d:const_preset=minimal -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=32" - when not (defined(windows) and defined(i386)): - buildAndRunBinary "state_sim", "research/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=128" + buildAndRunBinary "state_sim", "research/", "-d:const_preset=mainnet -d:ETH2_SPEC=\"v0.12.1\" -d:BLS_ETH2_SPEC=\"v0.12.x\"", "--validators=2000 --slots=128" From ee9f4a2e3fada891fcc45f04010aa56bb51f587e Mon Sep 17 00:00:00 2001 From: tersec Date: Thu, 18 Jun 2020 05:56:47 +0000 Subject: [PATCH 63/70] remove skipMerkleValidation and skipBlockParentRootValidation (#1197) --- beacon_chain/beacon_node.nim | 16 ++++++++++++---- beacon_chain/extras.nim | 5 ----- beacon_chain/keystore_management.nim | 2 +- beacon_chain/spec/beaconstate.nim | 4 ++-- beacon_chain/spec/state_transition_block.nim | 3 +-- tests/simulation/vars.sh | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 88fad5cb5..7033b2e3b 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -7,7 +7,7 @@ import # Standard library - os, tables, random, strutils, times, math, + algorithm, os, tables, random, strutils, times, math, # Nimble packages stew/[objects, byteutils], stew/shims/macros, @@ -1005,8 +1005,10 @@ programMain: case config.cmd of createTestnet: - var deposits: seq[Deposit] - var i = -1 + var + depositDirs: seq[string] + deposits: seq[Deposit] + i = -1 for kind, dir in walkDir(config.testnetDepositsDir.string): if kind != pcDir: continue @@ -1015,6 +1017,12 @@ programMain: if i < config.firstValidator.int: continue + depositDirs.add dir + + # Add deposits, in order, to pass Merkle validation + sort(depositDirs, system.cmp) + + for dir in depositDirs: let depositFile = dir / "deposit.json" try: deposits.add Json.loadFile(depositFile, Deposit) @@ -1031,7 +1039,7 @@ programMain: else: waitFor getLatestEth1BlockHash(config.web3Url) var initialState = initialize_beacon_state_from_eth1( - eth1Hash, startTime, deposits, {skipBlsValidation, skipMerkleValidation}) + eth1Hash, startTime, deposits, {skipBlsValidation}) # https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start#create-genesis-state initialState.genesis_time = startTime diff --git a/beacon_chain/extras.nim b/beacon_chain/extras.nim index 6981087b2..736826415 100644 --- a/beacon_chain/extras.nim +++ b/beacon_chain/extras.nim @@ -20,17 +20,12 @@ type UpdateFlag* = enum - skipMerkleValidation ##\ - ## When processing deposits, skip verifying the Merkle proof trees of each - ## deposit. skipBlsValidation ##\ ## Skip verification of BLS signatures in block processing. ## Predominantly intended for use in testing, e.g. to allow extra coverage. ## Also useful to avoid unnecessary work when replaying known, good blocks. skipStateRootValidation ##\ ## Skip verification of block state root. - skipBlockParentRootValidation ##\ - ## Skip verification that the block's parent root matches the previous block header. verifyFinalization UpdateFlags* = set[UpdateFlag] diff --git a/beacon_chain/keystore_management.nim b/beacon_chain/keystore_management.nim index d00e250ef..7e81f1527 100644 --- a/beacon_chain/keystore_management.nim +++ b/beacon_chain/keystore_management.nim @@ -109,7 +109,7 @@ proc generateDeposits*(totalValidators: int, let credentials = generateCredentials(password = password) let - keyName = $(credentials.signingKey.toPubKey) + keyName = intToStr(i, 6) & "_" & $(credentials.signingKey.toPubKey) validatorDir = validatorsDir / keyName passphraseFile = secretsDir / keyName depositFile = validatorDir / depositFileName diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 4b8e66300..7287850e5 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -55,14 +55,14 @@ proc process_deposit*( # Process an Eth1 deposit, registering a validator or increasing its balance. # Verify the Merkle branch - if skipMerkleValidation notin flags and not is_valid_merkle_branch( + if not is_valid_merkle_branch( hash_tree_root(deposit.data), deposit.proof, DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the `List` length mix-in state.eth1_deposit_index, state.eth1_data.deposit_root, ): - notice "Deposit merkle validation failed", + notice "Deposit Merkle validation failed", proof = deposit.proof, deposit_root = state.eth1_data.deposit_root, deposit_index = state.eth1_deposit_index return false diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 3a60ad2f3..aa9cfdef2 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -71,8 +71,7 @@ proc process_block_header*( return false # Verify that the parent matches - if skipBlockParentRootValidation notin flags and not (blck.parent_root == - hash_tree_root(state.latest_block_header)): + if not (blck.parent_root == hash_tree_root(state.latest_block_header)): notice "Block header: previous block root mismatch", latest_block_header = state.latest_block_header, blck = shortLog(blck), diff --git a/tests/simulation/vars.sh b/tests/simulation/vars.sh index 4afa7479a..8304bce5e 100644 --- a/tests/simulation/vars.sh +++ b/tests/simulation/vars.sh @@ -19,7 +19,7 @@ cd - &>/dev/null # When changing these, also update the readme section on running simulation # so that the run_node example is correct! -NUM_VALIDATORS=${VALIDATORS:-192} +NUM_VALIDATORS=${VALIDATORS:-128} TOTAL_NODES=${NODES:-4} TOTAL_USER_NODES=${USER_NODES:-0} TOTAL_SYSTEM_NODES=$(( TOTAL_NODES - TOTAL_USER_NODES )) From e813111b3b98d6ece99ccf964990d7d6d485b610 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Wed, 17 Jun 2020 16:27:07 +0200 Subject: [PATCH 64/70] peers rpc call simple way to display nbc peer table --- beacon_chain/beacon_node.nim | 16 ++++++++++++++++ beacon_chain/eth2_network.nim | 11 +++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 7033b2e3b..bb6a69a9a 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -705,6 +705,22 @@ proc installDebugApiHandlers(rpcServer: RpcServer, node: BeaconNode) = return res + rpcServer.rpc("peers") do () -> JsonNode: + var res = newJObject() + var peers = newJArray() + for id, peer in node.network.peerPool: + peers.add( + %( + info: shortLog(peer.info), + wasDialed: peer.wasDialed, + connectionState: $peer.connectionState, + score: peer.score, + ) + ) + res.add("peers", peers) + + return res + proc installRpcHandlers(rpcServer: RpcServer, node: BeaconNode) = rpcServer.installValidatorApiHandlers(node) rpcServer.installBeaconApiHandlers(node) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 26d60e666..00c8ca539 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -192,8 +192,6 @@ const TTFB_TIMEOUT* = 5.seconds RESP_TIMEOUT* = 10.seconds - readTimeoutErrorMsg = "Exceeded read timeout for a request" - NewPeerScore* = 200 ## Score which will be assigned to new connected Peer PeerScoreLowLimit* = 0 @@ -274,10 +272,6 @@ proc openStream(node: Eth2Node, else: raise -func peerId(conn: Connection): PeerID = - # TODO: Can this be `nil`? - conn.peerInfo.peerId - proc init*(T: type Peer, network: Eth2Node, info: PeerInfo): Peer {.gcsafe.} proc getPeer*(node: Eth2Node, peerInfo: PeerInfo): Peer {.gcsafe.} = @@ -572,6 +566,11 @@ proc handleIncomingStream(network: Eth2Node, try: let peer = peerFromStream(network, conn) + # TODO peer connection setup is broken, update info in some better place + # whenever race is fix: + # https://github.com/status-im/nim-beacon-chain/issues/1157 + peer.info = conn.peerInfo + template returnInvalidRequest(msg: ErrorMsg) = await sendErrorResponse(peer, conn, noSnappy, InvalidRequest, msg) return From e96a58b7d734944f42ce2a060041647190ee4cac Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Thu, 18 Jun 2020 09:37:15 +0200 Subject: [PATCH 65/70] remove orphaned/unmaintained minimal preset test result summaries --- AllTests-minimal.md | 268 --------------------------------- FixtureAll-minimal.md | 183 ---------------------- FixtureSSZConsensus-minimal.md | 36 ----- FixtureSSZGeneric-minimal.md | 21 --- 4 files changed, 508 deletions(-) delete mode 100644 AllTests-minimal.md delete mode 100644 FixtureAll-minimal.md delete mode 100644 FixtureSSZConsensus-minimal.md delete mode 100644 FixtureSSZGeneric-minimal.md diff --git a/AllTests-minimal.md b/AllTests-minimal.md deleted file mode 100644 index fa56cc121..000000000 --- a/AllTests-minimal.md +++ /dev/null @@ -1,268 +0,0 @@ -AllTests-minimal -=== -## Attestation pool processing [Preset: minimal] -```diff -+ Attestations may arrive in any order [Preset: minimal] OK -+ Attestations may overlap, bigger first [Preset: minimal] OK -+ Attestations may overlap, smaller first [Preset: minimal] OK -+ Attestations should be combined [Preset: minimal] OK -+ Can add and retrieve simple attestation [Preset: minimal] OK -+ Fork choice returns block with attestation OK -+ Fork choice returns latest block with no attestations OK -``` -OK: 7/7 Fail: 0/7 Skip: 0/7 -## Beacon chain DB [Preset: minimal] -```diff -+ empty database [Preset: minimal] OK -+ find ancestors [Preset: minimal] OK -+ sanity check blocks [Preset: minimal] OK -+ sanity check genesis roundtrip [Preset: minimal] OK -+ sanity check states [Preset: minimal] OK -``` -OK: 5/5 Fail: 0/5 Skip: 0/5 -## Beacon node -```diff -+ Compile OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## Beacon state [Preset: minimal] -```diff -+ Smoke test initialize_beacon_state_from_eth1 [Preset: minimal] OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## Block pool processing [Preset: minimal] -```diff -+ Can add same block twice [Preset: minimal] OK -+ Reverse order block add & get [Preset: minimal] OK -+ Simple block add&get [Preset: minimal] OK -+ getRef returns nil for missing blocks OK -+ loadTailState gets genesis block on first load [Preset: minimal] OK -+ updateHead updates head and headState [Preset: minimal] OK -+ updateStateData sanity [Preset: minimal] OK -``` -OK: 7/7 Fail: 0/7 Skip: 0/7 -## Block processing [Preset: minimal] -```diff -+ Attestation gets processed at epoch [Preset: minimal] OK -+ Passes from genesis state, empty block [Preset: minimal] OK -+ Passes from genesis state, no block [Preset: minimal] OK -+ Passes through epoch update, empty block [Preset: minimal] OK -+ Passes through epoch update, no block [Preset: minimal] OK -``` -OK: 5/5 Fail: 0/5 Skip: 0/5 -## BlockPool finalization tests [Preset: minimal] -```diff -+ init with gaps [Preset: minimal] OK -+ prune heads on finalization [Preset: minimal] OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## BlockRef and helpers [Preset: minimal] -```diff -+ getAncestorAt sanity [Preset: minimal] OK -+ isAncestorOf sanity [Preset: minimal] OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## BlockSlot and helpers [Preset: minimal] -```diff -+ atSlot sanity [Preset: minimal] OK -+ parent sanity [Preset: minimal] OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## Fork Choice + Finality [Preset: minimal] -```diff -+ fork_choice - testing finality #01 OK -+ fork_choice - testing finality #02 OK -+ fork_choice - testing no votes OK -+ fork_choice - testing with votes OK -``` -OK: 4/4 Fail: 0/4 Skip: 0/4 -## Honest validator -```diff -+ General pubsub topics: OK -+ Mainnet attestation topics OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## Interop -```diff -+ Interop genesis OK -+ Interop signatures OK -+ Mocked start private key OK -``` -OK: 3/3 Fail: 0/3 Skip: 0/3 -## Keystore -```diff -+ Pbkdf2 decryption OK -+ Pbkdf2 encryption OK -+ Pbkdf2 errors OK -``` -OK: 3/3 Fail: 0/3 Skip: 0/3 -## Mocking utilities -```diff -+ merkle_minimal OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## Official - constants & config [Preset: minimal] -```diff -+ BASE_REWARD_FACTOR 64 [Preset: minimal] OK -+ BLS_WITHDRAWAL_PREFIX "0x00" [Preset: minimal] OK -+ CHURN_LIMIT_QUOTIENT 65536 [Preset: minimal] OK -+ CUSTODY_PERIOD_TO_RANDAO_PADDING 2048 [Preset: minimal] OK - DEPOSIT_CONTRACT_ADDRESS "0x1234567890123456789012345678901234567 Skip -+ DOMAIN_AGGREGATE_AND_PROOF "0x06000000" [Preset: minimal] OK -+ DOMAIN_BEACON_ATTESTER "0x01000000" [Preset: minimal] OK -+ DOMAIN_BEACON_PROPOSER "0x00000000" [Preset: minimal] OK -+ DOMAIN_CUSTODY_BIT_SLASHING "0x83000000" [Preset: minimal] OK -+ DOMAIN_DEPOSIT "0x03000000" [Preset: minimal] OK -+ DOMAIN_LIGHT_CLIENT "0x82000000" [Preset: minimal] OK -+ DOMAIN_RANDAO "0x02000000" [Preset: minimal] OK -+ DOMAIN_SELECTION_PROOF "0x05000000" [Preset: minimal] OK -+ DOMAIN_SHARD_COMMITTEE "0x81000000" [Preset: minimal] OK -+ DOMAIN_SHARD_PROPOSAL "0x80000000" [Preset: minimal] OK -+ DOMAIN_VOLUNTARY_EXIT "0x04000000" [Preset: minimal] OK -+ EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS 4096 [Preset: minimal] OK -+ EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE 2 [Preset: minimal] OK -+ EFFECTIVE_BALANCE_INCREMENT 1000000000 [Preset: minimal] OK -+ EJECTION_BALANCE 16000000000 [Preset: minimal] OK -+ EPOCHS_PER_CUSTODY_PERIOD 2048 [Preset: minimal] OK -+ EPOCHS_PER_ETH1_VOTING_PERIOD 4 [Preset: minimal] OK -+ EPOCHS_PER_HISTORICAL_VECTOR 64 [Preset: minimal] OK -+ EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION 256 [Preset: minimal] OK -+ EPOCHS_PER_SLASHINGS_VECTOR 64 [Preset: minimal] OK -+ ETH1_FOLLOW_DISTANCE 16 [Preset: minimal] OK -+ GASPRICE_ADJUSTMENT_COEFFICIENT 8 [Preset: minimal] OK -+ GENESIS_DELAY 300 [Preset: minimal] OK - GENESIS_FORK_VERSION "0x00000001" [Preset: minimal] Skip -+ HISTORICAL_ROOTS_LIMIT 16777216 [Preset: minimal] OK -+ HYSTERESIS_DOWNWARD_MULTIPLIER 1 [Preset: minimal] OK -+ HYSTERESIS_QUOTIENT 4 [Preset: minimal] OK -+ HYSTERESIS_UPWARD_MULTIPLIER 5 [Preset: minimal] OK -+ INACTIVITY_PENALTY_QUOTIENT 16777216 [Preset: minimal] OK -+ INITIAL_ACTIVE_SHARDS 4 [Preset: minimal] OK -+ LIGHT_CLIENT_COMMITTEE_PERIOD 256 [Preset: minimal] OK -+ LIGHT_CLIENT_COMMITTEE_SIZE 128 [Preset: minimal] OK -+ MAX_ATTESTATIONS 128 [Preset: minimal] OK -+ MAX_ATTESTER_SLASHINGS 2 [Preset: minimal] OK -+ MAX_COMMITTEES_PER_SLOT 4 [Preset: minimal] OK -+ MAX_CUSTODY_KEY_REVEALS 256 [Preset: minimal] OK -+ MAX_CUSTODY_SLASHINGS 1 [Preset: minimal] OK -+ MAX_DEPOSITS 16 [Preset: minimal] OK -+ MAX_EARLY_DERIVED_SECRET_REVEALS 1 [Preset: minimal] OK -+ MAX_EFFECTIVE_BALANCE 32000000000 [Preset: minimal] OK -+ MAX_EPOCHS_PER_CROSSLINK 4 [Preset: minimal] OK -+ MAX_GASPRICE 16384 [Preset: minimal] OK -+ MAX_PROPOSER_SLASHINGS 16 [Preset: minimal] OK -+ MAX_REVEAL_LATENESS_DECREMENT 128 [Preset: minimal] OK -+ MAX_SEED_LOOKAHEAD 4 [Preset: minimal] OK -+ MAX_SHARDS 8 [Preset: minimal] OK -+ MAX_SHARD_BLOCKS_PER_ATTESTATION 12 [Preset: minimal] OK -+ MAX_SHARD_BLOCK_CHUNKS 4 [Preset: minimal] OK -+ MAX_VALIDATORS_PER_COMMITTEE 2048 [Preset: minimal] OK -+ MAX_VOLUNTARY_EXITS 16 [Preset: minimal] OK -+ MINOR_REWARD_QUOTIENT 256 [Preset: minimal] OK -+ MIN_ATTESTATION_INCLUSION_DELAY 1 [Preset: minimal] OK -+ MIN_DEPOSIT_AMOUNT 1000000000 [Preset: minimal] OK -+ MIN_EPOCHS_TO_INACTIVITY_PENALTY 4 [Preset: minimal] OK -+ MIN_GASPRICE 8 [Preset: minimal] OK -+ MIN_GENESIS_ACTIVE_VALIDATOR_COUNT 64 [Preset: minimal] OK -+ MIN_GENESIS_TIME 1578009600 [Preset: minimal] OK -+ MIN_PER_EPOCH_CHURN_LIMIT 4 [Preset: minimal] OK -+ MIN_SEED_LOOKAHEAD 1 [Preset: minimal] OK -+ MIN_SLASHING_PENALTY_QUOTIENT 32 [Preset: minimal] OK -+ MIN_VALIDATOR_WITHDRAWABILITY_DELAY 256 [Preset: minimal] OK -+ ONLINE_PERIOD 8 [Preset: minimal] OK -+ PHASE_1_FORK_VERSION "0x01000001" [Preset: minimal] OK -+ PHASE_1_GENESIS_SLOT 8 [Preset: minimal] OK -+ PROPOSER_REWARD_QUOTIENT 8 [Preset: minimal] OK -+ RANDAO_PENALTY_EPOCHS 2 [Preset: minimal] OK -+ RANDOM_SUBNETS_PER_VALIDATOR 1 [Preset: minimal] OK -+ SAFE_SLOTS_TO_UPDATE_JUSTIFIED 2 [Preset: minimal] OK -+ SECONDS_PER_ETH1_BLOCK 14 [Preset: minimal] OK -+ SECONDS_PER_SLOT 6 [Preset: minimal] OK -+ SHARD_BLOCK_CHUNK_SIZE 262144 [Preset: minimal] OK - SHARD_BLOCK_OFFSETS [1,2,3,5,8,13,21,34,55,89,144,233] [Pres Skip -+ SHARD_COMMITTEE_PERIOD 64 [Preset: minimal] OK -+ SHUFFLE_ROUND_COUNT 10 [Preset: minimal] OK -+ SLOTS_PER_EPOCH 8 [Preset: minimal] OK -+ SLOTS_PER_HISTORICAL_ROOT 64 [Preset: minimal] OK -+ TARGET_AGGREGATORS_PER_COMMITTEE 16 [Preset: minimal] OK -+ TARGET_COMMITTEE_SIZE 4 [Preset: minimal] OK -+ TARGET_SHARD_BLOCK_SIZE 196608 [Preset: minimal] OK -+ VALIDATOR_REGISTRY_LIMIT 1099511627776 [Preset: minimal] OK -+ WHISTLEBLOWER_REWARD_QUOTIENT 512 [Preset: minimal] OK -``` -OK: 83/86 Fail: 0/86 Skip: 3/86 -## PeerPool testing suite -```diff -+ Access peers by key test OK -+ Acquire from empty pool OK -+ Acquire/Sorting and consistency test OK -+ Iterators test OK -+ Peer lifetime test OK -+ Safe/Clear test OK -+ Score check test OK -+ addPeer() test OK -+ addPeerNoWait() test OK -+ deletePeer() test OK -``` -OK: 10/10 Fail: 0/10 Skip: 0/10 -## SSZ dynamic navigator -```diff -+ navigating fields OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## SSZ navigator -```diff -+ basictype OK -+ lists with max size OK -+ simple object fields OK -``` -OK: 3/3 Fail: 0/3 Skip: 0/3 -## Spec helpers -```diff -+ integer_squareroot OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## Sync protocol -```diff -+ Compile OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## Zero signature sanity checks -```diff -+ SSZ serialization roundtrip of SignedBeaconBlockHeader OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 -## [Unit - Spec - Block processing] Attestations [Preset: minimal] -```diff -+ Valid attestation OK -+ Valid attestation from previous epoch OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## [Unit - Spec - Block processing] Deposits [Preset: minimal] -```diff -+ Deposit at MAX_EFFECTIVE_BALANCE balance (32 ETH) OK -+ Deposit over MAX_EFFECTIVE_BALANCE balance (32 ETH) OK -+ Deposit under MAX_EFFECTIVE_BALANCE balance (32 ETH) OK -+ Validator top-up OK -``` -OK: 4/4 Fail: 0/4 Skip: 0/4 -## [Unit - Spec - Epoch processing] Justification and Finalization [Preset: minimal] -```diff -+ Rule I - 234 finalization with enough support OK -+ Rule I - 234 finalization without support OK -+ Rule II - 23 finalization with enough support OK -+ Rule II - 23 finalization without support OK -+ Rule III - 123 finalization with enough support OK -+ Rule III - 123 finalization without support OK -+ Rule IV - 12 finalization with enough support OK -+ Rule IV - 12 finalization without support OK -``` -OK: 8/8 Fail: 0/8 Skip: 0/8 -## hash -```diff -+ HashArray OK -``` -OK: 1/1 Fail: 0/1 Skip: 0/1 - ----TOTAL--- -OK: 160/163 Fail: 0/163 Skip: 3/163 diff --git a/FixtureAll-minimal.md b/FixtureAll-minimal.md deleted file mode 100644 index 6798bd997..000000000 --- a/FixtureAll-minimal.md +++ /dev/null @@ -1,183 +0,0 @@ -FixtureAll-minimal -=== -## Official - Epoch Processing - Final updates [Preset: minimal] -```diff -+ Final updates - effective_balance_hysteresis [Preset: minimal] OK -+ Final updates - eth1_vote_no_reset [Preset: minimal] OK -+ Final updates - eth1_vote_reset [Preset: minimal] OK -+ Final updates - historical_root_accumulator [Preset: minimal] OK -``` -OK: 4/4 Fail: 0/4 Skip: 0/4 -## Official - Epoch Processing - Justification & Finalization [Preset: minimal] -```diff -+ Justification & Finalization - 123_ok_support [Preset: minimal] OK -+ Justification & Finalization - 123_poor_support [Preset: minimal] OK -+ Justification & Finalization - 12_ok_support [Preset: minimal] OK -+ Justification & Finalization - 12_ok_support_messed_target [Preset: minimal] OK -+ Justification & Finalization - 12_poor_support [Preset: minimal] OK -+ Justification & Finalization - 234_ok_support [Preset: minimal] OK -+ Justification & Finalization - 234_poor_support [Preset: minimal] OK -+ Justification & Finalization - 23_ok_support [Preset: minimal] OK -+ Justification & Finalization - 23_poor_support [Preset: minimal] OK -``` -OK: 9/9 Fail: 0/9 Skip: 0/9 -## Official - Epoch Processing - Registry updates [Preset: minimal] -```diff -+ Registry updates - activation_queue_activation_and_ejection [Preset: minimal] OK -+ Registry updates - activation_queue_efficiency [Preset: minimal] OK -+ Registry updates - activation_queue_no_activation_no_finality [Preset: minimal] OK -+ Registry updates - activation_queue_sorting [Preset: minimal] OK -+ Registry updates - activation_queue_to_activated_if_finalized [Preset: minimal] OK -+ Registry updates - add_to_activation_queue [Preset: minimal] OK -+ Registry updates - ejection [Preset: minimal] OK -+ Registry updates - ejection_past_churn_limit [Preset: minimal] OK -``` -OK: 8/8 Fail: 0/8 Skip: 0/8 -## Official - Epoch Processing - Slashings [Preset: minimal] -```diff -+ Slashings - max_penalties [Preset: minimal] OK -+ Slashings - scaled_penalties [Preset: minimal] OK -+ Slashings - small_penalty [Preset: minimal] OK -``` -OK: 3/3 Fail: 0/3 Skip: 0/3 -## Official - Operations - Attestations [Preset: minimal] -```diff -+ [Invalid] after_epoch_slots OK -+ [Invalid] bad_source_root OK -+ [Invalid] before_inclusion_delay OK -+ [Invalid] empty_participants_seemingly_valid_sig OK -+ [Invalid] empty_participants_zeroes_sig OK -+ [Invalid] future_target_epoch OK -+ [Invalid] invalid_attestation_signature OK -+ [Invalid] invalid_current_source_root OK -+ [Invalid] invalid_index OK -+ [Invalid] mismatched_target_and_slot OK -+ [Invalid] new_source_epoch OK -+ [Invalid] old_source_epoch OK -+ [Invalid] old_target_epoch OK -+ [Invalid] source_root_is_target_root OK -+ [Invalid] too_few_aggregation_bits OK -+ [Invalid] too_many_aggregation_bits OK -+ [Invalid] wrong_index_for_committee_signature OK -+ [Invalid] wrong_index_for_slot OK -+ [Valid] success OK -+ [Valid] success_multi_proposer_index_iterations OK -+ [Valid] success_previous_epoch OK -``` -OK: 21/21 Fail: 0/21 Skip: 0/21 -## Official - Operations - Attester slashing [Preset: minimal] -```diff -+ [Invalid] att1_bad_extra_index OK -+ [Invalid] att1_bad_replaced_index OK -+ [Invalid] att1_duplicate_index_double_signed OK -+ [Invalid] att1_duplicate_index_normal_signed OK -+ [Invalid] att2_bad_extra_index OK -+ [Invalid] att2_bad_replaced_index OK -+ [Invalid] att2_duplicate_index_double_signed OK -+ [Invalid] att2_duplicate_index_normal_signed OK -+ [Invalid] invalid_sig_1 OK -+ [Invalid] invalid_sig_1_and_2 OK -+ [Invalid] invalid_sig_2 OK -+ [Invalid] no_double_or_surround OK -+ [Invalid] participants_already_slashed OK -+ [Invalid] same_data OK -+ [Invalid] unsorted_att_1 OK -+ [Invalid] unsorted_att_2 OK -+ [Valid] success_already_exited_long_ago OK -+ [Valid] success_already_exited_recent OK -+ [Valid] success_double OK -+ [Valid] success_surround OK -``` -OK: 20/20 Fail: 0/20 Skip: 0/20 -## Official - Operations - Block header [Preset: minimal] -```diff -+ [Invalid] invalid_multiple_blocks_single_slot OK -+ [Invalid] invalid_parent_root OK -+ [Invalid] invalid_proposer_index OK -+ [Invalid] invalid_slot_block_header OK -+ [Invalid] proposer_slashed OK -+ [Valid] success_block_header OK -``` -OK: 6/6 Fail: 0/6 Skip: 0/6 -## Official - Operations - Deposits [Preset: minimal] -```diff -+ [Invalid] bad_merkle_proof OK -+ [Invalid] wrong_deposit_for_deposit_count OK -+ [Valid] invalid_sig_new_deposit OK -+ [Valid] invalid_sig_other_version OK -+ [Valid] invalid_sig_top_up OK -+ [Valid] invalid_withdrawal_credentials_top_up OK -+ [Valid] new_deposit_max OK -+ [Valid] new_deposit_over_max OK -+ [Valid] new_deposit_under_max OK -+ [Valid] success_top_up OK -+ [Valid] valid_sig_but_forked_state OK -``` -OK: 11/11 Fail: 0/11 Skip: 0/11 -## Official - Operations - Proposer slashing [Preset: minimal] -```diff -+ [Invalid] identifier OK -+ [Valid] identifier OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 -## Official - Operations - Voluntary exit [Preset: minimal] -```diff -+ [Invalid] invalid_signature OK -+ [Invalid] validator_already_exited OK -+ [Invalid] validator_exit_in_future OK -+ [Invalid] validator_invalid_validator_index OK -+ [Invalid] validator_not_active OK -+ [Invalid] validator_not_active_long_enough OK -+ [Valid] default_exit_epoch_subsequent_exit OK -+ [Valid] success OK -+ [Valid] success_exit_queue OK -``` -OK: 9/9 Fail: 0/9 Skip: 0/9 -## Official - Sanity - Blocks [Preset: minimal] -```diff -+ [Invalid] double_same_proposer_slashings_same_block OK -+ [Invalid] double_similar_proposer_slashings_same_block OK -+ [Invalid] double_validator_exit_same_block OK -+ [Invalid] duplicate_attester_slashing OK -+ [Invalid] expected_deposit_in_block OK -+ [Invalid] invalid_block_sig OK -+ [Invalid] invalid_proposer_index_sig_from_expected_proposer OK -+ [Invalid] invalid_proposer_index_sig_from_proposer_index OK -+ [Invalid] invalid_state_root OK -+ [Invalid] parent_from_same_slot OK -+ [Invalid] prev_slot_block_transition OK -+ [Invalid] proposal_for_genesis_slot OK -+ [Invalid] same_slot_block_transition OK -+ [Invalid] zero_block_sig OK -+ [Valid] attestation OK -+ [Valid] attester_slashing OK -+ [Valid] balance_driven_status_transitions OK -+ [Valid] deposit_in_block OK -+ [Valid] deposit_top_up OK -+ [Valid] empty_block_transition OK -+ [Valid] empty_epoch_transition OK -+ [Valid] empty_epoch_transition_not_finalizing OK -+ [Valid] high_proposer_index OK -+ [Valid] historical_batch OK -+ [Valid] multiple_attester_slashings_no_overlap OK -+ [Valid] multiple_attester_slashings_partial_overlap OK -+ [Valid] multiple_different_proposer_slashings_same_block OK -+ [Valid] multiple_different_validator_exits_same_block OK -+ [Valid] proposer_after_inactive_index OK -+ [Valid] proposer_slashing OK -+ [Valid] skipped_slots OK -+ [Valid] voluntary_exit OK -``` -OK: 32/32 Fail: 0/32 Skip: 0/32 -## Official - Sanity - Slots [Preset: minimal] -```diff -+ Slots - double_empty_epoch OK -+ Slots - empty_epoch OK -+ Slots - over_epoch_boundary OK -+ Slots - slots_1 OK -+ Slots - slots_2 OK -``` -OK: 5/5 Fail: 0/5 Skip: 0/5 - ----TOTAL--- -OK: 130/130 Fail: 0/130 Skip: 0/130 diff --git a/FixtureSSZConsensus-minimal.md b/FixtureSSZConsensus-minimal.md deleted file mode 100644 index 69044d5bd..000000000 --- a/FixtureSSZConsensus-minimal.md +++ /dev/null @@ -1,36 +0,0 @@ -FixtureSSZConsensus-minimal -=== -## Official - SSZ consensus objects [Preset: minimal] -```diff -+ Testing AggregateAndProof OK -+ Testing Attestation OK -+ Testing AttestationData OK -+ Testing AttesterSlashing OK -+ Testing BeaconBlock OK -+ Testing BeaconBlockBody OK -+ Testing BeaconBlockHeader OK -+ Testing BeaconState OK -+ Testing Checkpoint OK -+ Testing Deposit OK -+ Testing DepositData OK -+ Testing DepositMessage OK -+ Testing Eth1Block OK -+ Testing Eth1Data OK -+ Testing Fork OK -+ Testing ForkData OK -+ Testing HistoricalBatch OK -+ Testing IndexedAttestation OK -+ Testing PendingAttestation OK -+ Testing ProposerSlashing OK -+ Testing SignedAggregateAndProof OK -+ Testing SignedBeaconBlock OK -+ Testing SignedBeaconBlockHeader OK -+ Testing SignedVoluntaryExit OK -+ Testing SigningData OK -+ Testing Validator OK -+ Testing VoluntaryExit OK -``` -OK: 27/27 Fail: 0/27 Skip: 0/27 - ----TOTAL--- -OK: 27/27 Fail: 0/27 Skip: 0/27 diff --git a/FixtureSSZGeneric-minimal.md b/FixtureSSZGeneric-minimal.md deleted file mode 100644 index 7ab89707e..000000000 --- a/FixtureSSZGeneric-minimal.md +++ /dev/null @@ -1,21 +0,0 @@ -FixtureSSZGeneric-minimal -=== -## Official - SSZ generic types -```diff - Testing basic_vector inputs - invalid - skipping Vector[uint128, N] and Vector[uint256, N] Skip -+ Testing basic_vector inputs - valid - skipping Vector[uint128, N] and Vector[uint256, N] OK -+ Testing bitlist inputs - invalid OK -+ Testing bitlist inputs - valid OK - Testing bitvector inputs - invalid Skip -+ Testing bitvector inputs - valid OK -+ Testing boolean inputs - invalid OK -+ Testing boolean inputs - valid OK -+ Testing containers inputs - invalid - skipping BitsStruct OK -+ Testing containers inputs - valid - skipping BitsStruct OK -+ Testing uints inputs - invalid - skipping uint128 and uint256 OK -+ Testing uints inputs - valid - skipping uint128 and uint256 OK -``` -OK: 10/12 Fail: 0/12 Skip: 2/12 - ----TOTAL--- -OK: 10/12 Fail: 0/12 Skip: 2/12 From db870fead4ceda5f3217f8c16331e5e62cc208e1 Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Thu, 18 Jun 2020 10:16:05 +0200 Subject: [PATCH 66/70] update 20 beacon chain protocol spec refs --- beacon_chain/spec/crypto.nim | 2 +- beacon_chain/spec/datatypes.nim | 8 ++++---- beacon_chain/spec/presets/minimal.nim | 6 +++--- beacon_chain/spec/state_transition_block.nim | 4 ++-- beacon_chain/spec/state_transition_epoch.nim | 2 +- beacon_chain/spec/validator.nim | 16 ++++++++-------- beacon_chain/validator_duties.nim | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index ca7172147..4d1725a45 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -86,7 +86,7 @@ template `==`*[N, T](a: T, b: BlsValue[N, T]): bool = # API # ---------------------------------------------------------------------- -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#bls-signatures func toPubKey*(privkey: ValidatorPrivKey): ValidatorPubKey = ## Create a private key from a public key diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 424ae3d0c..bfef91348 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -129,7 +129,7 @@ type # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase1/custody-game.md#signature-domain-types DOMAIN_CUSTODY_BIT_SLASHING = 0x83 - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#custom-types + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#custom-types Domain* = array[32, byte] # https://github.com/nim-lang/Nim/issues/574 and be consistent across @@ -171,7 +171,7 @@ type Version* = distinct array[4, byte] ForkDigest* = distinct array[4, byte] - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#forkdata + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#forkdata ForkData* = object current_version*: Version genesis_validators_root*: Eth2Digest @@ -203,7 +203,7 @@ type data*: DepositData - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#depositmessage + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#depositmessage DepositMessage* = object pubkey*: ValidatorPubKey withdrawal_credentials*: Eth2Digest @@ -320,7 +320,7 @@ type BeaconStateRef* = ref BeaconStateObj not nil NilableBeaconStateRef* = ref BeaconStateObj - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#validator + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#validator Validator* = object pubkey*: ValidatorPubKey diff --git a/beacon_chain/spec/presets/minimal.nim b/beacon_chain/spec/presets/minimal.nim index 4dbb29e9c..e2ff572e3 100644 --- a/beacon_chain/spec/presets/minimal.nim +++ b/beacon_chain/spec/presets/minimal.nim @@ -20,7 +20,7 @@ type const # Misc # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L4 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L4 # Changed MAX_COMMITTEES_PER_SLOT* = 4 @@ -95,7 +95,7 @@ const # State vector lengths # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L105 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L105 # Changed EPOCHS_PER_HISTORICAL_VECTOR* = 64 @@ -127,7 +127,7 @@ const # Fork choice # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/configs/minimal.yaml#L32 + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/configs/minimal.yaml#L32 # Changed SAFE_SLOTS_TO_UPDATE_JUSTIFIED* = 2 diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index aa9cfdef2..497f3de57 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -99,7 +99,7 @@ proc `xor`[T: array](a, b: T): T = for i in 0.. Date: Thu, 18 Jun 2020 13:03:36 +0300 Subject: [PATCH 67/70] Forward sync refactoring. (#1191) * Forward sync refactoring. Rename Quarantine.pending to Quarantine.orphans. Removing "old" fields. * Fix test's FetchRecord. * Fix `checkResponse` to not allow duplicates in response. --- beacon_chain/beacon_node.nim | 26 +-- .../block_pools/block_pools_types.nim | 4 +- beacon_chain/block_pools/clearance.nim | 36 ++-- beacon_chain/block_pools/quarantine.nim | 25 ++- beacon_chain/request_manager.nim | 172 ++++++++++++------ tests/test_block_pool.nim | 2 +- 6 files changed, 160 insertions(+), 105 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index bb6a69a9a..3c4a2ad02 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -213,7 +213,6 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async nickname: nickname, network: network, netKeys: netKeys, - requestManager: RequestManager.init(network), db: db, config: conf, attachedValidators: ValidatorPool.init(), @@ -227,6 +226,11 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async topicAggregateAndProofs: topicAggregateAndProofs, ) + res.requestManager = RequestManager.init(network, + proc(signedBlock: SignedBeaconBlock) = + onBeaconBlock(res, signedBlock) + ) + traceAsyncErrors res.addLocalValidators() # This merely configures the BeaconSync @@ -501,21 +505,8 @@ proc handleMissingBlocks(node: BeaconNode) = let missingBlocks = node.blockPool.checkMissing() if missingBlocks.len > 0: var left = missingBlocks.len - - info "Requesting detected missing blocks", missingBlocks - node.requestManager.fetchAncestorBlocks(missingBlocks) do (b: SignedBeaconBlock): - onBeaconBlock(node, b) - - # TODO instead of waiting for a full second to try the next missing block - # fetching, we'll do it here again in case we get all blocks we asked - # for (there might be new parents to fetch). of course, this is not - # good because the onSecond fetching also kicks in regardless but - # whatever - this is just a quick fix for making the testnet easier - # work with while the sync problem is dealt with more systematically - # dec left - # if left == 0: - # discard setTimer(Moment.now()) do (p: pointer): - # handleMissingBlocks(node) + info "Requesting detected missing blocks", blocks = shortLog(missingBlocks) + node.requestManager.fetchAncestorBlocks(missingBlocks) proc onSecond(node: BeaconNode) {.async.} = ## This procedure will be called once per second. @@ -815,6 +806,8 @@ proc run*(node: BeaconNode) = node.onSecondLoop = runOnSecondLoop(node) node.forwardSyncLoop = runForwardSyncLoop(node) + node.requestManager.start() + # main event loop while status == BeaconNodeStatus.Running: try: @@ -1163,4 +1156,3 @@ programMain: config.depositContractAddress, config.depositPrivateKey, delayGenerator) - diff --git a/beacon_chain/block_pools/block_pools_types.nim b/beacon_chain/block_pools/block_pools_types.nim index b2e69406d..b83fbd3c1 100644 --- a/beacon_chain/block_pools/block_pools_types.nim +++ b/beacon_chain/block_pools/block_pools_types.nim @@ -37,7 +37,7 @@ type ## ## Invalid blocks are dropped immediately. - pending*: Table[Eth2Digest, SignedBeaconBlock] ##\ + orphans*: Table[Eth2Digest, SignedBeaconBlock] ##\ ## Blocks that have passed validation but that we lack a link back to tail ## for - when we receive a "missing link", we can use this data to build ## an entire branch @@ -49,12 +49,10 @@ type inAdd*: bool MissingBlock* = object - slots*: uint64 # number of slots that are suspected missing tries*: int FetchRecord* = object root*: Eth2Digest - historySlots*: uint64 CandidateChains* = ref object ## Pool of blocks responsible for keeping a DAG of resolved blocks. diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index f2798401b..6baf602f3 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -12,7 +12,7 @@ import metrics, stew/results, ../ssz/merkleization, ../state_transition, ../extras, ../spec/[crypto, datatypes, digest, helpers, signatures], - block_pools_types, candidate_chains + block_pools_types, candidate_chains, quarantine export results @@ -32,7 +32,7 @@ func getOrResolve*(dag: CandidateChains, quarantine: var Quarantine, root: Eth2D result = dag.getRef(root) if result.isNil: - quarantine.missing[root] = MissingBlock(slots: 1) + quarantine.missing[root] = MissingBlock() proc add*( dag: var CandidateChains, quarantine: var Quarantine, @@ -99,12 +99,12 @@ proc addResolvedBlock( defer: quarantine.inAdd = false var keepGoing = true while keepGoing: - let retries = quarantine.pending + let retries = quarantine.orphans for k, v in retries: discard add(dag, quarantine, k, v) # Keep going for as long as the pending dag is shrinking # TODO inefficient! so what? - keepGoing = quarantine.pending.len < retries.len + keepGoing = quarantine.orphans.len < retries.len blockRef proc add*( @@ -165,9 +165,9 @@ proc add*( return err Invalid - # The block might have been in either of pending or missing - we don't want - # any more work done on its behalf - quarantine.pending.del(blockRoot) + # The block might have been in either of `orphans` or `missing` - we don't + # want any more work done on its behalf + quarantine.orphans.del(blockRoot) # The block is resolved, now it's time to validate it to ensure that the # blocks we add to the database are clean for the given state @@ -209,7 +209,7 @@ proc add*( # the pending dag calls this function back later in a loop, so as long # as dag.add(...) requires a SignedBeaconBlock, easier to keep them in # pending too. - quarantine.pending[blockRoot] = signedBlock + quarantine.add(dag, signedBlock, some(blockRoot)) # TODO possibly, it makes sense to check the database - that would allow sync # to simply fill up the database with random blocks the other clients @@ -217,7 +217,7 @@ proc add*( # junk that's not part of the block graph if blck.parent_root in quarantine.missing or - blck.parent_root in quarantine.pending: + blck.parent_root in quarantine.orphans: return err MissingParent # This is an unresolved block - put its parent on the missing list for now... @@ -232,24 +232,11 @@ proc add*( # filter. # TODO when we receive the block, we don't know how many others we're missing # from that branch, so right now, we'll just do a blind guess - let parentSlot = blck.slot - 1 - - quarantine.missing[blck.parent_root] = MissingBlock( - slots: - # The block is at least two slots ahead - try to grab whole history - if parentSlot > dag.head.blck.slot: - parentSlot - dag.head.blck.slot - else: - # It's a sibling block from a branch that we're missing - fetch one - # epoch at a time - max(1.uint64, SLOTS_PER_EPOCH.uint64 - - (parentSlot.uint64 mod SLOTS_PER_EPOCH.uint64)) - ) debug "Unresolved block (parent missing)", blck = shortLog(blck), blockRoot = shortLog(blockRoot), - pending = quarantine.pending.len, + orphans = quarantine.orphans.len, missing = quarantine.missing.len, cat = "filtering" @@ -345,8 +332,7 @@ proc isValidBeaconBlock*( # not specific to this, but by the pending dag keying on the htr of the # BeaconBlock, not SignedBeaconBlock, opens up certain spoofing attacks. debug "parent unknown, putting block in quarantine" - quarantine.pending[hash_tree_root(signed_beacon_block.message)] = - signed_beacon_block + quarantine.add(dag, signed_beacon_block) return err(MissingParent) # The proposer signature, signed_beacon_block.signature, is valid with diff --git a/beacon_chain/block_pools/quarantine.nim b/beacon_chain/block_pools/quarantine.nim index d224d8ed0..08851a973 100644 --- a/beacon_chain/block_pools/quarantine.nim +++ b/beacon_chain/block_pools/quarantine.nim @@ -6,13 +6,15 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - chronicles, tables, + chronicles, tables, options, stew/bitops2, metrics, - ../spec/digest, - + ../spec/[datatypes, digest], + ../ssz/merkleization, block_pools_types +export options + logScope: topics = "quarant" {.push raises: [Defect].} @@ -35,4 +37,19 @@ func checkMissing*(quarantine: var Quarantine): seq[FetchRecord] = # simple (simplistic?) exponential backoff for retries.. for k, v in quarantine.missing.pairs(): if countOnes(v.tries.uint64) == 1: - result.add(FetchRecord(root: k, historySlots: v.slots)) + result.add(FetchRecord(root: k)) + +func add*(quarantine: var Quarantine, dag: CandidateChains, + sblck: SignedBeaconBlock, + broot: Option[Eth2Digest] = none[Eth2Digest]()) = + ## Adds block to quarantine's `orphans` and `missing` lists. + let blockRoot = if broot.isSome(): + broot.get() + else: + hash_tree_root(sblck.message) + + quarantine.orphans[blockRoot] = sblck + + let parentRoot = sblck.message.parent_root + if parentRoot notin quarantine.missing: + quarantine.missing[parentRoot] = MissingBlock() diff --git a/beacon_chain/request_manager.nim b/beacon_chain/request_manager.nim index 9ad7ec795..24e70d352 100644 --- a/beacon_chain/request_manager.nim +++ b/beacon_chain/request_manager.nim @@ -1,71 +1,133 @@ -import - options, random, - chronos, chronicles, - spec/datatypes, - eth2_network, beacon_node_types, sync_protocol, - eth/async_utils +import options, sequtils, strutils +import chronos, chronicles +import spec/[datatypes, digest], eth2_network, beacon_node_types, sync_protocol, + sync_manager, ssz/merkleization + +logScope: + topics = "requman" + +const + MAX_REQUEST_BLOCKS* = 4 # Specification's value is 1024. + ## Maximum number of blocks, which can be requested by beaconBlocksByRoot. + PARALLEL_REQUESTS* = 2 + ## Number of peers we using to resolve our request. type RequestManager* = object network*: Eth2Node + queue*: AsyncQueue[FetchRecord] + responseHandler*: FetchAncestorsResponseHandler + loopFuture: Future[void] -proc init*(T: type RequestManager, network: Eth2Node): T = - T(network: network) - -type FetchAncestorsResponseHandler = proc (b: SignedBeaconBlock) {.gcsafe.} -proc fetchAncestorBlocksFromPeer( - peer: Peer, - rec: FetchRecord, - responseHandler: FetchAncestorsResponseHandler) {.async.} = - # TODO: It's not clear if this function follows the intention of the - # FetchRecord data type. Perhaps it is supposed to get a range of blocks - # instead. In order to do this, we'll need the slot number of the known - # block to be stored in the FetchRecord, so we can ask for a range of - # blocks starting N positions before this slot number. - try: - let blocks = await peer.beaconBlocksByRoot(BlockRootsList @[rec.root]) - if blocks.isOk: - for b in blocks.get: - responseHandler(b) - except CatchableError as err: - debug "Error while fetching ancestor blocks", - err = err.msg, root = rec.root, peer = peer +func shortLog*(x: seq[Eth2Digest]): string = + "[" & x.mapIt(shortLog(it)).join(", ") & "]" -proc fetchAncestorBlocksFromNetwork( - network: Eth2Node, - rec: FetchRecord, - responseHandler: FetchAncestorsResponseHandler) {.async.} = +func shortLog*(x: seq[FetchRecord]): string = + "[" & x.mapIt(shortLog(it.root)).join(", ") & "]" + +proc init*(T: type RequestManager, network: Eth2Node, + responseCb: FetchAncestorsResponseHandler): T = + T( + network: network, queue: newAsyncQueue[FetchRecord](), + responseHandler: responseCb + ) + +proc checkResponse(roots: openArray[Eth2Digest], + blocks: openArray[SignedBeaconBlock]): bool = + ## This procedure checks peer's response. + var checks = @roots + if len(blocks) > len(roots): + return false + for blk in blocks: + let blockRoot = hash_tree_root(blk.message) + let res = checks.find(blockRoot) + if res == -1: + return false + else: + checks.del(res) + return true + +proc fetchAncestorBlocksFromNetwork(rman: RequestManager, + items: seq[Eth2Digest]) {.async.} = var peer: Peer try: - peer = await network.peerPool.acquire() - let blocks = await peer.beaconBlocksByRoot(BlockRootsList @[rec.root]) + peer = await rman.network.peerPool.acquire() + debug "Requesting blocks by root", peer = peer, blocks = shortLog(items), + peer_score = peer.getScore() + + let blocks = await peer.beaconBlocksByRoot(BlockRootsList items) if blocks.isOk: - for b in blocks.get: - responseHandler(b) - except CatchableError as err: - debug "Error while fetching ancestor blocks", - err = err.msg, root = rec.root, peer = peer + let ublocks = blocks.get() + if checkResponse(items, ublocks): + for b in ublocks: + rman.responseHandler(b) + peer.updateScore(PeerScoreGoodBlocks) + else: + peer.updateScore(PeerScoreBadResponse) + else: + peer.updateScore(PeerScoreNoBlocks) + + except CancelledError as exc: + raise exc + except CatchableError as exc: + debug "Error while fetching ancestor blocks", exc = exc.msg, + items = shortLog(items), peer = peer, peer_score = peer.getScore() + raise exc finally: if not(isNil(peer)): - network.peerPool.release(peer) + rman.network.peerPool.release(peer) -proc fetchAncestorBlocks*(requestManager: RequestManager, - roots: seq[FetchRecord], - responseHandler: FetchAncestorsResponseHandler) = - # TODO: we could have some fancier logic here: - # - # * Keeps track of what was requested - # (this would give a little bit of time for the asked peer to respond) - # - # * Keep track of the average latency of each peer - # (we can give priority to peers with better latency) - # - const ParallelRequests = 2 +proc requestManagerLoop(rman: RequestManager) {.async.} = + var rootList = newSeq[Eth2Digest]() + var workers = newSeq[Future[void]](PARALLEL_REQUESTS) + while true: + try: + rootList.setLen(0) + let req = await rman.queue.popFirst() + rootList.add(req.root) - for i in 0 ..< ParallelRequests: - traceAsyncErrors fetchAncestorBlocksFromNetwork(requestManager.network, - roots.sample(), - responseHandler) + var count = min(MAX_REQUEST_BLOCKS - 1, len(rman.queue)) + while count > 0: + rootList.add(rman.queue.popFirstNoWait().root) + dec(count) + let start = SyncMoment.now(Slot(0)) + + for i in 0 ..< PARALLEL_REQUESTS: + workers[i] = rman.fetchAncestorBlocksFromNetwork(rootList) + + # We do not care about + await allFutures(workers) + + let finish = SyncMoment.now(Slot(0) + uint64(len(rootList))) + + var succeed = 0 + for worker in workers: + if worker.finished() and not(worker.failed()): + inc(succeed) + + debug "Request manager tick", blocks_count = len(rootList), + succeed = succeed, + failed = (len(workers) - succeed), + queue_size = len(rman.queue), + sync_speed = speed(start, finish) + + except CatchableError as exc: + debug "Got a problem in request manager", exc = exc.msg + +proc start*(rman: var RequestManager) = + ## Start Request Manager's loop. + rman.loopFuture = requestManagerLoop(rman) + +proc stop*(rman: RequestManager) = + ## Stop Request Manager's loop. + if not(isNil(rman.loopFuture)): + rman.loopFuture.cancel() + +proc fetchAncestorBlocks*(rman: RequestManager, roots: seq[FetchRecord]) = + ## Enqueue list missing blocks roots ``roots`` for download by + ## Request Manager ``rman``. + for item in roots: + rman.queue.addLastNoWait(item) diff --git a/tests/test_block_pool.nim b/tests/test_block_pool.nim index 53f1509be..d5642bc9b 100644 --- a/tests/test_block_pool.nim +++ b/tests/test_block_pool.nim @@ -178,7 +178,7 @@ suiteReport "Block pool processing" & preset(): check: pool.get(b2Root).isNone() # Unresolved, shouldn't show up - FetchRecord(root: b1Root, historySlots: 1) in pool.checkMissing() + FetchRecord(root: b1Root) in pool.checkMissing() check: pool.add(b1Root, b1).isOk From dc1a565b3ffe8c63a4980d364890627355ebc3f2 Mon Sep 17 00:00:00 2001 From: tersec Date: Thu, 18 Jun 2020 13:10:25 +0000 Subject: [PATCH 68/70] support v0.12.1 attestation topics in beacon node/inspector subscribing (#1187) * support v0.12.1 attestation topics in beacon node and inspector subscribing * bump is_valid_merkle_branch() spec ref --- beacon_chain/beacon_node.nim | 20 ++- beacon_chain/block_pools/candidate_chains.nim | 4 +- beacon_chain/inspector.nim | 8 +- beacon_chain/spec/beaconstate.nim | 2 +- beacon_chain/spec/helpers.nim | 13 +- beacon_chain/spec/network.nim | 50 +++++-- beacon_chain/spec/presets/mainnet.nim | 2 +- tests/test_honest_validator.nim | 128 ++++++++++++------ 8 files changed, 159 insertions(+), 68 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 3c4a2ad02..a477c6171 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -742,12 +742,20 @@ proc installAttestationHandlers(node: BeaconNode) = for it in 0'u64 ..< ATTESTATION_SUBNET_COUNT.uint64: closureScope: let ci = it - attestationSubscriptions.add(node.network.subscribe( - getMainnetAttestationTopic(node.forkDigest, ci), attestationHandler, - # This proc needs to be within closureScope; don't lift out of loop. - proc(attestation: Attestation): bool = - attestationValidator(attestation, ci) - )) + when ETH2_SPEC == "v0.12.1": + attestationSubscriptions.add(node.network.subscribe( + getAttestationTopic(node.forkDigest, ci), attestationHandler, + # This proc needs to be within closureScope; don't lift out of loop. + proc(attestation: Attestation): bool = + attestationValidator(attestation, ci) + )) + else: + attestationSubscriptions.add(node.network.subscribe( + getMainnetAttestationTopic(node.forkDigest, ci), attestationHandler, + # This proc needs to be within closureScope; don't lift out of loop. + proc(attestation: Attestation): bool = + attestationValidator(attestation, ci) + )) when ETH2_SPEC == "v0.11.3": # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#interop-3 diff --git a/beacon_chain/block_pools/candidate_chains.nim b/beacon_chain/block_pools/candidate_chains.nim index bb22ef600..1a5df91ed 100644 --- a/beacon_chain/block_pools/candidate_chains.nim +++ b/beacon_chain/block_pools/candidate_chains.nim @@ -53,7 +53,7 @@ func parent*(bs: BlockSlot): BlockSlot = slot: bs.slot - 1 ) -func populateEpochCache*(state: BeaconState, epoch: Epoch): EpochRef = +func populateEpochCache(state: BeaconState, epoch: Epoch): EpochRef = result = (EpochRef)( epoch: state.slot.compute_epoch_at_slot, shuffled_active_validator_indices: @@ -148,7 +148,7 @@ func getEpochInfo*(blck: BlockRef, state: BeaconState): EpochRef = if matching_epochinfo.len == 0: let cache = populateEpochCache(state, state_epoch) blck.epochsInfo.add(cache) - trace "candidate_chains.skipAndUpdateState(): back-filling parent.epochInfo", + trace "candidate_chains.getEpochInfo: back-filling parent.epochInfo", state_slot = state.slot cache elif matching_epochinfo.len == 1: diff --git a/beacon_chain/inspector.nim b/beacon_chain/inspector.nim index 6e676bfbf..dda016887 100644 --- a/beacon_chain/inspector.nim +++ b/beacon_chain/inspector.nim @@ -207,8 +207,12 @@ func getTopics(forkDigest: ForkDigest, var topics = newSeq[string](ATTESTATION_SUBNET_COUNT * 2) var offset = 0 for i in 0'u64 ..< ATTESTATION_SUBNET_COUNT.uint64: - topics[offset] = getMainnetAttestationTopic(forkDigest, i) - topics[offset + 1] = getMainnetAttestationTopic(forkDigest, i) & "_snappy" + when ETH2_SPEC == "v0.12.1": + topics[offset] = getAttestationTopic(forkDigest, i) + topics[offset + 1] = getAttestationTopic(forkDigest, i) & "_snappy" + else: + topics[offset] = getMainnetAttestationTopic(forkDigest, i) + topics[offset + 1] = getMainnetAttestationTopic(forkDigest, i) & "_snappy" offset += 2 topics diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 7287850e5..45a3e8216 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -14,7 +14,7 @@ import ./crypto, ./datatypes, ./digest, ./helpers, ./signatures, ./validator, ../../nbench/bench_lab -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_valid_merkle_branch +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_valid_merkle_branch func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openarray[Eth2Digest], depth: uint64, index: uint64, root: Eth2Digest): bool {.nbench.}= ## Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and ## ``branch``. diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 879fdd46e..6d9416df6 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -64,7 +64,13 @@ func get_active_validator_indices*(state: BeaconState, epoch: Epoch): if is_active_validator(val, epoch): result.add idx.ValidatorIndex -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_committee_count_at_slot +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_committee_count_at_slot +func get_committee_count_at_slot*(num_active_validators: auto): + uint64 = + clamp( + num_active_validators div SLOTS_PER_EPOCH div TARGET_COMMITTEE_SIZE, + 1, MAX_COMMITTEES_PER_SLOT).uint64 + func get_committee_count_at_slot*(state: BeaconState, slot: Slot): uint64 = # Return the number of committees at ``slot``. @@ -74,10 +80,7 @@ func get_committee_count_at_slot*(state: BeaconState, slot: Slot): uint64 = # CommitteeIndex return type here. let epoch = compute_epoch_at_slot(slot) let active_validator_indices = get_active_validator_indices(state, epoch) - let committees_per_slot = clamp( - len(active_validator_indices) div SLOTS_PER_EPOCH div TARGET_COMMITTEE_SIZE, - 1, MAX_COMMITTEES_PER_SLOT).uint64 - result = committees_per_slot + result = get_committee_count_at_slot(len(active_validator_indices)) # Otherwise, get_beacon_committee(...) cannot access some committees. doAssert (SLOTS_PER_EPOCH * MAX_COMMITTEES_PER_SLOT).uint64 >= result diff --git a/beacon_chain/spec/network.nim b/beacon_chain/spec/network.nim index 0c237fb36..3f1e7426c 100644 --- a/beacon_chain/spec/network.nim +++ b/beacon_chain/spec/network.nim @@ -12,14 +12,17 @@ import datatypes const + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#topics-and-messages topicBeaconBlocksSuffix* = "beacon_block/ssz" - topicMainnetAttestationsSuffix* = "_beacon_attestation/ssz" topicVoluntaryExitsSuffix* = "voluntary_exit/ssz" topicProposerSlashingsSuffix* = "proposer_slashing/ssz" topicAttesterSlashingsSuffix* = "attester_slashing/ssz" topicAggregateAndProofsSuffix* = "beacon_aggregate_and_proof/ssz" - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#configuration + # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/p2p-interface.md#topics-and-messages + topicMainnetAttestationsSuffix* = "_beacon_attestation/ssz" + + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#misc ATTESTATION_SUBNET_COUNT* = 64 defaultEth2TcpPort* = 9000 @@ -30,35 +33,30 @@ const when ETH2_SPEC == "v0.11.3": const topicInteropAttestationSuffix* = "beacon_attestation/ssz" -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#topics-and-messages func getBeaconBlocksTopic*(forkDigest: ForkDigest): string = try: &"/eth2/{$forkDigest}/{topicBeaconBlocksSuffix}" except ValueError as e: raiseAssert e.msg -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#topics-and-messages func getVoluntaryExitsTopic*(forkDigest: ForkDigest): string = try: &"/eth2/{$forkDigest}/{topicVoluntaryExitsSuffix}" except ValueError as e: raiseAssert e.msg -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#topics-and-messages func getProposerSlashingsTopic*(forkDigest: ForkDigest): string = try: &"/eth2/{$forkDigest}/{topicProposerSlashingsSuffix}" except ValueError as e: raiseAssert e.msg -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#topics-and-messages func getAttesterSlashingsTopic*(forkDigest: ForkDigest): string = try: &"/eth2/{$forkDigest}/{topicAttesterSlashingsSuffix}" except ValueError as e: raiseAssert e.msg -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#topics-and-messages func getAggregateAndProofsTopic*(forkDigest: ForkDigest): string = try: &"/eth2/{$forkDigest}/{topicAggregateAndProofsSuffix}" @@ -72,10 +70,44 @@ when ETH2_SPEC == "v0.11.3": except ValueError as e: raiseAssert e.msg -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#mainnet-3 +# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/p2p-interface.md#mainnet-3 func getMainnetAttestationTopic*(forkDigest: ForkDigest, committeeIndex: uint64): string = + let topicIndex = committeeIndex mod ATTESTATION_SUBNET_COUNT try: - let topicIndex = committeeIndex mod ATTESTATION_SUBNET_COUNT &"/eth2/{$forkDigest}/committee_index{topicIndex}{topicMainnetAttestationsSuffix}" except ValueError as e: raiseAssert e.msg + +when ETH2_SPEC == "v0.12.1": + import helpers + + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-attestation + func compute_subnet_for_attestation*( + num_active_validators: uint64, attestation: Attestation): uint64 = + # Compute the correct subnet for an attestation for Phase 0. + # Note, this mimics expected Phase 1 behavior where attestations will be + # mapped to their shard subnet. + # + # The spec version has params (state: BeaconState, attestation: Attestation), + # but it's only to call get_committee_count_at_slot(), which needs only epoch + # and the number of active validators. + let + slots_since_epoch_start = attestation.data.slot mod SLOTS_PER_EPOCH + committees_since_epoch_start = + get_committee_count_at_slot(num_active_validators) * slots_since_epoch_start + + (committees_since_epoch_start + attestation.data.index) mod ATTESTATION_SUBNET_COUNT + + # https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#broadcast-attestation + func getAttestationTopic*(forkDigest: ForkDigest, subnetIndex: uint64): + string = + # This is for subscribing or broadcasting manually to a known index. + try: + &"/eth2/{$forkDigest}/beacon_attestation_{subnetIndex}/ssz" + except ValueError as e: + raiseAssert e.msg + + func getAttestationTopic*(forkDigest: ForkDigest, attestation: Attestation, num_active_validators: uint64): string = + getAttestationTopic( + forkDigest, + compute_subnet_for_attestation(num_active_validators, attestation)) diff --git a/beacon_chain/spec/presets/mainnet.nim b/beacon_chain/spec/presets/mainnet.nim index e32626834..cb1e7168f 100644 --- a/beacon_chain/spec/presets/mainnet.nim +++ b/beacon_chain/spec/presets/mainnet.nim @@ -24,7 +24,7 @@ const MAX_COMMITTEES_PER_SLOT* {.intdefine.} = 64 - TARGET_COMMITTEE_SIZE* = 2^7 ##\ + TARGET_COMMITTEE_SIZE* = 128 ##\ ## Number of validators in the committee attesting to one shard ## Per spec: ## For the safety of crosslinks `TARGET_COMMITTEE_SIZE` exceeds diff --git a/tests/test_honest_validator.nim b/tests/test_honest_validator.nim index 1ecb9b502..1ae8b90b5 100644 --- a/tests/test_honest_validator.nim +++ b/tests/test_honest_validator.nim @@ -19,45 +19,89 @@ suiteReport "Honest validator": true getAggregateAndProofsTopic(forkDigest) == "/eth2/00000000/beacon_aggregate_and_proof/ssz" - timedTest "Mainnet attestation topics": - check: - getMainnetAttestationTopic(forkDigest, 0) == - "/eth2/00000000/committee_index0_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 9) == - "/eth2/00000000/committee_index9_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 10) == - "/eth2/00000000/committee_index10_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 11) == - "/eth2/00000000/committee_index11_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 14) == - "/eth2/00000000/committee_index14_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 22) == - "/eth2/00000000/committee_index22_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 34) == - "/eth2/00000000/committee_index34_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 46) == - "/eth2/00000000/committee_index46_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 60) == - "/eth2/00000000/committee_index60_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 63) == - "/eth2/00000000/committee_index63_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 200) == - "/eth2/00000000/committee_index8_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 400) == - "/eth2/00000000/committee_index16_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 469) == - "/eth2/00000000/committee_index21_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 550) == - "/eth2/00000000/committee_index38_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 600) == - "/eth2/00000000/committee_index24_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 613) == - "/eth2/00000000/committee_index37_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 733) == - "/eth2/00000000/committee_index29_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 775) == - "/eth2/00000000/committee_index7_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 888) == - "/eth2/00000000/committee_index56_beacon_attestation/ssz" - getMainnetAttestationTopic(forkDigest, 995) == - "/eth2/00000000/committee_index35_beacon_attestation/ssz" + when ETH2_SPEC == "v0.11.3": + timedTest "Mainnet attestation topics": + check: + getMainnetAttestationTopic(forkDigest, 0) == + "/eth2/00000000/committee_index0_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 9) == + "/eth2/00000000/committee_index9_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 10) == + "/eth2/00000000/committee_index10_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 11) == + "/eth2/00000000/committee_index11_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 14) == + "/eth2/00000000/committee_index14_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 22) == + "/eth2/00000000/committee_index22_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 34) == + "/eth2/00000000/committee_index34_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 46) == + "/eth2/00000000/committee_index46_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 60) == + "/eth2/00000000/committee_index60_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 63) == + "/eth2/00000000/committee_index63_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 200) == + "/eth2/00000000/committee_index8_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 400) == + "/eth2/00000000/committee_index16_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 469) == + "/eth2/00000000/committee_index21_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 550) == + "/eth2/00000000/committee_index38_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 600) == + "/eth2/00000000/committee_index24_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 613) == + "/eth2/00000000/committee_index37_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 733) == + "/eth2/00000000/committee_index29_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 775) == + "/eth2/00000000/committee_index7_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 888) == + "/eth2/00000000/committee_index56_beacon_attestation/ssz" + getMainnetAttestationTopic(forkDigest, 995) == + "/eth2/00000000/committee_index35_beacon_attestation/ssz" + else: + timedTest "Mainnet attestation topics": + check: + getAttestationTopic(forkDigest, 0) == + "/eth2/00000000/beacon_attestation_0/ssz" + getAttestationTopic(forkDigest, 5) == + "/eth2/00000000/beacon_attestation_5/ssz" + getAttestationTopic(forkDigest, 7) == + "/eth2/00000000/beacon_attestation_7/ssz" + getAttestationTopic(forkDigest, 9) == + "/eth2/00000000/beacon_attestation_9/ssz" + getAttestationTopic(forkDigest, 13) == + "/eth2/00000000/beacon_attestation_13/ssz" + getAttestationTopic(forkDigest, 19) == + "/eth2/00000000/beacon_attestation_19/ssz" + getAttestationTopic(forkDigest, 20) == + "/eth2/00000000/beacon_attestation_20/ssz" + getAttestationTopic(forkDigest, 22) == + "/eth2/00000000/beacon_attestation_22/ssz" + getAttestationTopic(forkDigest, 25) == + "/eth2/00000000/beacon_attestation_25/ssz" + getAttestationTopic(forkDigest, 27) == + "/eth2/00000000/beacon_attestation_27/ssz" + getAttestationTopic(forkDigest, 31) == + "/eth2/00000000/beacon_attestation_31/ssz" + getAttestationTopic(forkDigest, 39) == + "/eth2/00000000/beacon_attestation_39/ssz" + getAttestationTopic(forkDigest, 45) == + "/eth2/00000000/beacon_attestation_45/ssz" + getAttestationTopic(forkDigest, 47) == + "/eth2/00000000/beacon_attestation_47/ssz" + getAttestationTopic(forkDigest, 48) == + "/eth2/00000000/beacon_attestation_48/ssz" + getAttestationTopic(forkDigest, 50) == + "/eth2/00000000/beacon_attestation_50/ssz" + getAttestationTopic(forkDigest, 53) == + "/eth2/00000000/beacon_attestation_53/ssz" + getAttestationTopic(forkDigest, 54) == + "/eth2/00000000/beacon_attestation_54/ssz" + getAttestationTopic(forkDigest, 62) == + "/eth2/00000000/beacon_attestation_62/ssz" + getAttestationTopic(forkDigest, 63) == + "/eth2/00000000/beacon_attestation_63/ssz" From 72dfe7f578b80ac3f3dd1c92ece311f0a7ebafc9 Mon Sep 17 00:00:00 2001 From: Viktor Kirilov Date: Fri, 19 Jun 2020 12:21:17 +0300 Subject: [PATCH 69/70] - updated the validator shell script after the keystore changes - better logging & retrying requests on the VC side if the BN fails for some reason - VC now fetches the attestation duties 1 epoch in advance - in the future it will tell the BN to subscribe to the appropriate attestation topics in advance based on that info - a bunch of other code cleanup & fixes such as better naming for consoles when using multitail, etc. reviewed in PR #1184 - proper review of the API & VC are pending --- README.md | 2 + .../spec/eth2_apis/beacon_callsigs.nim | 16 +- .../spec/eth2_apis/callsigs_types.nim | 23 +++ .../spec/eth2_apis/validator_callsigs.nim | 28 ++- .../eth2_apis/validator_callsigs_types.nim | 27 --- beacon_chain/validator_api.nim | 161 +++++++++++------- beacon_chain/validator_client.nim | 97 +++++++---- tests/simulation/run_node.sh | 2 +- tests/simulation/run_validator.sh | 28 +-- tests/simulation/start.sh | 9 +- tests/simulation/vars.sh | 3 - 11 files changed, 238 insertions(+), 158 deletions(-) create mode 100644 beacon_chain/spec/eth2_apis/callsigs_types.nim delete mode 100644 beacon_chain/spec/eth2_apis/validator_callsigs_types.nim diff --git a/README.md b/README.md index fb776e3a9..7a3ae733c 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,8 @@ make VALIDATORS=192 NODES=6 USER_NODES=1 eth2_network_simulation # looks like from a single nodes' perspective. ``` +By default all validators are loaded within the beacon nodes, but if you want to use external processes as validator clients you can pass `BN_VC_VALIDATOR_SPLIT=yes` as an additional argument to the `make eth2_network_simulation` command and that will split the `VALIDATORS` between beacon nodes and validator clients - for example with `192` validators and `6` nodes you will end up with 6 beacon node and 6 validator client processes, where each of them will handle 16 validators. + You can also separate the output from each beacon node in its own panel, using [multitail](http://www.vanheusden.com/multitail/): ```bash diff --git a/beacon_chain/spec/eth2_apis/beacon_callsigs.nim b/beacon_chain/spec/eth2_apis/beacon_callsigs.nim index 0bba59fba..b4e082cc2 100644 --- a/beacon_chain/spec/eth2_apis/beacon_callsigs.nim +++ b/beacon_chain/spec/eth2_apis/beacon_callsigs.nim @@ -1,6 +1,20 @@ import options, - ../datatypes + ../[datatypes, digest, crypto], + json_rpc/jsonmarshal, + callsigs_types + +proc get_v1_beacon_genesis(): BeaconGenesisTuple + +# TODO stateId is part of the REST path +proc get_v1_beacon_states_root(stateId: string): Eth2Digest + +# TODO stateId is part of the REST path +proc get_v1_beacon_states_fork(stateId: string): Fork + + + +# TODO: delete old stuff # https://github.com/ethereum/eth2.0-APIs/blob/master/apis/beacon/basic.md # diff --git a/beacon_chain/spec/eth2_apis/callsigs_types.nim b/beacon_chain/spec/eth2_apis/callsigs_types.nim new file mode 100644 index 000000000..d90ffce9b --- /dev/null +++ b/beacon_chain/spec/eth2_apis/callsigs_types.nim @@ -0,0 +1,23 @@ +import + # Standard library + options, + # Local modules + # TODO for some reason "../[datatypes, digest, crypto]" results in "Error: cannot open file" + ../datatypes, + ../digest, + ../crypto + +type + AttesterDuties* = tuple + public_key: ValidatorPubKey + committee_index: CommitteeIndex + committee_length: uint64 + validator_committee_index: uint64 + slot: Slot + + ValidatorPubkeySlotPair* = tuple[public_key: ValidatorPubKey, slot: Slot] + + BeaconGenesisTuple* = tuple + genesis_time: uint64 + genesis_validators_root: Eth2Digest + genesis_fork_version: Version diff --git a/beacon_chain/spec/eth2_apis/validator_callsigs.nim b/beacon_chain/spec/eth2_apis/validator_callsigs.nim index af191d0b8..18b35f44f 100644 --- a/beacon_chain/spec/eth2_apis/validator_callsigs.nim +++ b/beacon_chain/spec/eth2_apis/validator_callsigs.nim @@ -4,24 +4,17 @@ import # Local modules ../[datatypes, digest, crypto], json_rpc/jsonmarshal, - validator_callsigs_types - -# TODO check which arguments are part of the path in the REST API + callsigs_types +# calls that return a bool are actually without a return type in the main REST API +# spec but nim-json-rpc requires that all RPC calls have a return type. -# TODO this doesn't have "validator" in it's path but is used by the validators nonetheless -proc get_v1_beacon_states_fork(stateId: string): Fork - -# TODO this doesn't have "validator" in it's path but is used by the validators nonetheless -proc get_v1_beacon_genesis(): BeaconGenesisTuple - -# TODO returns a bool even though in the API there is no return type - because of nim-json-rpc proc post_v1_beacon_pool_attestations(attestation: Attestation): bool +# TODO slot is part of the REST path proc get_v1_validator_blocks(slot: Slot, graffiti: Eth2Digest, randao_reveal: ValidatorSig): BeaconBlock -# TODO returns a bool even though in the API there is no return type - because of nim-json-rpc proc post_v1_beacon_blocks(body: SignedBeaconBlock): bool proc get_v1_validator_attestation_data(slot: Slot, committee_index: CommitteeIndex): AttestationData @@ -31,16 +24,17 @@ proc get_v1_validator_attestation_data(slot: Slot, committee_index: CommitteeInd # https://docs.google.com/spreadsheets/d/1kVIx6GvzVLwNYbcd-Fj8YUlPf4qGrWUlS35uaTnIAVg/edit?disco=AAAAGh7r_fQ proc get_v1_validator_aggregate_attestation(attestation_data: AttestationData): Attestation -# TODO returns a bool even though in the API there is no return type - because of nim-json-rpc proc post_v1_validator_aggregate_and_proof(payload: SignedAggregateAndProof): bool # this is a POST instead of a GET because of this: https://docs.google.com/spreadsheets/d/1kVIx6GvzVLwNYbcd-Fj8YUlPf4qGrWUlS35uaTnIAVg/edit?disco=AAAAJk5rbKA +# TODO epoch is part of the REST path proc post_v1_validator_duties_attester(epoch: Epoch, public_keys: seq[ValidatorPubKey]): seq[AttesterDuties] +# TODO epoch is part of the REST path proc get_v1_validator_duties_proposer(epoch: Epoch): seq[ValidatorPubkeySlotPair] -proc post_v1_validator_beacon_committee_subscription(committee_index: CommitteeIndex, - slot: Slot, - aggregator: bool, - validator_pubkey: ValidatorPubKey, - slot_signature: ValidatorSig) +proc post_v1_validator_beacon_committee_subscriptions(committee_index: CommitteeIndex, + slot: Slot, + aggregator: bool, + validator_pubkey: ValidatorPubKey, + slot_signature: ValidatorSig): bool diff --git a/beacon_chain/spec/eth2_apis/validator_callsigs_types.nim b/beacon_chain/spec/eth2_apis/validator_callsigs_types.nim deleted file mode 100644 index c4cd4d696..000000000 --- a/beacon_chain/spec/eth2_apis/validator_callsigs_types.nim +++ /dev/null @@ -1,27 +0,0 @@ -import - # Standard library - options, - # Local modules - # TODO for some reason "../[datatypes, digest, crypto]" results in "Error: cannot open file" - ../datatypes, - ../digest, - ../crypto - -type - AttesterDuties* = object - public_key*: ValidatorPubKey - committee_index*: CommitteeIndex - committee_length*: uint64 - validator_committee_index*: uint64 - slot*: Slot - - # TODO do we even need this? how about a simple tuple (alias)? - ValidatorPubkeySlotPair* = object - public_key*: ValidatorPubKey - slot*: Slot - - # TODO do we even need this? how about a simple tuple (alias)? - BeaconGenesisTuple* = object - genesis_time*: uint64 - genesis_validators_root*: Eth2Digest - genesis_fork_version*: Version diff --git a/beacon_chain/validator_api.nim b/beacon_chain/validator_api.nim index 35c477e4f..44aca3cfb 100644 --- a/beacon_chain/validator_api.nim +++ b/beacon_chain/validator_api.nim @@ -7,7 +7,7 @@ import # Standard library - tables, strutils, + tables, strutils, parseutils, # Nimble packages stew/[objects], @@ -19,7 +19,7 @@ import block_pool, ssz/merkleization, beacon_node_common, beacon_node_types, validator_duties, eth2_network, - spec/eth2_apis/validator_callsigs_types, + spec/eth2_apis/callsigs_types, eth2_json_rpc_serialization type @@ -27,64 +27,102 @@ type logScope: topics = "valapi" +# TODO Probably the `beacon` ones should be defined elsewhere...? + proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) = - # TODO Probably the `beacon` ones (and not `validator`) should be defined elsewhere... - rpcServer.rpc("get_v1_beacon_states_fork") do (stateId: string) -> Fork: - notice "== get_v1_beacon_states_fork", stateId = stateId + template withStateForSlot(stateId: string, body: untyped): untyped = + var res: BiggestInt + if parseBiggestInt(stateId, res) == stateId.len: + raise newException(CatchableError, "Not a valid slot number") + let head = node.updateHead() + let blockSlot = head.atSlot(res.Slot) + node.blockPool.withState(node.blockPool.tmpState, blockSlot): + body + + rpcServer.rpc("get_v1_beacon_genesis") do () -> BeaconGenesisTuple: + debug "get_v1_beacon_genesis" + return (genesis_time: node.blockPool.headState.data.data.genesis_time, + genesis_validators_root: + node.blockPool.headState.data.data.genesis_validators_root, + genesis_fork_version: Version(GENESIS_FORK_VERSION)) + + rpcServer.rpc("get_v1_beacon_states_root") do (stateId: string) -> Eth2Digest: + debug "get_v1_beacon_states_root", stateId = stateId + # TODO do we need to call node.updateHead() before using headState? + result = case stateId: + of "head": + node.blockPool.headState.blck.root + of "genesis": + node.blockPool.headState.data.data.genesis_validators_root + of "finalized": + node.blockPool.headState.data.data.finalized_checkpoint.root + of "justified": + node.blockPool.headState.data.data.current_justified_checkpoint.root + else: + if stateId.startsWith("0x"): + # TODO not sure if `fromHex` is the right thing here... + # https://github.com/ethereum/eth2.0-APIs/issues/37#issuecomment-638566144 + # we return whatever was passed to us (this is a nonsense request) + fromHex(Eth2Digest, stateId[2.. Fork: + debug "get_v1_beacon_states_fork", stateId = stateId result = case stateId: of "head": - discard node.updateHead() # TODO do we need this? node.blockPool.headState.data.data.fork of "genesis": Fork(previous_version: Version(GENESIS_FORK_VERSION), current_version: Version(GENESIS_FORK_VERSION), epoch: GENESIS_EPOCH) of "finalized": - # TODO - Fork() + node.blockPool.withState(node.blockPool.tmpState, node.blockPool.finalizedHead): + state.fork of "justified": - # TODO - Fork() + node.blockPool.justifiedState.data.data.fork else: - # TODO parse `stateId` as either a number (slot) or a hash (stateRoot) - Fork() + if stateId.startsWith("0x"): + # TODO not sure if `fromHex` is the right thing here... + # https://github.com/ethereum/eth2.0-APIs/issues/37#issuecomment-638566144 + let blckRoot = fromHex(Eth2Digest, stateId[2.. BeaconGenesisTuple: - notice "== get_v1_beacon_genesis" - return BeaconGenesisTuple(genesis_time: node.blockPool.headState.data.data.genesis_time, - genesis_validators_root: node.blockPool.headState.data.data.genesis_validators_root, - genesis_fork_version: Version(GENESIS_FORK_VERSION)) - - rpcServer.rpc("post_v1_beacon_pool_attestations") do (attestation: Attestation) -> bool: - #notice "== post_v1_beacon_pool_attestations" + rpcServer.rpc("post_v1_beacon_pool_attestations") do ( + attestation: Attestation) -> bool: node.sendAttestation(attestation) return true rpcServer.rpc("get_v1_validator_blocks") do ( slot: Slot, graffiti: Eth2Digest, randao_reveal: ValidatorSig) -> BeaconBlock: - notice "== get_v1_validator_blocks", slot = slot + debug "get_v1_validator_blocks", slot = slot let head = node.updateHead() - let proposer = node.blockPool.getProposer(head, slot) - # TODO how do we handle the case when we cannot return a meaningful block? 404... - doAssert(proposer.isSome()) - + if proposer.isNone(): + raise newException(CatchableError, "could not retrieve block for slot: " & $slot) let valInfo = ValidatorInfoForMakeBeaconBlock(kind: viRandao_reveal, randao_reveal: randao_reveal) let res = makeBeaconBlockForHeadAndSlot( node, valInfo, proposer.get()[0], graffiti, head, slot) - - # TODO how do we handle the case when we cannot return a meaningful block? 404... - doAssert(res.message.isSome()) - return res.message.get(BeaconBlock()) # returning a default if empty + if res.message.isNone(): + raise newException(CatchableError, "could not retrieve block for slot: " & $slot) + return res.message.get() rpcServer.rpc("post_v1_beacon_blocks") do (body: SignedBeaconBlock) -> bool: - notice "== post_v1_beacon_blocks" + debug "post_v1_beacon_blocks", + slot = body.message.slot, + prop_idx = body.message.proposer_index - logScope: pcs = "block_proposal" - let head = node.updateHead() if head.slot >= body.message.slot: warn "Skipping proposal, have newer head already", @@ -92,14 +130,15 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) = headBlockRoot = shortLog(head.root), slot = shortLog(body.message.slot), cat = "fastforward" - return false - return head != await proposeSignedBlock(node, head, AttachedValidator(), - body, hash_tree_root(body.message)) + raise newException(CatchableError, + "Proposal is for a past slot: " & $body.message.slot) + if head == await proposeSignedBlock(node, head, AttachedValidator(), + body, hash_tree_root(body.message)): + raise newException(CatchableError, "Could not propose block") + return true rpcServer.rpc("get_v1_validator_attestation_data") do ( slot: Slot, committee_index: CommitteeIndex) -> AttestationData: - #notice "== get_v1_validator_attestation_data" - # Obtain the data to form an attestation let head = node.updateHead() let attestationHead = head.atSlot(slot) node.blockPool.withState(node.blockPool.tmpState, attestationHead): @@ -107,45 +146,43 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) = rpcServer.rpc("get_v1_validator_aggregate_attestation") do ( attestation_data: AttestationData)-> Attestation: - notice "== get_v1_validator_aggregate_attestation" + debug "get_v1_validator_aggregate_attestation" rpcServer.rpc("post_v1_validator_aggregate_and_proof") do ( payload: SignedAggregateAndProof) -> bool: - notice "== post_v1_validator_aggregate_and_proof" - # TODO is this enough? node.network.broadcast(node.topicAggregateAndProofs, payload) return true rpcServer.rpc("post_v1_validator_duties_attester") do ( epoch: Epoch, public_keys: seq[ValidatorPubKey]) -> seq[AttesterDuties]: - notice "== post_v1_validator_duties_attester", epoch = epoch - discard node.updateHead() # TODO do we need this? - for pubkey in public_keys: - let idx = node.blockPool.headState.data.data.validators.asSeq.findIt(it.pubKey == pubkey) - if idx != -1: - # TODO this might crash if the requested epoch is further than the BN epoch - # because of this: `doAssert epoch <= next_epoch` - let res = node.blockPool.headState.data.data.get_committee_assignment( - epoch, idx.ValidatorIndex) - if res.isSome: - result.add(AttesterDuties(public_key: pubkey, - committee_index: res.get.b, - committee_length: res.get.a.len.uint64, - validator_committee_index: res.get.a.find(idx.ValidatorIndex).uint64, - slot: res.get.c)) + debug "post_v1_validator_duties_attester", epoch = epoch + let head = node.updateHead() + let attestationHead = head.atSlot(compute_start_slot_at_epoch(epoch)) + node.blockPool.withState(node.blockPool.tmpState, attestationHead): + for pubkey in public_keys: + let idx = state.validators.asSeq.findIt(it.pubKey == pubkey) + if idx == -1: + continue + let ca = state.get_committee_assignment(epoch, idx.ValidatorIndex) + if ca.isSome: + result.add((public_key: pubkey, + committee_index: ca.get.b, + committee_length: ca.get.a.len.uint64, + validator_committee_index: ca.get.a.find(idx.ValidatorIndex).uint64, + slot: ca.get.c)) rpcServer.rpc("get_v1_validator_duties_proposer") do ( epoch: Epoch) -> seq[ValidatorPubkeySlotPair]: - notice "== get_v1_validator_duties_proposer", epoch = epoch + debug "get_v1_validator_duties_proposer", epoch = epoch let head = node.updateHead() for i in 0 ..< SLOTS_PER_EPOCH: let currSlot = (compute_start_slot_at_epoch(epoch).int + i).Slot let proposer = node.blockPool.getProposer(head, currSlot) if proposer.isSome(): - result.add(ValidatorPubkeySlotPair(public_key: proposer.get()[1], slot: currSlot)) + result.add((public_key: proposer.get()[1], slot: currSlot)) - rpcServer.rpc("post_v1_validator_beacon_committee_subscription") do ( + rpcServer.rpc("post_v1_validator_beacon_committee_subscriptions") do ( committee_index: CommitteeIndex, slot: Slot, aggregator: bool, - validator_pubkey: ValidatorPubKey, slot_signature: ValidatorSig): - notice "== post_v1_validator_beacon_committee_subscription" - # TODO + validator_pubkey: ValidatorPubKey, slot_signature: ValidatorSig) -> bool: + debug "post_v1_validator_beacon_committee_subscriptions" + raise newException(CatchableError, "Not implemented") diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index 273aa9b63..5dd298393 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -22,7 +22,7 @@ import nimbus_binary_common, version, ssz/merkleization, sync_manager, keystore_management, - spec/eth2_apis/validator_callsigs_types, + spec/eth2_apis/callsigs_types, eth2_json_rpc_serialization logScope: topics = "vc" @@ -31,6 +31,7 @@ template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0] ## Generate client convenience marshalling wrappers from forward declarations createRpcSigs(RpcClient, sourceDir / "spec" / "eth2_apis" / "validator_callsigs.nim") +createRpcSigs(RpcClient, sourceDir / "spec" / "eth2_apis" / "beacon_callsigs.nim") type ValidatorClient = ref object @@ -39,31 +40,66 @@ type beaconClock: BeaconClock attachedValidators: ValidatorPool fork: Fork - proposalsForEpoch: Table[Slot, ValidatorPubKey] - attestationsForEpoch: Table[Slot, seq[AttesterDuties]] + proposalsForCurrentEpoch: Table[Slot, ValidatorPubKey] + attestationsForEpoch: Table[Epoch, Table[Slot, seq[AttesterDuties]]] beaconGenesis: BeaconGenesisTuple +proc connectToBN(vc: ValidatorClient) {.gcsafe, async.} = + while true: + try: + await vc.client.connect($vc.config.rpcAddress, Port(vc.config.rpcPort)) + info "Connected to BN", + port = vc.config.rpcPort, + address = vc.config.rpcAddress + return + except CatchableError as err: + warn "Could not connect to the BN - retrying!", err = err.msg + await sleepAsync(chronos.seconds(1)) # 1 second before retrying + +template attemptUntilSuccess(vc: ValidatorClient, body: untyped) = + while true: + try: + body + break + except CatchableError as err: + warn "Caught an unexpected error", err = err.msg + waitFor vc.connectToBN() + proc getValidatorDutiesForEpoch(vc: ValidatorClient, epoch: Epoch) {.gcsafe, async.} = let proposals = await vc.client.get_v1_validator_duties_proposer(epoch) # update the block proposal duties this VC should do during this epoch - vc.proposalsForEpoch.clear() + vc.proposalsForCurrentEpoch.clear() for curr in proposals: if vc.attachedValidators.validators.contains curr.public_key: - vc.proposalsForEpoch.add(curr.slot, curr.public_key) + vc.proposalsForCurrentEpoch.add(curr.slot, curr.public_key) # couldn't use mapIt in ANY shape or form so reverting to raw loops - sorry Sean Parent :| var validatorPubkeys: seq[ValidatorPubKey] for key in vc.attachedValidators.validators.keys: validatorPubkeys.add key - # update the attestation duties this VC should do during this epoch - let attestations = await vc.client.post_v1_validator_duties_attester( - epoch, validatorPubkeys) - vc.attestationsForEpoch.clear() - for a in attestations: - if vc.attestationsForEpoch.hasKeyOrPut(a.slot, @[a]): - vc.attestationsForEpoch[a.slot].add(a) + + proc getAttesterDutiesForEpoch(epoch: Epoch) {.gcsafe, async.} = + let attestations = await vc.client.post_v1_validator_duties_attester( + epoch, validatorPubkeys) + # make sure there's an entry + if not vc.attestationsForEpoch.contains epoch: + vc.attestationsForEpoch.add(epoch, Table[Slot, seq[AttesterDuties]]()) + for a in attestations: + if vc.attestationsForEpoch[epoch].hasKeyOrPut(a.slot, @[a]): + vc.attestationsForEpoch[epoch][a.slot].add(a) + + # obtain the attestation duties this VC should do during the next epoch + await getAttesterDutiesForEpoch(epoch + 1) + # also get the attestation duties for the current epoch if missing + if not vc.attestationsForEpoch.contains epoch: + await getAttesterDutiesForEpoch(epoch) + # cleanup old epoch attestation duties + vc.attestationsForEpoch.del(epoch - 1) + # TODO handle subscriptions to beacon committees for both the next epoch and + # for the current if missing (beacon_committee_subscriptions from the REST api) # for now we will get the fork each time we update the validator duties for each epoch + # TODO should poll occasionally `/v1/config/fork_schedule` vc.fork = await vc.client.get_v1_beacon_states_fork("head") proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, async.} = @@ -76,6 +112,7 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a let slot = wallSlot.slot # afterGenesis == true! nextSlot = slot + 1 + epoch = slot.compute_epoch_at_slot info "Slot start", lastSlot = shortLog(lastSlot), @@ -91,11 +128,11 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a # could take up time for attesting... Perhaps this should be called more # than once per epoch because of forks & other events... if slot.isEpoch: - await getValidatorDutiesForEpoch(vc, slot.compute_epoch_at_slot) + await getValidatorDutiesForEpoch(vc, epoch) # check if we have a validator which needs to propose on this slot - if vc.proposalsForEpoch.contains slot: - let public_key = vc.proposalsForEpoch[slot] + if vc.proposalsForCurrentEpoch.contains slot: + let public_key = vc.proposalsForCurrentEpoch[slot] let validator = vc.attachedValidators.validators[public_key] let randao_reveal = validator.genRandaoReveal( @@ -121,8 +158,8 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a seconds(int64(SECONDS_PER_SLOT)) div 3, slot, "Waiting to send attestations") # check if we have validators which need to attest on this slot - if vc.attestationsForEpoch.contains slot: - for a in vc.attestationsForEpoch[slot]: + if vc.attestationsForEpoch[epoch].contains slot: + for a in vc.attestationsForEpoch[epoch][slot]: let validator = vc.attachedValidators.validators[a.public_key] let ad = await vc.client.get_v1_validator_attestation_data(slot, a.committee_index) @@ -135,7 +172,8 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a discard await vc.client.post_v1_beacon_pool_attestations(attestation) except CatchableError as err: - error "Caught an unexpected error", err = err.msg + warn "Caught an unexpected error", err = err.msg, slot = shortLog(slot) + await vc.connectToBN() let nextSlotStart = saturate(vc.beaconClock.fromNow(nextSlot)) @@ -177,34 +215,27 @@ programMain: var vc = ValidatorClient( config: config, - client: newRpcHttpClient(), - attachedValidators: ValidatorPool.init() + client: newRpcHttpClient() ) - vc.proposalsForEpoch.init() - vc.attestationsForEpoch.init() # load all the validators from the data dir into memory for curr in vc.config.validatorKeys: vc.attachedValidators.addLocalValidator(curr.toPubKey, curr) - # TODO perhaps we should handle the case if the BN is down and try to connect to it - # untill success, and also later on disconnets we should continue trying to reconnect - waitFor vc.client.connect("localhost", Port(config.rpcPort)) # TODO: use config.rpcAddress - info "Connected to beacon node", port = config.rpcPort + waitFor vc.connectToBN() - # init the beacon clock - vc.beaconGenesis = waitFor vc.client.get_v1_beacon_genesis() - vc.beaconClock = BeaconClock.init(vc.beaconGenesis.genesis_time) + vc.attemptUntilSuccess: + # init the beacon clock + vc.beaconGenesis = waitFor vc.client.get_v1_beacon_genesis() + vc.beaconClock = BeaconClock.init(vc.beaconGenesis.genesis_time) let curSlot = vc.beaconClock.now().slotOrZero() nextSlot = curSlot + 1 # No earlier than GENESIS_SLOT + 1 fromNow = saturate(vc.beaconClock.fromNow(nextSlot)) - # onSlotStart() requests the validator duties only on the start of each epoch - # so we should request the duties here when the VC binary boots up in order - # to handle the case when in the middle of an epoch. Also for the genesis slot. - waitFor vc.getValidatorDutiesForEpoch(curSlot.compute_epoch_at_slot) + vc.attemptUntilSuccess: + waitFor vc.getValidatorDutiesForEpoch(curSlot.compute_epoch_at_slot) info "Scheduling first slot action", beaconTime = shortLog(vc.beaconClock.now()), diff --git a/tests/simulation/run_node.sh b/tests/simulation/run_node.sh index 39621bc3d..b2d3c7eb3 100755 --- a/tests/simulation/run_node.sh +++ b/tests/simulation/run_node.sh @@ -53,7 +53,7 @@ VALIDATORS_PER_NODE=$((NUM_VALIDATORS / TOTAL_NODES)) if [[ $NODE_ID -lt $TOTAL_NODES ]]; then # if using validator client binaries in addition to beacon nodes # we will split the keys for this instance in half between the BN and the VC - if [ "${SPLIT_VALIDATORS_BETWEEN_BN_AND_VC:-}" == "yes" ]; then + if [ "${BN_VC_VALIDATOR_SPLIT:-}" == "yes" ]; then ATTACHED_VALIDATORS=$((VALIDATORS_PER_NODE / 2)) else ATTACHED_VALIDATORS=$VALIDATORS_PER_NODE diff --git a/tests/simulation/run_validator.sh b/tests/simulation/run_validator.sh index 8d5227c1e..8c0bda1c1 100755 --- a/tests/simulation/run_validator.sh +++ b/tests/simulation/run_validator.sh @@ -15,26 +15,34 @@ source "${SIM_ROOT}/../../env.sh" cd "$GIT_ROOT" -VC_DATA_DIR="${SIMULATION_DIR}/validator-$NODE_ID" +NODE_DATA_DIR="${SIMULATION_DIR}/validator-$NODE_ID" +NODE_VALIDATORS_DIR=$NODE_DATA_DIR/validators/ +NODE_SECRETS_DIR=$NODE_DATA_DIR/secrets/ -mkdir -p "$VC_DATA_DIR/validators" -rm -f $VC_DATA_DIR/validators/* +rm -rf "$NODE_VALIDATORS_DIR" +mkdir -p "$NODE_VALIDATORS_DIR" + +rm -rf "$NODE_SECRETS_DIR" +mkdir -p "$NODE_SECRETS_DIR" + +VALIDATORS_PER_NODE=$((NUM_VALIDATORS / TOTAL_NODES)) if [[ $NODE_ID -lt $TOTAL_NODES ]]; then # we will split the keys for this instance in half between the BN and the VC - VALIDATORS_PER_NODE=$((NUM_VALIDATORS / TOTAL_NODES)) - VALIDATORS_PER_NODE_HALF=$((VALIDATORS_PER_NODE / 2)) - FIRST_VALIDATOR_IDX=$(( VALIDATORS_PER_NODE * NODE_ID + VALIDATORS_PER_NODE_HALF)) - LAST_VALIDATOR_IDX=$(( FIRST_VALIDATOR_IDX + VALIDATORS_PER_NODE_HALF - 1 )) + ATTACHED_VALIDATORS=$((VALIDATORS_PER_NODE / 2)) pushd "$VALIDATORS_DIR" >/dev/null - cp $(seq -s " " -f v%07g.privkey $FIRST_VALIDATOR_IDX $LAST_VALIDATOR_IDX) "$VC_DATA_DIR/validators" + for VALIDATOR in $(ls | tail -n +$(( ($VALIDATORS_PER_NODE * $NODE_ID) + 1 + $ATTACHED_VALIDATORS )) | head -n $ATTACHED_VALIDATORS); do + cp -ar "$VALIDATOR" "$NODE_VALIDATORS_DIR" + cp -a "$SECRETS_DIR/$VALIDATOR" "$NODE_SECRETS_DIR" + done popd >/dev/null fi -cd "$VC_DATA_DIR" +cd "$NODE_DATA_DIR" $VALIDATOR_CLIENT_BIN \ --log-level=${LOG_LEVEL:-DEBUG} \ - --data-dir=$VC_DATA_DIR \ + --data-dir=$NODE_DATA_DIR \ + --secrets-dir=$NODE_SECRETS_DIR \ --rpc-port="$(( $BASE_RPC_PORT + $NODE_ID ))" diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index 22807cc68..26f521a5d 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -179,6 +179,7 @@ LAST_WAITING_NODE=0 function run_cmd { i=$1 CMD=$2 + bin_name=$3 if [[ "$USE_TMUX" != "no" ]]; then echo "Starting node $i..." echo $TMUX split-window -t "${TMUX_SESSION_NAME}" "$CMD" @@ -191,7 +192,7 @@ function run_cmd { SLEEP="3" fi # "multitail" closes the corresponding panel when a command exits, so let's make sure it doesn't exit - COMMANDS+=( " -cT ansi -t 'node #$i' -l 'sleep $SLEEP; $CMD; echo [node execution completed]; while true; do sleep 100; done'" ) + COMMANDS+=( " -cT ansi -t '$bin_name #$i' -l 'sleep $SLEEP; $CMD; echo [node execution completed]; while true; do sleep 100; done'" ) else eval "${CMD}" & fi @@ -209,11 +210,11 @@ for i in $(seq $MASTER_NODE -1 $TOTAL_USER_NODES); do done fi - run_cmd $i "${SIM_ROOT}/run_node.sh ${i} --verify-finalization" + run_cmd $i "${SIM_ROOT}/run_node.sh ${i} --verify-finalization" "node" - if [ "${SPLIT_VALIDATORS_BETWEEN_BN_AND_VC:-}" == "yes" ]; then + if [ "${BN_VC_VALIDATOR_SPLIT:-}" == "yes" ]; then # start the VC with a few seconds of delay so that we can connect through RPC - run_cmd $i "sleep 3 && ${SIM_ROOT}/run_validator.sh ${i}" + run_cmd $i "sleep 3 && ${SIM_ROOT}/run_validator.sh ${i}" "validator" fi done diff --git a/tests/simulation/vars.sh b/tests/simulation/vars.sh index 8304bce5e..7e7574692 100644 --- a/tests/simulation/vars.sh +++ b/tests/simulation/vars.sh @@ -46,6 +46,3 @@ else WEB3_ARG="" DEPOSIT_CONTRACT_ADDRESS="0x" fi - -# uncomment to enable the use of VCs in addition to BNs - will split the validators equally -#SPLIT_VALIDATORS_BETWEEN_BN_AND_VC="yes" From 4ecbc655eaf72bd859191698a0058ddeb85ef4b3 Mon Sep 17 00:00:00 2001 From: Dustin Brody Date: Fri, 19 Jun 2020 13:48:23 +0200 Subject: [PATCH 70/70] switch 11 beaconstate.nim spec refs from v0.11.x to v0.12.1 --- beacon_chain/spec/beaconstate.nim | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 45a3e8216..e77d77d39 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -32,24 +32,24 @@ func is_valid_merkle_branch*(leaf: Eth2Digest, branch: openarray[Eth2Digest], de value = eth2digest(buf) value == root -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#increase_balance +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#increase_balance func increase_balance*( state: var BeaconState, index: ValidatorIndex, delta: Gwei) = # Increase the validator balance at index ``index`` by ``delta``. state.balances[index] += delta -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#decrease_balance +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#decrease_balance func decrease_balance*( state: var BeaconState, index: ValidatorIndex, delta: Gwei) = - ## Decrease the validator balance at index ``index`` by ``delta``, with - ## underflow protection. + # Decrease the validator balance at index ``index`` by ``delta``, with + # underflow protection. state.balances[index] = if delta > state.balances[index]: 0'u64 else: state.balances[index] - delta -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#deposits +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#deposits proc process_deposit*( state: var BeaconState, deposit: Deposit, flags: UpdateFlags = {}): bool {.nbench.}= # Process an Eth1 deposit, registering a validator or increasing its balance. @@ -111,7 +111,7 @@ func compute_activation_exit_epoch(epoch: Epoch): Epoch = ## ``epoch`` take effect. epoch + 1 + MAX_SEED_LOOKAHEAD -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_validator_churn_limit +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_validator_churn_limit func get_validator_churn_limit(state: BeaconState, cache: var StateCache): uint64 = # Return the validator churn limit for the current epoch. @@ -119,7 +119,7 @@ func get_validator_churn_limit(state: BeaconState, cache: var StateCache): len(cache.shuffled_active_validator_indices) div CHURN_LIMIT_QUOTIENT).uint64 -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#initiate_validator_exit +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#initiate_validator_exit func initiate_validator_exit*(state: var BeaconState, index: ValidatorIndex, cache: var StateCache) = # Initiate the exit of the validator with index ``index``. @@ -148,7 +148,7 @@ func initiate_validator_exit*(state: var BeaconState, validator.withdrawable_epoch = validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#slash_validator +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#slash_validator proc slash_validator*(state: var BeaconState, slashed_index: ValidatorIndex, cache: var StateCache) = # Slash the validator with index ``index``. @@ -303,12 +303,12 @@ func get_block_root_at_slot*(state: BeaconState, doAssert slot < state.slot state.block_roots[slot mod SLOTS_PER_HISTORICAL_ROOT] -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_block_root +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_block_root func get_block_root*(state: BeaconState, epoch: Epoch): Eth2Digest = # Return the block root at the start of a recent ``epoch``. get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch)) -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_total_balance +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_total_balance func get_total_balance*(state: BeaconState, validators: auto): Gwei = ## Return the combined effective balance of the ``indices``. ## ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. @@ -317,15 +317,13 @@ func get_total_balance*(state: BeaconState, validators: auto): Gwei = foldl(validators, a + state.validators[b].effective_balance, 0'u64) ) -# XXX: Move to state_transition_epoch.nim? - -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_eligible_for_activation_queue +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_eligible_for_activation_queue func is_eligible_for_activation_queue(validator: Validator): bool = # Check if ``validator`` is eligible to be placed into the activation queue. validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH and validator.effective_balance == MAX_EFFECTIVE_BALANCE -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#is_eligible_for_activation +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_eligible_for_activation func is_eligible_for_activation(state: BeaconState, validator: Validator): bool = # Check if ``validator`` is eligible for activation. @@ -449,7 +447,7 @@ func get_attesting_indices*(state: BeaconState, if bits[i]: result.incl index -# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_indexed_attestation +# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_indexed_attestation func get_indexed_attestation*(state: BeaconState, attestation: Attestation, stateCache: var StateCache): IndexedAttestation = # Return the indexed attestation corresponding to ``attestation``.