mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-08 23:28:10 -05:00
chore: remove eth1 related code (#8692)
**Motivation** All networks are post-electra now and transition period is completed, which means due to [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110) we no longer need to process deposits via eth1 bridge as those are now processed by the execution layer. This code is effectively tech debt, no longer exercised and just gets in the way when doing refactors. **Description** Removes all code related to eth1 bridge mechanism to include new deposits - removed all eth1 related code, we can no longer produce blocks with deposits pre-electra (syncing blocks still works) - building a genesis state from eth1 is no longer supported (only for testing) - removed various db repositories related to deposits/eth1 data - removed various `lodestar_eth1_*` metrics and dashboard panels - deprecated all `--eth1.*` flags (but kept for backward compatibility) - moved shared utility functions from eth1 to execution engine module Closes https://github.com/ChainSafe/lodestar/issues/7682 Closes https://github.com/ChainSafe/lodestar/issues/8654
This commit is contained in:
@@ -273,7 +273,6 @@
|
||||
"**/packages/beacon-node/src/db/buckets.ts",
|
||||
"**/packages/beacon-node/src/execution/engine/mock.ts",
|
||||
"**/packages/beacon-node/src/execution/engine/types.ts",
|
||||
"**/packages/beacon-node/src/eth1/provider/eth1Provider.ts",
|
||||
"**/packages/validator/src/buckets.ts",
|
||||
"**/packages/prover/src/types.ts",
|
||||
"**/prover/src/utils/process.ts",
|
||||
|
||||
@@ -212,19 +212,6 @@
|
||||
"range": true,
|
||||
"refId": "attestations"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "rate(beacon_block_production_execution_steps_seconds_sum{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds_count{step=\"eth1DataAndDeposits\"}[$rate_interval])",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"legendFormat": "{{step}}",
|
||||
"range": true,
|
||||
"refId": "eth1DataAndDeposits"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
@@ -388,19 +375,6 @@
|
||||
"range": true,
|
||||
"refId": "attestations"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "rate(beacon_block_production_builder_steps_seconds_sum{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds_count{step=\"eth1DataAndDeposits\"}[$rate_interval])",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"legendFormat": "{{step}}",
|
||||
"range": true,
|
||||
"refId": "eth1DataAndDeposits"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
|
||||
@@ -1608,855 +1608,6 @@
|
||||
],
|
||||
"title": "forkchoiceUpdatedV2",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 67
|
||||
},
|
||||
"id": 380,
|
||||
"panels": [],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Eth1 Stats",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"mappings": [],
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 68
|
||||
},
|
||||
"id": 429,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.2.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_remote_highest_block",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "remote_highest_block",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_last_processed_deposit_block_number",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "last_processed_deposit_block",
|
||||
"refId": "D"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_last_fetched_block_block_number",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "last_fetched_block_block_number",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Eth1 Block Details",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"index": 0,
|
||||
"text": "False"
|
||||
},
|
||||
"1": {
|
||||
"index": 1,
|
||||
"text": "True"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
],
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 12,
|
||||
"y": 68
|
||||
},
|
||||
"id": 426,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "value",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.4.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_deposit_tracker_is_caughtup",
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Up to date",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": []
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 15,
|
||||
"y": 68
|
||||
},
|
||||
"id": 427,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.4.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_http_client_config_urls_count",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Urls",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 18,
|
||||
"y": 68
|
||||
},
|
||||
"id": 411,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.4.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_follow_distance_seconds_config",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Follow Config",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"unit": "dateTimeFromNow"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 3,
|
||||
"x": 21,
|
||||
"y": 68
|
||||
},
|
||||
"id": 431,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "/^Time$/",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "10.4.1",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_last_fetched_block_timestamp",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_last_fetched_block_timestamp",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Last fetched",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"mappings": [],
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 72
|
||||
},
|
||||
"id": 474,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.2.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_follow_distance_dynamic",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_follow_distance_dynamic",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_blocks_batch_size_dynamic",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_blocks_batch_size_dynamic",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_logs_batch_size_dynamic",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_logs_batch_size_dynamic",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Eth1 Dynamic Stats",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 1,
|
||||
"scaleDistribution": {
|
||||
"log": 2,
|
||||
"type": "log"
|
||||
},
|
||||
"showPoints": "always",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 76
|
||||
},
|
||||
"id": 384,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.4.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_http_client_request_time_seconds_sum[32m])/rate(lodestar_eth1_http_client_request_time_seconds_count[32m])",
|
||||
"format": "time_series",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "{{routeId}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Average response times",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"mappings": [],
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 78
|
||||
},
|
||||
"id": 413,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.2.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "lodestar_eth1_http_client_request_errors_total",
|
||||
"interval": "",
|
||||
"legendFormat": "{{routeId}} request_errors",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_deposit_tracker_update_errors_total[$rate_interval])",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_deposit_tracker_update_errors_total",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_http_client_request_used_fallback_url_total[$rate_interval])",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "{{routeId}} used_fallback_url",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Error rates",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 7,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 1,
|
||||
"scaleDistribution": {
|
||||
"log": 2,
|
||||
"type": "log"
|
||||
},
|
||||
"showPoints": "always",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 84
|
||||
},
|
||||
"id": 434,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.4.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "12*rate(lodestar_eth1_http_client_request_time_seconds_count[32m])",
|
||||
"interval": "",
|
||||
"legendFormat": "{{routeId}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Requests / slot",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"mappings": [],
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 84
|
||||
},
|
||||
"id": 428,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.2.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_blocks_fetched_total[32m])",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "blocks",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_deposit_events_fetched_total[32m])",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "deposits",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Eth1 fetch rate",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "10s",
|
||||
|
||||
@@ -1825,18 +1825,6 @@
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"exemplar": false,
|
||||
"expr": "rate(lodestar_eth1_http_client_request_time_seconds_sum{routeId=\"getBlockNumber\"}[$rate_interval])\n/\nrate(lodestar_eth1_http_client_request_time_seconds_count{routeId=\"getBlockNumber\"}[$rate_interval])",
|
||||
"hide": false,
|
||||
"interval": "",
|
||||
"legendFormat": "eth1_getBlockNumber_roundtrip",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
|
||||
@@ -39,11 +39,6 @@
|
||||
"types": "./lib/db/index.d.ts",
|
||||
"import": "./lib/db/index.js"
|
||||
},
|
||||
"./eth1": {
|
||||
"bun": "./src/eth1/index.ts",
|
||||
"types": "./lib/eth1/index.d.ts",
|
||||
"import": "./lib/eth1/index.js"
|
||||
},
|
||||
"./metrics": {
|
||||
"bun": "./src/metrics/index.ts",
|
||||
"types": "./lib/metrics/index.d.ts",
|
||||
@@ -126,7 +121,6 @@
|
||||
"@chainsafe/ssz": "^1.2.2",
|
||||
"@chainsafe/threads": "^1.11.3",
|
||||
"@crate-crypto/node-eth-kzg": "0.9.1",
|
||||
"@ethersproject/abi": "^5.7.0",
|
||||
"@fastify/bearer-auth": "^10.0.1",
|
||||
"@fastify/cors": "^10.0.1",
|
||||
"@fastify/swagger": "^9.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {blockToHeader} from "@lodestar/state-transition";
|
||||
import {RootHex, SignedBeaconBlock, Slot} from "@lodestar/types";
|
||||
import {IBeaconChain} from "../../../../chain/interface.js";
|
||||
import {GENESIS_SLOT} from "../../../../constants/index.js";
|
||||
import {rootHexRegex} from "../../../../eth1/provider/utils.js";
|
||||
import {rootHexRegex} from "../../../../execution/engine/utils.js";
|
||||
import {ApiError, ValidationError} from "../../errors.js";
|
||||
|
||||
export function toBeaconHeaderResponse(
|
||||
|
||||
@@ -43,7 +43,6 @@ import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRoot
|
||||
import {ProcessShutdownCallback} from "@lodestar/validator";
|
||||
import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
import {IEth1ForBlockProduction} from "../eth1/index.js";
|
||||
import {BuilderStatus} from "../execution/builder/http.js";
|
||||
import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js";
|
||||
import {Metrics} from "../metrics/index.js";
|
||||
@@ -117,7 +116,6 @@ const DEFAULT_MAX_CACHED_PRODUCED_RESULTS = 4;
|
||||
export class BeaconChain implements IBeaconChain {
|
||||
readonly genesisTime: UintNum64;
|
||||
readonly genesisValidatorsRoot: Root;
|
||||
readonly eth1: IEth1ForBlockProduction;
|
||||
readonly executionEngine: IExecutionEngine;
|
||||
readonly executionBuilder?: IExecutionBuilder;
|
||||
// Expose config for convenience in modularized functions
|
||||
@@ -216,7 +214,6 @@ export class BeaconChain implements IBeaconChain {
|
||||
validatorMonitor,
|
||||
anchorState,
|
||||
isAnchorStateFinalized,
|
||||
eth1,
|
||||
executionEngine,
|
||||
executionBuilder,
|
||||
}: {
|
||||
@@ -233,7 +230,6 @@ export class BeaconChain implements IBeaconChain {
|
||||
validatorMonitor: ValidatorMonitor | null;
|
||||
anchorState: BeaconStateAllForks;
|
||||
isAnchorStateFinalized: boolean;
|
||||
eth1: IEth1ForBlockProduction;
|
||||
executionEngine: IExecutionEngine;
|
||||
executionBuilder?: IExecutionBuilder;
|
||||
}
|
||||
@@ -248,7 +244,6 @@ export class BeaconChain implements IBeaconChain {
|
||||
this.genesisTime = anchorState.genesisTime;
|
||||
this.anchorStateLatestBlockSlot = anchorState.latestBlockHeader.slot;
|
||||
this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot;
|
||||
this.eth1 = eth1;
|
||||
this.executionEngine = executionEngine;
|
||||
this.executionBuilder = executionBuilder;
|
||||
const signal = this.abortController.signal;
|
||||
@@ -294,7 +289,7 @@ export class BeaconChain implements IBeaconChain {
|
||||
// Restore state caches
|
||||
// anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all
|
||||
// pubkeys takes ~30 seconds for 350k keys (mainnet 2022Q2).
|
||||
// When the BeaconStateCache is created in eth1 genesis builder it may be incorrect. Until we can ensure that
|
||||
// When the BeaconStateCache is created in initializeBeaconStateFromEth1 it may be incorrect. Until we can ensure that
|
||||
// it's safe to re-use _ANY_ BeaconStateCache, this option is disabled by default and only used in tests.
|
||||
const cachedState =
|
||||
isCachedBeaconState(anchorState) && opts.skipCreateStateCacheIfAvailable
|
||||
@@ -417,15 +412,6 @@ export class BeaconChain implements IBeaconChain {
|
||||
signal
|
||||
);
|
||||
|
||||
// Stop polling eth1 data if anchor state is in Electra AND deposit_requests_start_index is reached
|
||||
const anchorStateFork = this.config.getForkName(anchorState.slot);
|
||||
if (isForkPostElectra(anchorStateFork)) {
|
||||
const {eth1DepositIndex, depositRequestsStartIndex} = anchorState as BeaconStateElectra;
|
||||
if (eth1DepositIndex === Number(depositRequestsStartIndex)) {
|
||||
this.eth1.stopPollingEth1Data();
|
||||
}
|
||||
}
|
||||
|
||||
// always run PrepareNextSlotScheduler except for fork_choice spec tests
|
||||
if (!opts?.disablePrepareNextSlot) {
|
||||
new PrepareNextSlotScheduler(this, this.config, metrics, this.logger, signal);
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import {Tree, toGindex} from "@chainsafe/persistent-merkle-tree";
|
||||
import {BeaconConfig, ChainForkConfig} from "@lodestar/config";
|
||||
import {GENESIS_EPOCH, GENESIS_SLOT} from "@lodestar/params";
|
||||
import {
|
||||
BeaconStateAllForks,
|
||||
CachedBeaconStateAllForks,
|
||||
applyDeposits,
|
||||
applyEth1BlockHash,
|
||||
applyTimestamp,
|
||||
createCachedBeaconState,
|
||||
createEmptyEpochCacheImmutableData,
|
||||
getActiveValidatorIndices,
|
||||
getGenesisBeaconState,
|
||||
getTemporaryBlockHeader,
|
||||
} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {DepositTree} from "../../db/repositories/depositDataRoot.js";
|
||||
import {IEth1Provider} from "../../eth1/index.js";
|
||||
import {IEth1StreamParams} from "../../eth1/interface.js";
|
||||
import {getDepositsAndBlockStreamForGenesis, getDepositsStream} from "../../eth1/stream.js";
|
||||
import {GenesisResult, IGenesisBuilder} from "./interface.js";
|
||||
|
||||
export type GenesisBuilderKwargs = {
|
||||
config: ChainForkConfig;
|
||||
eth1Provider: IEth1Provider;
|
||||
logger: Logger;
|
||||
|
||||
/** Use to restore pending progress */
|
||||
pendingStatus?: {
|
||||
state: BeaconStateAllForks;
|
||||
depositTree: DepositTree;
|
||||
lastProcessedBlockNumber: number;
|
||||
};
|
||||
|
||||
signal?: AbortSignal;
|
||||
maxBlocksPerPoll?: number;
|
||||
};
|
||||
|
||||
export class GenesisBuilder implements IGenesisBuilder {
|
||||
// Expose state to persist on error
|
||||
readonly state: CachedBeaconStateAllForks;
|
||||
readonly depositTree: DepositTree;
|
||||
/** Is null if no block has been processed yet */
|
||||
lastProcessedBlockNumber: number | null = null;
|
||||
|
||||
private readonly config: BeaconConfig;
|
||||
private readonly eth1Provider: IEth1Provider;
|
||||
private readonly logger: Logger;
|
||||
private readonly signal?: AbortSignal;
|
||||
private readonly eth1Params: IEth1StreamParams;
|
||||
private readonly depositCache = new Set<number>();
|
||||
private readonly fromBlock: number;
|
||||
private readonly logEvery = 30 * 1000;
|
||||
private lastLog = 0;
|
||||
/** Current count of active validators in the state */
|
||||
private activatedValidatorCount: number;
|
||||
|
||||
constructor({config, eth1Provider, logger, signal, pendingStatus, maxBlocksPerPoll}: GenesisBuilderKwargs) {
|
||||
// at genesis builder, there is no genesis validator so we don't have a real BeaconConfig
|
||||
// but we need BeaconConfig to temporarily create CachedBeaconState, the cast here is safe since we don't use any getDomain here
|
||||
// the use of state as CachedBeaconState is just for convenient, GenesisResult returns TreeView anyway
|
||||
this.eth1Provider = eth1Provider;
|
||||
this.logger = logger;
|
||||
this.signal = signal;
|
||||
this.eth1Params = {
|
||||
...config,
|
||||
maxBlocksPerPoll: maxBlocksPerPoll ?? 10000,
|
||||
};
|
||||
|
||||
let stateView: BeaconStateAllForks;
|
||||
|
||||
if (pendingStatus) {
|
||||
this.logger.info("Restoring pending genesis state", {block: pendingStatus.lastProcessedBlockNumber});
|
||||
stateView = pendingStatus.state;
|
||||
this.depositTree = pendingStatus.depositTree;
|
||||
this.fromBlock = Math.max(pendingStatus.lastProcessedBlockNumber + 1, this.eth1Provider.deployBlock);
|
||||
} else {
|
||||
stateView = getGenesisBeaconState(
|
||||
config,
|
||||
ssz.phase0.Eth1Data.defaultValue(),
|
||||
getTemporaryBlockHeader(config, config.getForkTypes(GENESIS_SLOT).BeaconBlock.defaultValue())
|
||||
);
|
||||
this.depositTree = ssz.phase0.DepositDataRootList.defaultViewDU();
|
||||
this.fromBlock = this.eth1Provider.deployBlock;
|
||||
}
|
||||
|
||||
// TODO - PENDING: Ensure EpochCacheImmutableData is created only once
|
||||
this.state = createCachedBeaconState(stateView, createEmptyEpochCacheImmutableData(config, stateView));
|
||||
this.config = this.state.config;
|
||||
this.activatedValidatorCount = getActiveValidatorIndices(stateView, GENESIS_EPOCH).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eth1 deposit events and blocks and apply to this.state until we found genesis.
|
||||
*/
|
||||
async waitForGenesis(): Promise<GenesisResult> {
|
||||
await this.eth1Provider.validateContract();
|
||||
|
||||
// Load data from data from this.db.depositData, this.db.depositDataRoot
|
||||
// And start from a more recent fromBlock
|
||||
const blockNumberValidatorGenesis = await this.waitForGenesisValidators();
|
||||
|
||||
const depositsAndBlocksStream = getDepositsAndBlockStreamForGenesis(
|
||||
blockNumberValidatorGenesis,
|
||||
this.eth1Provider,
|
||||
this.eth1Params,
|
||||
this.signal
|
||||
);
|
||||
|
||||
for await (const [depositEvents, block] of depositsAndBlocksStream) {
|
||||
this.applyDeposits(depositEvents);
|
||||
applyTimestamp(this.config, this.state, block.timestamp);
|
||||
applyEth1BlockHash(this.state, block.blockHash);
|
||||
this.lastProcessedBlockNumber = block.blockNumber;
|
||||
|
||||
if (
|
||||
this.state.genesisTime >= this.config.MIN_GENESIS_TIME &&
|
||||
this.activatedValidatorCount >= this.config.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
|
||||
) {
|
||||
this.logger.info("Found genesis state", {blockNumber: block.blockNumber});
|
||||
return {
|
||||
state: this.state,
|
||||
depositTree: this.depositTree,
|
||||
block,
|
||||
};
|
||||
}
|
||||
|
||||
this.throttledLog(`Waiting for min genesis time ${block.timestamp} / ${this.config.MIN_GENESIS_TIME}`);
|
||||
}
|
||||
|
||||
throw Error("depositsStream stopped without a valid genesis state");
|
||||
}
|
||||
|
||||
/**
|
||||
* First phase of waiting for genesis.
|
||||
* Stream deposits events in batches as big as possible without querying block data
|
||||
* @returns Block number at which there are enough active validators is state for genesis
|
||||
*/
|
||||
private async waitForGenesisValidators(): Promise<number> {
|
||||
const depositsStream = getDepositsStream(this.fromBlock, this.eth1Provider, this.eth1Params, this.signal);
|
||||
|
||||
for await (const {depositEvents, blockNumber} of depositsStream) {
|
||||
this.applyDeposits(depositEvents);
|
||||
this.lastProcessedBlockNumber = blockNumber;
|
||||
|
||||
if (this.activatedValidatorCount >= this.config.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT) {
|
||||
this.logger.info("Found enough genesis validators", {blockNumber});
|
||||
return blockNumber;
|
||||
}
|
||||
|
||||
this.throttledLog(
|
||||
`Found ${this.state.validators.length} / ${this.config.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT} validators to genesis`
|
||||
);
|
||||
}
|
||||
|
||||
throw Error("depositsStream stopped without a valid genesis state");
|
||||
}
|
||||
|
||||
private applyDeposits(depositEvents: phase0.DepositEvent[]): void {
|
||||
const newDeposits = depositEvents
|
||||
.filter((depositEvent) => !this.depositCache.has(depositEvent.index))
|
||||
.map((depositEvent) => {
|
||||
this.depositCache.add(depositEvent.index);
|
||||
this.depositTree.push(ssz.phase0.DepositData.hashTreeRoot(depositEvent.depositData));
|
||||
const gindex = toGindex(this.depositTree.type.depth, BigInt(depositEvent.index));
|
||||
|
||||
// Apply changes from the push above
|
||||
this.depositTree.commit();
|
||||
const depositTreeNode = this.depositTree.node;
|
||||
return {
|
||||
proof: new Tree(depositTreeNode).getSingleProof(gindex),
|
||||
data: depositEvent.depositData,
|
||||
};
|
||||
});
|
||||
|
||||
const {activatedValidatorCount} = applyDeposits(this.config, this.state, newDeposits, this.depositTree);
|
||||
this.activatedValidatorCount += activatedValidatorCount;
|
||||
|
||||
// TODO: If necessary persist deposits here to this.db.depositData, this.db.depositDataRoot
|
||||
}
|
||||
|
||||
/** Throttle genesis generation status log to prevent spamming */
|
||||
private throttledLog(message: string): void {
|
||||
if (Date.now() - this.lastLog > this.logEvery) {
|
||||
this.lastLog = Date.now();
|
||||
this.logger.info(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import {CompositeViewDU, VectorCompositeType} from "@chainsafe/ssz";
|
||||
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {ssz} from "@lodestar/types";
|
||||
import {Eth1Block} from "../../eth1/interface.js";
|
||||
|
||||
export type GenesisResult = {
|
||||
state: CachedBeaconStateAllForks;
|
||||
depositTree: CompositeViewDU<VectorCompositeType<typeof ssz.Root>>;
|
||||
block: Eth1Block;
|
||||
};
|
||||
|
||||
export interface IGenesisBuilder {
|
||||
waitForGenesis: () => Promise<GenesisResult>;
|
||||
}
|
||||
@@ -1,37 +1,11 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {ZERO_HASH} from "@lodestar/params";
|
||||
import {
|
||||
BeaconStateAllForks,
|
||||
CachedBeaconStateAllForks,
|
||||
computeEpochAtSlot,
|
||||
computeStartSlotAtEpoch,
|
||||
} from "@lodestar/state-transition";
|
||||
import {BeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
||||
import {SignedBeaconBlock, ssz} from "@lodestar/types";
|
||||
import {Logger, toHex, toRootHex} from "@lodestar/utils";
|
||||
import {GENESIS_SLOT} from "../constants/index.js";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
import {Eth1Provider} from "../eth1/index.js";
|
||||
import {Eth1Options} from "../eth1/options.js";
|
||||
import {Metrics} from "../metrics/index.js";
|
||||
import {GenesisBuilder} from "./genesis/genesis.js";
|
||||
import {GenesisResult} from "./genesis/interface.js";
|
||||
|
||||
export async function persistGenesisResult(
|
||||
db: IBeaconDb,
|
||||
genesisResult: GenesisResult,
|
||||
genesisBlock: SignedBeaconBlock
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
db.stateArchive.add(genesisResult.state),
|
||||
db.blockArchive.add(genesisBlock),
|
||||
db.depositDataRoot.putList(genesisResult.depositTree.getAllReadonlyValues()),
|
||||
db.eth1Data.put(genesisResult.block.timestamp, {
|
||||
...genesisResult.block,
|
||||
depositCount: genesisResult.depositTree.length,
|
||||
depositRoot: genesisResult.depositTree.hashTreeRoot(),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function persistAnchorState(
|
||||
config: ChainForkConfig,
|
||||
@@ -75,76 +49,6 @@ export function createGenesisBlock(config: ChainForkConfig, genesisState: Beacon
|
||||
return genesisBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and persist a genesis state and related data
|
||||
*/
|
||||
export async function initStateFromEth1({
|
||||
config,
|
||||
db,
|
||||
logger,
|
||||
opts,
|
||||
signal,
|
||||
}: {
|
||||
config: ChainForkConfig;
|
||||
db: IBeaconDb;
|
||||
logger: Logger;
|
||||
opts: Eth1Options;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CachedBeaconStateAllForks> {
|
||||
logger.info("Listening to eth1 for genesis state");
|
||||
|
||||
const statePreGenesis = await db.preGenesisState.get();
|
||||
const depositTree = await db.depositDataRoot.getDepositRootTree();
|
||||
const lastProcessedBlockNumber = await db.preGenesisStateLastProcessedBlock.get();
|
||||
|
||||
const builder = new GenesisBuilder({
|
||||
config,
|
||||
eth1Provider: new Eth1Provider(config, {...opts, logger}, signal),
|
||||
logger,
|
||||
signal,
|
||||
pendingStatus:
|
||||
statePreGenesis && depositTree !== undefined && lastProcessedBlockNumber != null
|
||||
? {state: statePreGenesis, depositTree, lastProcessedBlockNumber}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const genesisResult = await builder.waitForGenesis();
|
||||
|
||||
// Note: .hashTreeRoot() automatically commits()
|
||||
const genesisBlock = createGenesisBlock(config, genesisResult.state);
|
||||
const types = config.getForkTypes(GENESIS_SLOT);
|
||||
const stateRoot = genesisResult.state.hashTreeRoot();
|
||||
const blockRoot = types.BeaconBlock.hashTreeRoot(genesisBlock.message);
|
||||
|
||||
logger.info("Initializing genesis state", {
|
||||
stateRoot: toRootHex(stateRoot),
|
||||
blockRoot: toRootHex(blockRoot),
|
||||
validatorCount: genesisResult.state.validators.length,
|
||||
});
|
||||
|
||||
await persistGenesisResult(db, genesisResult, genesisBlock);
|
||||
|
||||
logger.verbose("Clearing pending genesis state if any");
|
||||
await db.preGenesisState.delete();
|
||||
await db.preGenesisStateLastProcessedBlock.delete();
|
||||
|
||||
return genesisResult.state;
|
||||
} catch (e) {
|
||||
if (builder.lastProcessedBlockNumber != null) {
|
||||
logger.info("Persisting genesis state", {block: builder.lastProcessedBlockNumber});
|
||||
|
||||
// Commit changed before serializing
|
||||
builder.state.commit();
|
||||
|
||||
await db.preGenesisState.put(builder.state);
|
||||
await db.depositDataRoot.putList(builder.depositTree.getAllReadonlyValues());
|
||||
await db.preGenesisStateLastProcessedBlock.put(builder.lastProcessedBlockNumber);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the latest beacon state from db
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
phase0,
|
||||
} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {IEth1ForBlockProduction} from "../eth1/index.js";
|
||||
import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js";
|
||||
import {Metrics} from "../metrics/metrics.js";
|
||||
import {BufferPool} from "../util/bufferPool.js";
|
||||
@@ -88,7 +87,6 @@ export interface IBeaconChain {
|
||||
readonly genesisTime: UintNum64;
|
||||
readonly genesisValidatorsRoot: Root;
|
||||
readonly earliestAvailableSlot: Slot;
|
||||
readonly eth1: IEth1ForBlockProduction;
|
||||
readonly executionEngine: IExecutionEngine;
|
||||
readonly executionBuilder?: IExecutionBuilder;
|
||||
// Expose config for convenience in modularized functions
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {routes} from "@lodestar/api";
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {getSafeExecutionBlockHash} from "@lodestar/fork-choice";
|
||||
import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra} from "@lodestar/params";
|
||||
import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
|
||||
import {
|
||||
BeaconStateElectra,
|
||||
CachedBeaconStateAllForks,
|
||||
CachedBeaconStateExecutions,
|
||||
StateHashTreeRootSource,
|
||||
@@ -222,9 +221,6 @@ export class PrepareNextSlotScheduler {
|
||||
}
|
||||
this.metrics?.precomputeNextEpochTransition.hits.set(previousHits ?? 0);
|
||||
|
||||
// Check if we can stop polling eth1 data
|
||||
this.stopEth1Polling();
|
||||
|
||||
this.logger.verbose("Completed PrepareNextSlotScheduler epoch transition", {
|
||||
nextEpoch,
|
||||
headSlot,
|
||||
@@ -252,27 +248,4 @@ export class PrepareNextSlotScheduler {
|
||||
state.hashTreeRoot();
|
||||
hashTreeRootTimer?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop eth1 data polling after eth1_deposit_index has reached deposit_requests_start_index in Electra as described in EIP-6110
|
||||
*/
|
||||
stopEth1Polling(): void {
|
||||
// Only continue if eth1 is still polling and finalized checkpoint is in Electra. State regen is expensive
|
||||
if (this.chain.eth1.isPollingEth1Data()) {
|
||||
const finalizedCheckpoint = this.chain.forkChoice.getFinalizedCheckpoint();
|
||||
const checkpointFork = this.config.getForkInfoAtEpoch(finalizedCheckpoint.epoch).name;
|
||||
|
||||
if (isForkPostElectra(checkpointFork)) {
|
||||
const finalizedState = this.chain.getStateByCheckpoint(finalizedCheckpoint)?.state;
|
||||
|
||||
if (
|
||||
finalizedState !== undefined &&
|
||||
finalizedState.eth1DepositIndex === Number((finalizedState as BeaconStateElectra).depositRequestsStartIndex)
|
||||
) {
|
||||
// Signal eth1 to stop polling eth1Data
|
||||
this.chain.eth1.stopPollingEth1Data();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
} from "@lodestar/types";
|
||||
import {Logger, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
|
||||
import {ZERO_HASH_HEX} from "../../constants/index.js";
|
||||
import {numToQuantity} from "../../eth1/provider/utils.js";
|
||||
import {numToQuantity} from "../../execution/engine/utils.js";
|
||||
import {
|
||||
IExecutionBuilder,
|
||||
IExecutionEngine,
|
||||
@@ -78,7 +78,6 @@ export enum BlockProductionStep {
|
||||
voluntaryExits = "voluntaryExits",
|
||||
blsToExecutionChanges = "blsToExecutionChanges",
|
||||
attestations = "attestations",
|
||||
eth1DataAndDeposits = "eth1DataAndDeposits",
|
||||
syncAggregate = "syncAggregate",
|
||||
executionPayload = "executionPayload",
|
||||
}
|
||||
@@ -667,20 +666,17 @@ export async function produceCommonBlockBody<T extends BlockType>(
|
||||
step: BlockProductionStep.attestations,
|
||||
});
|
||||
|
||||
const endEth1DataAndDeposits = stepsMetrics?.startTimer();
|
||||
const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState);
|
||||
endEth1DataAndDeposits?.({
|
||||
step: BlockProductionStep.eth1DataAndDeposits,
|
||||
});
|
||||
|
||||
const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
|
||||
randaoReveal,
|
||||
graffiti,
|
||||
eth1Data,
|
||||
// Eth1 data voting is no longer required since electra
|
||||
eth1Data: currentState.eth1Data,
|
||||
proposerSlashings,
|
||||
attesterSlashings,
|
||||
attestations,
|
||||
deposits,
|
||||
// Since electra, deposits are processed by the execution layer,
|
||||
// we no longer support handling deposits from earlier forks.
|
||||
deposits: [],
|
||||
voluntaryExits,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,16 +14,12 @@ import {
|
||||
CheckpointHeaderRepository,
|
||||
DataColumnSidecarArchiveRepository,
|
||||
DataColumnSidecarRepository,
|
||||
DepositDataRootRepository,
|
||||
DepositEventRepository,
|
||||
Eth1DataRepository,
|
||||
ProposerSlashingRepository,
|
||||
StateArchiveRepository,
|
||||
SyncCommitteeRepository,
|
||||
SyncCommitteeWitnessRepository,
|
||||
VoluntaryExitRepository,
|
||||
} from "./repositories/index.js";
|
||||
import {PreGenesisState, PreGenesisStateLastProcessedBlock} from "./single/index.js";
|
||||
|
||||
export type BeaconDbModules = {
|
||||
config: ChainForkConfig;
|
||||
@@ -45,14 +41,8 @@ export class BeaconDb implements IBeaconDb {
|
||||
voluntaryExit: VoluntaryExitRepository;
|
||||
proposerSlashing: ProposerSlashingRepository;
|
||||
attesterSlashing: AttesterSlashingRepository;
|
||||
depositEvent: DepositEventRepository;
|
||||
blsToExecutionChange: BLSToExecutionChangeRepository;
|
||||
|
||||
depositDataRoot: DepositDataRootRepository;
|
||||
eth1Data: Eth1DataRepository;
|
||||
preGenesisState: PreGenesisState;
|
||||
preGenesisStateLastProcessedBlock: PreGenesisStateLastProcessedBlock;
|
||||
|
||||
// lightclient
|
||||
bestLightClientUpdate: BestLightClientUpdateRepository;
|
||||
checkpointHeader: CheckpointHeaderRepository;
|
||||
@@ -80,11 +70,6 @@ export class BeaconDb implements IBeaconDb {
|
||||
this.blsToExecutionChange = new BLSToExecutionChangeRepository(config, db);
|
||||
this.proposerSlashing = new ProposerSlashingRepository(config, db);
|
||||
this.attesterSlashing = new AttesterSlashingRepository(config, db);
|
||||
this.depositEvent = new DepositEventRepository(config, db);
|
||||
this.depositDataRoot = new DepositDataRootRepository(config, db);
|
||||
this.eth1Data = new Eth1DataRepository(config, db);
|
||||
this.preGenesisState = new PreGenesisState(config, db);
|
||||
this.preGenesisStateLastProcessedBlock = new PreGenesisStateLastProcessedBlock(config, db);
|
||||
|
||||
// lightclient
|
||||
this.bestLightClientUpdate = new BestLightClientUpdateRepository(config, db);
|
||||
|
||||
@@ -16,14 +16,13 @@ export enum Bucket {
|
||||
index_mainChain = 6, // Slot -> Root<BeaconBlock>
|
||||
// justified, finalized state and block hashes
|
||||
index_chainInfo = 7, // Key -> Number64 | stateHash | blockHash
|
||||
// eth1 processing
|
||||
phase0_eth1Data = 8, // timestamp -> Eth1Data
|
||||
index_depositDataRoot = 9, // depositIndex -> Root<DepositData>
|
||||
// phase0_eth1Data = 8, // DEPRECATED - eth1 deposit tracking is not required since electra
|
||||
// index_depositDataRoot = 9, // DEPRECATED - eth1 deposit tracking is not required since electra
|
||||
|
||||
// op pool
|
||||
// phase0_attestation = 10, // DEPRECATED on v0.25.0
|
||||
// phase0_aggregateAndProof = 11, // Root -> AggregateAndProof, DEPRECATED on v.27.0
|
||||
phase0_depositData = 12, // [DEPRECATED] index -> DepositData
|
||||
// phase0_depositData = 12, // DEPRECATED - eth1 deposit tracking is not required since electra
|
||||
phase0_exit = 13, // ValidatorIndex -> VoluntaryExit
|
||||
phase0_proposerSlashing = 14, // ValidatorIndex -> ProposerSlashing
|
||||
allForks_attesterSlashing = 15, // Root -> AttesterSlashing
|
||||
@@ -32,15 +31,15 @@ export enum Bucket {
|
||||
allForks_checkpointState = 17, // Root -> BeaconState
|
||||
|
||||
// allForks_pendingBlock = 25, // Root -> SignedBeaconBlock // DEPRECATED on v0.30.0
|
||||
phase0_depositEvent = 19, // depositIndex -> DepositEvent
|
||||
// phase0_depositEvent = 19, // DEPRECATED - eth1 deposit tracking is not required since electra
|
||||
|
||||
index_stateArchiveRootIndex = 26, // State Root -> slot
|
||||
|
||||
deneb_blobSidecars = 27, // DENEB BeaconBlockRoot -> BlobSidecars
|
||||
deneb_blobSidecarsArchive = 28, // DENEB BeaconBlockSlot -> BlobSidecars
|
||||
|
||||
phase0_preGenesisState = 30, // Single = phase0.BeaconState
|
||||
phase0_preGenesisStateLastProcessedBlock = 31, // Single = Uint8
|
||||
// phase0_preGenesisState = 30, // DEPRECATED - genesis from eth1 is no longer supported
|
||||
// phase0_preGenesisStateLastProcessedBlock = 31, // DEPRECATED - genesis from eth1 is no longer supported
|
||||
|
||||
// Lightclient server
|
||||
// altair_bestUpdatePerCommitteePeriod = 30, // DEPRECATED on v0.32.0
|
||||
|
||||
@@ -12,16 +12,12 @@ import {
|
||||
CheckpointHeaderRepository,
|
||||
DataColumnSidecarArchiveRepository,
|
||||
DataColumnSidecarRepository,
|
||||
DepositDataRootRepository,
|
||||
DepositEventRepository,
|
||||
Eth1DataRepository,
|
||||
ProposerSlashingRepository,
|
||||
StateArchiveRepository,
|
||||
SyncCommitteeRepository,
|
||||
SyncCommitteeWitnessRepository,
|
||||
VoluntaryExitRepository,
|
||||
} from "./repositories/index.js";
|
||||
import {PreGenesisState, PreGenesisStateLastProcessedBlock} from "./single/index.js";
|
||||
|
||||
/**
|
||||
* The DB service manages the data layer of the beacon chain
|
||||
@@ -48,17 +44,8 @@ export interface IBeaconDb {
|
||||
voluntaryExit: VoluntaryExitRepository;
|
||||
proposerSlashing: ProposerSlashingRepository;
|
||||
attesterSlashing: AttesterSlashingRepository;
|
||||
depositEvent: DepositEventRepository;
|
||||
blsToExecutionChange: BLSToExecutionChangeRepository;
|
||||
|
||||
// eth1 processing
|
||||
preGenesisState: PreGenesisState;
|
||||
preGenesisStateLastProcessedBlock: PreGenesisStateLastProcessedBlock;
|
||||
|
||||
// all deposit data roots and merkle tree
|
||||
depositDataRoot: DepositDataRootRepository;
|
||||
eth1Data: Eth1DataRepository;
|
||||
|
||||
// lightclient
|
||||
bestLightClientUpdate: BestLightClientUpdateRepository;
|
||||
checkpointHeader: CheckpointHeaderRepository;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import {ByteVectorType, CompositeViewDU, ListCompositeType} from "@chainsafe/ssz";
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {Db, KeyValue, Repository} from "@lodestar/db";
|
||||
import {Root, ssz} from "@lodestar/types";
|
||||
import {bytesToInt} from "@lodestar/utils";
|
||||
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
||||
|
||||
// TODO: Review where is best to put this type
|
||||
export type DepositTree = CompositeViewDU<ListCompositeType<ByteVectorType>>;
|
||||
|
||||
export class DepositDataRootRepository extends Repository<number, Root> {
|
||||
private depositRootTree?: DepositTree;
|
||||
|
||||
constructor(config: ChainForkConfig, db: Db) {
|
||||
const bucket = Bucket.index_depositDataRoot;
|
||||
super(config, db, bucket, ssz.Root, getBucketNameByValue(bucket));
|
||||
}
|
||||
|
||||
decodeKey(data: Buffer): number {
|
||||
return bytesToInt(super.decodeKey(data) as unknown as Uint8Array, "be");
|
||||
}
|
||||
|
||||
// depositDataRoots stored by depositData index
|
||||
getId(_value: Root): number {
|
||||
throw new Error("Unable to create depositIndex from root");
|
||||
}
|
||||
|
||||
async put(index: number, value: Root): Promise<void> {
|
||||
await super.put(index, value);
|
||||
await this.depositRootTreeSet(index, value);
|
||||
}
|
||||
|
||||
async batchPut(items: KeyValue<number, Root>[]): Promise<void> {
|
||||
await super.batchPut(items);
|
||||
for (const {key, value} of items) {
|
||||
await this.depositRootTreeSet(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async putList(roots: Root[]): Promise<void> {
|
||||
await this.batchPut(roots.map((root, index) => ({key: index, value: root})));
|
||||
}
|
||||
|
||||
async batchPutValues(values: {index: number; root: Root}[]): Promise<void> {
|
||||
await this.batchPut(
|
||||
values.map(({index, root}) => ({
|
||||
key: index,
|
||||
value: root,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async getDepositRootTree(): Promise<DepositTree> {
|
||||
if (!this.depositRootTree) {
|
||||
const values = await this.values();
|
||||
this.depositRootTree = ssz.phase0.DepositDataRootList.toViewDU(values);
|
||||
}
|
||||
return this.depositRootTree;
|
||||
}
|
||||
|
||||
async getDepositRootTreeAtIndex(depositIndex: number): Promise<DepositTree> {
|
||||
const depositRootTree = await this.getDepositRootTree();
|
||||
return depositRootTree.sliceTo(depositIndex);
|
||||
}
|
||||
|
||||
private async depositRootTreeSet(index: number, value: Uint8Array): Promise<void> {
|
||||
const depositRootTree = await this.getDepositRootTree();
|
||||
|
||||
// TODO: Review and fix properly
|
||||
if (index > depositRootTree.length) {
|
||||
throw Error(`Error setting depositRootTree index ${index} > length ${depositRootTree.length}`);
|
||||
}
|
||||
|
||||
if (index === depositRootTree.length) {
|
||||
depositRootTree.push(value);
|
||||
} else {
|
||||
depositRootTree.set(index, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {Db, Repository} from "@lodestar/db";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
||||
|
||||
/**
|
||||
* DepositData indexed by deposit index
|
||||
* Removed when included on chain or old
|
||||
*/
|
||||
export class DepositEventRepository extends Repository<number, phase0.DepositEvent> {
|
||||
constructor(config: ChainForkConfig, db: Db) {
|
||||
const bucket = Bucket.phase0_depositEvent;
|
||||
super(config, db, bucket, ssz.phase0.DepositEvent, getBucketNameByValue(bucket));
|
||||
}
|
||||
|
||||
async deleteOld(depositCount: number): Promise<void> {
|
||||
const firstDepositIndex = await this.firstKey();
|
||||
if (firstDepositIndex === null) {
|
||||
return;
|
||||
}
|
||||
await this.batchDelete(Array.from({length: depositCount - firstDepositIndex}, (_, i) => i + firstDepositIndex));
|
||||
}
|
||||
|
||||
async batchPutValues(depositEvents: phase0.DepositEvent[]): Promise<void> {
|
||||
await this.batchPut(
|
||||
depositEvents.map((depositEvent) => ({
|
||||
key: depositEvent.index,
|
||||
value: depositEvent,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {Db, Repository} from "@lodestar/db";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {bytesToInt} from "@lodestar/utils";
|
||||
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
||||
|
||||
export class Eth1DataRepository extends Repository<number, phase0.Eth1DataOrdered> {
|
||||
constructor(config: ChainForkConfig, db: Db) {
|
||||
const bucket = Bucket.phase0_eth1Data;
|
||||
super(config, db, bucket, ssz.phase0.Eth1DataOrdered, getBucketNameByValue(bucket));
|
||||
}
|
||||
|
||||
decodeKey(data: Buffer): number {
|
||||
return bytesToInt(super.decodeKey(data) as unknown as Uint8Array, "be");
|
||||
}
|
||||
|
||||
getId(_value: phase0.Eth1Data): number {
|
||||
throw new Error("Unable to create timestamp from block hash");
|
||||
}
|
||||
|
||||
async batchPutValues(eth1Datas: (phase0.Eth1DataOrdered & {timestamp: number})[]): Promise<void> {
|
||||
await this.batchPut(
|
||||
eth1Datas.map((eth1Data) => ({
|
||||
key: eth1Data.timestamp,
|
||||
value: eth1Data,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async deleteOld(timestamp: number): Promise<void> {
|
||||
await this.batchDelete(await this.keys({lt: timestamp}));
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,6 @@ export {BlockArchiveRepository} from "./blockArchive.js";
|
||||
export {BLSToExecutionChangeRepository} from "./blsToExecutionChange.js";
|
||||
export {DataColumnSidecarRepository} from "./dataColumnSidecar.js";
|
||||
export {DataColumnSidecarArchiveRepository} from "./dataColumnSidecarArchive.js";
|
||||
export {DepositDataRootRepository} from "./depositDataRoot.js";
|
||||
export {DepositEventRepository} from "./depositEvent.js";
|
||||
export {Eth1DataRepository} from "./eth1Data.js";
|
||||
export {BestLightClientUpdateRepository} from "./lightclientBestUpdate.js";
|
||||
export {CheckpointHeaderRepository} from "./lightclientCheckpointHeader.js";
|
||||
export {SyncCommitteeRepository} from "./lightclientSyncCommittee.js";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export {PreGenesisState} from "./preGenesisState.js";
|
||||
export {PreGenesisStateLastProcessedBlock} from "./preGenesisStateLastProcessedBlock.js";
|
||||
@@ -1,37 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {Db, DbReqOpts} from "@lodestar/db";
|
||||
import {ForkAll, GENESIS_SLOT} from "@lodestar/params";
|
||||
import {BeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {SSZTypesFor} from "@lodestar/types";
|
||||
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
||||
|
||||
export class PreGenesisState {
|
||||
private readonly config: ChainForkConfig;
|
||||
private readonly bucket: Bucket;
|
||||
private readonly db: Db;
|
||||
private readonly key: Uint8Array;
|
||||
private readonly type: SSZTypesFor<ForkAll, "BeaconState">;
|
||||
private readonly dbReqOpts: DbReqOpts;
|
||||
|
||||
constructor(config: ChainForkConfig, db: Db) {
|
||||
this.config = config;
|
||||
this.db = db;
|
||||
this.bucket = Bucket.phase0_preGenesisState;
|
||||
this.key = new Uint8Array([this.bucket]);
|
||||
this.type = this.config.getForkTypes(GENESIS_SLOT).BeaconState;
|
||||
this.dbReqOpts = {bucketId: getBucketNameByValue(this.bucket)};
|
||||
}
|
||||
|
||||
async put(value: BeaconStateAllForks): Promise<void> {
|
||||
await this.db.put(this.key, value.serialize(), this.dbReqOpts);
|
||||
}
|
||||
|
||||
async get(): Promise<BeaconStateAllForks | null> {
|
||||
const value = await this.db.get(this.key, this.dbReqOpts);
|
||||
return value ? this.type.deserializeToViewDU(value) : null;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
await this.db.delete(this.key, this.dbReqOpts);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {UintNumberType} from "@chainsafe/ssz";
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {Db, DbReqOpts} from "@lodestar/db";
|
||||
import {ssz} from "@lodestar/types";
|
||||
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
||||
|
||||
export class PreGenesisStateLastProcessedBlock {
|
||||
private readonly bucket: Bucket;
|
||||
private readonly type: UintNumberType;
|
||||
private readonly db: Db;
|
||||
private readonly key: Uint8Array;
|
||||
private readonly dbReqOpts: DbReqOpts;
|
||||
|
||||
constructor(_config: ChainForkConfig, db: Db) {
|
||||
this.db = db;
|
||||
this.type = ssz.UintNum64;
|
||||
this.bucket = Bucket.phase0_preGenesisStateLastProcessedBlock;
|
||||
this.key = new Uint8Array([this.bucket]);
|
||||
this.dbReqOpts = {bucketId: getBucketNameByValue(this.bucket)};
|
||||
}
|
||||
|
||||
async put(value: number): Promise<void> {
|
||||
await this.db.put(this.key, this.type.serialize(value), this.dbReqOpts);
|
||||
}
|
||||
|
||||
async get(): Promise<number | null> {
|
||||
const value = await this.db.get(this.key, this.dbReqOpts);
|
||||
return value ? this.type.deserialize(value) : null;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
await this.db.delete(this.key, this.dbReqOpts);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import {LodestarError} from "@lodestar/utils";
|
||||
|
||||
export enum Eth1ErrorCode {
|
||||
/** Deposit index too high */
|
||||
DEPOSIT_INDEX_TOO_HIGH = "ETH1_ERROR_DEPOSIT_INDEX_TOO_HIGH",
|
||||
/** Not enough deposits in DB */
|
||||
NOT_ENOUGH_DEPOSITS = "ETH1_ERROR_NOT_ENOUGH_DEPOSITS",
|
||||
/** Too many deposits returned by DB */
|
||||
TOO_MANY_DEPOSITS = "ETH1_ERROR_TOO_MANY_DEPOSITS",
|
||||
/** Deposit root tree does not match current eth1Data */
|
||||
WRONG_DEPOSIT_ROOT = "ETH1_ERROR_WRONG_DEPOSIT_ROOT",
|
||||
|
||||
/** No deposits found for block range */
|
||||
NO_DEPOSITS_FOR_BLOCK_RANGE = "ETH1_ERROR_NO_DEPOSITS_FOR_BLOCK_RANGE",
|
||||
/** No depositRoot for depositCount */
|
||||
NO_DEPOSIT_ROOT = "ETH1_ERROR_NO_DEPOSIT_ROOT",
|
||||
/** Not enough deposit roots for index */
|
||||
NOT_ENOUGH_DEPOSIT_ROOTS = "ETH1_ERROR_NOT_ENOUGH_DEPOSIT_ROOTS",
|
||||
|
||||
/** Attempted to insert a duplicate log for same index into the Eth1DepositsCache */
|
||||
DUPLICATE_DISTINCT_LOG = "ETH1_ERROR_DUPLICATE_DISTINCT_LOG",
|
||||
/** Attempted to insert a log with index != prev + 1 into the Eth1DepositsCache */
|
||||
NON_CONSECUTIVE_LOGS = "ETH1_ERROR_NON_CONSECUTIVE_LOGS",
|
||||
/** Expected a deposit log in the db for the index, missing log implies a corrupted db */
|
||||
MISSING_DEPOSIT_LOG = "ETH1_ERROR_MISSING_DEPOSIT_LOG",
|
||||
}
|
||||
|
||||
export type Eth1ErrorType =
|
||||
| {code: Eth1ErrorCode.DEPOSIT_INDEX_TOO_HIGH; depositIndex: number; depositCount: number}
|
||||
| {code: Eth1ErrorCode.NOT_ENOUGH_DEPOSITS; len: number; expectedLen: number}
|
||||
| {code: Eth1ErrorCode.TOO_MANY_DEPOSITS; len: number; expectedLen: number}
|
||||
| {code: Eth1ErrorCode.WRONG_DEPOSIT_ROOT; root: string; expectedRoot: string}
|
||||
| {code: Eth1ErrorCode.NO_DEPOSITS_FOR_BLOCK_RANGE; fromBlock: number; toBlock: number}
|
||||
| {code: Eth1ErrorCode.NO_DEPOSIT_ROOT; depositCount: number}
|
||||
| {code: Eth1ErrorCode.NOT_ENOUGH_DEPOSIT_ROOTS; index: number; treeLength: number}
|
||||
| {code: Eth1ErrorCode.DUPLICATE_DISTINCT_LOG; newIndex: number; lastLogIndex: number}
|
||||
| {code: Eth1ErrorCode.NON_CONSECUTIVE_LOGS; newIndex: number; lastLogIndex: number}
|
||||
| {code: Eth1ErrorCode.MISSING_DEPOSIT_LOG; newIndex: number; lastLogIndex: number};
|
||||
|
||||
export class Eth1Error extends LodestarError<Eth1ErrorType> {}
|
||||
@@ -1,26 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {phase0} from "@lodestar/types";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
|
||||
export class Eth1DataCache {
|
||||
db: IBeaconDb;
|
||||
config: ChainForkConfig;
|
||||
|
||||
constructor(config: ChainForkConfig, db: IBeaconDb) {
|
||||
this.config = config;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async get({timestampRange}: {timestampRange: {gte: number; lte: number}}): Promise<phase0.Eth1DataOrdered[]> {
|
||||
return this.db.eth1Data.values(timestampRange);
|
||||
}
|
||||
|
||||
async add(eth1Datas: (phase0.Eth1DataOrdered & {timestamp: number})[]): Promise<void> {
|
||||
await this.db.eth1Data.batchPutValues(eth1Datas);
|
||||
}
|
||||
|
||||
async getHighestCachedBlockNumber(): Promise<number | null> {
|
||||
const highestEth1Data = await this.db.eth1Data.lastValue();
|
||||
return highestEth1Data?.blockNumber ?? null;
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {
|
||||
BeaconStateAllForks,
|
||||
CachedBeaconStateAllForks,
|
||||
CachedBeaconStateElectra,
|
||||
becomesNewEth1Data,
|
||||
} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {ErrorAborted, Logger, TimeoutError, fromHex, isErrorAborted, sleep} from "@lodestar/utils";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
import {Metrics} from "../metrics/index.js";
|
||||
import {Eth1DataCache} from "./eth1DataCache.js";
|
||||
import {Eth1DepositsCache} from "./eth1DepositsCache.js";
|
||||
import {Eth1DataAndDeposits, EthJsonRpcBlockRaw, IEth1Provider} from "./interface.js";
|
||||
import {Eth1Options} from "./options.js";
|
||||
import {parseEth1Block} from "./provider/eth1Provider.js";
|
||||
import {HttpRpcError} from "./provider/jsonRpcHttpClient.js";
|
||||
import {isJsonRpcTruncatedError} from "./provider/utils.js";
|
||||
import {getDeposits} from "./utils/deposits.js";
|
||||
import {getEth1VotesToConsider, pickEth1Vote} from "./utils/eth1Vote.js";
|
||||
|
||||
const MAX_BLOCKS_PER_BLOCK_QUERY = 1000;
|
||||
const MIN_BLOCKS_PER_BLOCK_QUERY = 10;
|
||||
|
||||
const MAX_BLOCKS_PER_LOG_QUERY = 1000;
|
||||
const MIN_BLOCKS_PER_LOG_QUERY = 10;
|
||||
|
||||
/** Eth1 blocks happen every 14s approx, not need to update too often once synced */
|
||||
const AUTO_UPDATE_PERIOD_MS = 60 * 1000;
|
||||
/** Prevent infinite loops */
|
||||
const MIN_UPDATE_PERIOD_MS = 1 * 1000;
|
||||
/** Milliseconds to wait after getting 429 Too Many Requests */
|
||||
const RATE_LIMITED_WAIT_MS = 30 * 1000;
|
||||
/** Min time to wait on auto update loop on unknown error */
|
||||
const MIN_WAIT_ON_ERROR_MS = 1 * 1000;
|
||||
|
||||
/** Number of blocks to download if the node detects it is lagging behind due to an inaccurate
|
||||
relationship between block-number-based follow distance and time-based follow distance. */
|
||||
const ETH1_FOLLOW_DISTANCE_DELTA_IF_SLOW = 32;
|
||||
|
||||
/** The absolute minimum follow distance to enforce when downloading catchup batches, from LH */
|
||||
const ETH_MIN_FOLLOW_DISTANCE = 64;
|
||||
|
||||
export type Eth1DepositDataTrackerModules = {
|
||||
config: ChainForkConfig;
|
||||
db: IBeaconDb;
|
||||
metrics: Metrics | null;
|
||||
logger: Logger;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main class handling eth1 data fetching, processing and storing
|
||||
* Upon instantiation, starts fetching deposits and blocks at regular intervals
|
||||
*/
|
||||
export class Eth1DepositDataTracker {
|
||||
private config: ChainForkConfig;
|
||||
private logger: Logger;
|
||||
private signal: AbortSignal;
|
||||
private readonly metrics: Metrics | null;
|
||||
|
||||
// Internal modules, state
|
||||
private depositsCache: Eth1DepositsCache;
|
||||
private eth1DataCache: Eth1DataCache;
|
||||
private lastProcessedDepositBlockNumber: number | null = null;
|
||||
|
||||
/** Dynamically adjusted follow distance */
|
||||
private eth1FollowDistance: number;
|
||||
/** Dynamically adjusted batch size to fetch deposit logs */
|
||||
private eth1GetBlocksBatchSizeDynamic = MAX_BLOCKS_PER_BLOCK_QUERY;
|
||||
/** Dynamically adjusted batch size to fetch deposit logs */
|
||||
private eth1GetLogsBatchSizeDynamic = MAX_BLOCKS_PER_LOG_QUERY;
|
||||
private readonly forcedEth1DataVote: phase0.Eth1Data | null;
|
||||
/** To stop `runAutoUpdate()` in addition to AbortSignal */
|
||||
private stopPolling: boolean;
|
||||
|
||||
constructor(
|
||||
opts: Eth1Options,
|
||||
{config, db, metrics, logger, signal}: Eth1DepositDataTrackerModules,
|
||||
private readonly eth1Provider: IEth1Provider
|
||||
) {
|
||||
this.config = config;
|
||||
this.metrics = metrics;
|
||||
this.logger = logger;
|
||||
this.signal = signal;
|
||||
this.eth1Provider = eth1Provider;
|
||||
this.depositsCache = new Eth1DepositsCache(opts, config, db);
|
||||
this.eth1DataCache = new Eth1DataCache(config, db);
|
||||
this.eth1FollowDistance = config.ETH1_FOLLOW_DISTANCE;
|
||||
this.stopPolling = false;
|
||||
|
||||
this.forcedEth1DataVote = opts.forcedEth1DataVote
|
||||
? ssz.phase0.Eth1Data.deserialize(fromHex(opts.forcedEth1DataVote))
|
||||
: null;
|
||||
|
||||
if (opts.depositContractDeployBlock === undefined) {
|
||||
this.logger.warn("No depositContractDeployBlock provided");
|
||||
}
|
||||
|
||||
if (metrics) {
|
||||
// Set constant value once
|
||||
metrics?.eth1.eth1FollowDistanceSecondsConfig.set(config.SECONDS_PER_ETH1_BLOCK * config.ETH1_FOLLOW_DISTANCE);
|
||||
metrics.eth1.eth1FollowDistanceDynamic.addCollect(() => {
|
||||
metrics.eth1.eth1FollowDistanceDynamic.set(this.eth1FollowDistance);
|
||||
metrics.eth1.eth1GetBlocksBatchSizeDynamic.set(this.eth1GetBlocksBatchSizeDynamic);
|
||||
metrics.eth1.eth1GetLogsBatchSizeDynamic.set(this.eth1GetLogsBatchSizeDynamic);
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.enabled) {
|
||||
this.runAutoUpdate().catch((e: Error) => {
|
||||
if (!(e instanceof ErrorAborted)) {
|
||||
this.logger.error("Error on eth1 loop", {}, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isPollingEth1Data(): boolean {
|
||||
return !this.stopPolling;
|
||||
}
|
||||
|
||||
stopPollingEth1Data(): void {
|
||||
this.stopPolling = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return eth1Data and deposits ready for block production for a given state
|
||||
*/
|
||||
async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits> {
|
||||
if (
|
||||
state.epochCtx.isPostElectra() &&
|
||||
state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositRequestsStartIndex
|
||||
) {
|
||||
// No need to poll eth1Data since Electra deprecates the mechanism after depositRequestsStartIndex is reached
|
||||
return {eth1Data: state.eth1Data, deposits: []};
|
||||
}
|
||||
const eth1Data = this.forcedEth1DataVote ?? (await this.getEth1Data(state));
|
||||
const deposits = await this.getDeposits(state, eth1Data);
|
||||
return {eth1Data, deposits};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an eth1Data vote for a given state.
|
||||
* Requires internal caches to be updated regularly to return good results
|
||||
*/
|
||||
private async getEth1Data(state: BeaconStateAllForks): Promise<phase0.Eth1Data> {
|
||||
try {
|
||||
const eth1VotesToConsider = await getEth1VotesToConsider(
|
||||
this.config,
|
||||
state,
|
||||
this.eth1DataCache.get.bind(this.eth1DataCache)
|
||||
);
|
||||
return pickEth1Vote(state, eth1VotesToConsider);
|
||||
} catch (e) {
|
||||
// Note: In case there's a DB issue, don't stop a block proposal. Just vote for current eth1Data
|
||||
this.logger.error("CRITICAL: Error reading valid votes, voting for current eth1Data", {}, e as Error);
|
||||
return state.eth1Data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns deposits to be included for a given state and eth1Data vote.
|
||||
* Requires internal caches to be updated regularly to return good results
|
||||
*/
|
||||
private async getDeposits(
|
||||
state: CachedBeaconStateAllForks,
|
||||
eth1DataVote: phase0.Eth1Data
|
||||
): Promise<phase0.Deposit[]> {
|
||||
// No new deposits have to be included, continue
|
||||
if (eth1DataVote.depositCount === state.eth1DepositIndex) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: Review if this is optimal
|
||||
// Convert to view first to hash once and compare hashes
|
||||
const eth1DataVoteView = ssz.phase0.Eth1Data.toViewDU(eth1DataVote);
|
||||
|
||||
// Eth1 data may change due to the vote included in this block
|
||||
const newEth1Data = becomesNewEth1Data(state, eth1DataVoteView) ? eth1DataVoteView : state.eth1Data;
|
||||
return getDeposits(state, newEth1Data, this.depositsCache.get.bind(this.depositsCache));
|
||||
}
|
||||
|
||||
/**
|
||||
* Abortable async setInterval that runs its callback once at max between `ms` at minimum
|
||||
*/
|
||||
private async runAutoUpdate(): Promise<void> {
|
||||
let lastRunMs = 0;
|
||||
|
||||
while (!this.signal.aborted && !this.stopPolling) {
|
||||
lastRunMs = Date.now();
|
||||
|
||||
try {
|
||||
const hasCaughtUp = await this.update();
|
||||
|
||||
this.metrics?.eth1.depositTrackerIsCaughtup.set(hasCaughtUp ? 1 : 0);
|
||||
|
||||
if (hasCaughtUp) {
|
||||
const sleepTimeMs = Math.max(AUTO_UPDATE_PERIOD_MS + lastRunMs - Date.now(), MIN_UPDATE_PERIOD_MS);
|
||||
await sleep(sleepTimeMs, this.signal);
|
||||
}
|
||||
} catch (e) {
|
||||
this.metrics?.eth1.depositTrackerUpdateErrors.inc(1);
|
||||
|
||||
// From Infura: 429 Too Many Requests
|
||||
if (e instanceof HttpRpcError && e.status === 429) {
|
||||
this.logger.debug("Eth1 provider rate limited", {}, e);
|
||||
await sleep(RATE_LIMITED_WAIT_MS, this.signal);
|
||||
// only log error if state switched from online to some other state
|
||||
} else if (!isErrorAborted(e)) {
|
||||
await sleep(MIN_WAIT_ON_ERROR_MS, this.signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the deposit and block cache, returning an error if either fail
|
||||
* @returns true if it has catched up to the remote follow block
|
||||
*/
|
||||
private async update(): Promise<boolean> {
|
||||
const remoteHighestBlock = await this.eth1Provider.getBlockNumber();
|
||||
this.metrics?.eth1.remoteHighestBlock.set(remoteHighestBlock);
|
||||
|
||||
const remoteFollowBlock = remoteHighestBlock - this.eth1FollowDistance;
|
||||
|
||||
// If remoteFollowBlock is not at or beyond deployBlock, there is no need to
|
||||
// fetch and track any deposit data yet
|
||||
if (remoteFollowBlock < (this.eth1Provider.deployBlock ?? 0)) return true;
|
||||
|
||||
const hasCaughtUpDeposits = await this.updateDepositCache(remoteFollowBlock);
|
||||
const hasCaughtUpBlocks = await this.updateBlockCache(remoteFollowBlock);
|
||||
return hasCaughtUpDeposits && hasCaughtUpBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch deposit events from remote eth1 node up to follow-distance block
|
||||
* @returns true if it has catched up to the remote follow block
|
||||
*/
|
||||
private async updateDepositCache(remoteFollowBlock: number): Promise<boolean> {
|
||||
const lastProcessedDepositBlockNumber = await this.getLastProcessedDepositBlockNumber();
|
||||
// The DB may contain deposits from a different chain making lastProcessedDepositBlockNumber > current chain tip
|
||||
// The Math.min() fixes those rare scenarios where fromBlock > toBlock
|
||||
const fromBlock = Math.min(remoteFollowBlock, this.getFromBlockToFetch(lastProcessedDepositBlockNumber));
|
||||
const toBlock = Math.min(remoteFollowBlock, fromBlock + this.eth1GetLogsBatchSizeDynamic - 1);
|
||||
|
||||
let depositEvents: phase0.DepositEvent[];
|
||||
try {
|
||||
depositEvents = await this.eth1Provider.getDepositEvents(fromBlock, toBlock);
|
||||
// Increase the batch size linearly even if we scale down exponentially (half each time)
|
||||
this.eth1GetLogsBatchSizeDynamic = Math.min(
|
||||
MAX_BLOCKS_PER_LOG_QUERY,
|
||||
this.eth1GetLogsBatchSizeDynamic + MIN_BLOCKS_PER_LOG_QUERY
|
||||
);
|
||||
} catch (e) {
|
||||
if (isJsonRpcTruncatedError(e as Error) || e instanceof TimeoutError) {
|
||||
this.eth1GetLogsBatchSizeDynamic = Math.max(
|
||||
MIN_BLOCKS_PER_LOG_QUERY,
|
||||
Math.floor(this.eth1GetLogsBatchSizeDynamic / 2)
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logger.verbose("Fetched deposits", {depositCount: depositEvents.length, fromBlock, toBlock});
|
||||
this.metrics?.eth1.depositEventsFetched.inc(depositEvents.length);
|
||||
|
||||
await this.depositsCache.add(depositEvents);
|
||||
// Store the `toBlock` since that block may not contain
|
||||
this.lastProcessedDepositBlockNumber = toBlock;
|
||||
this.metrics?.eth1.lastProcessedDepositBlockNumber.set(toBlock);
|
||||
|
||||
return toBlock >= remoteFollowBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch block headers from a remote eth1 node up to follow-distance block
|
||||
*
|
||||
* depositRoot and depositCount are inferred from already fetched deposits.
|
||||
* Calling get_deposit_root() and the smart contract for a non-latest block requires an
|
||||
* archive node, something most users don't have access too.
|
||||
* @returns true if it has catched up to the remote follow timestamp
|
||||
*/
|
||||
private async updateBlockCache(remoteFollowBlock: number): Promise<boolean> {
|
||||
const lastCachedBlock = await this.eth1DataCache.getHighestCachedBlockNumber();
|
||||
// lastProcessedDepositBlockNumber sets the upper bound of the possible block range to fetch in this update
|
||||
const lastProcessedDepositBlockNumber = await this.getLastProcessedDepositBlockNumber();
|
||||
// lowestEventBlockNumber set a lower bound of possible block range to fetch in this update
|
||||
const lowestEventBlockNumber = await this.depositsCache.getLowestDepositEventBlockNumber();
|
||||
|
||||
// We are all caught up if:
|
||||
// 1. If lowestEventBlockNumber is null = no deposits have been fetch or found yet.
|
||||
// So there's not useful blocks to fetch until at least 1 deposit is found.
|
||||
// 2. If the remoteFollowBlock is behind the lowestEventBlockNumber. This can happen
|
||||
// if the EL's data was wiped and restarted. Not exiting here would other wise
|
||||
// cause a NO_DEPOSITS_FOR_BLOCK_RANGE error
|
||||
if (
|
||||
lowestEventBlockNumber === null ||
|
||||
lastProcessedDepositBlockNumber === null ||
|
||||
remoteFollowBlock < lowestEventBlockNumber
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cap the upper limit of fromBlock with remoteFollowBlock in case deployBlock is set to a different network value
|
||||
const fromBlock = Math.min(
|
||||
remoteFollowBlock,
|
||||
// Fetch from the last cached block or the lowest known deposit block number
|
||||
Math.max(this.getFromBlockToFetch(lastCachedBlock), lowestEventBlockNumber)
|
||||
);
|
||||
const toBlock = Math.min(
|
||||
remoteFollowBlock,
|
||||
fromBlock + this.eth1GetBlocksBatchSizeDynamic - 1, // Block range is inclusive
|
||||
lastProcessedDepositBlockNumber
|
||||
);
|
||||
|
||||
let blocksRaw: EthJsonRpcBlockRaw[];
|
||||
try {
|
||||
blocksRaw = await this.eth1Provider.getBlocksByNumber(fromBlock, toBlock);
|
||||
// Increase the batch size linearly even if we scale down exponentially (half each time)
|
||||
this.eth1GetBlocksBatchSizeDynamic = Math.min(
|
||||
MAX_BLOCKS_PER_BLOCK_QUERY,
|
||||
this.eth1GetBlocksBatchSizeDynamic + MIN_BLOCKS_PER_BLOCK_QUERY
|
||||
);
|
||||
} catch (e) {
|
||||
if (isJsonRpcTruncatedError(e as Error) || e instanceof TimeoutError) {
|
||||
this.eth1GetBlocksBatchSizeDynamic = Math.max(
|
||||
MIN_BLOCKS_PER_BLOCK_QUERY,
|
||||
Math.floor(this.eth1GetBlocksBatchSizeDynamic / 2)
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const blocks = blocksRaw.map(parseEth1Block);
|
||||
|
||||
this.logger.verbose("Fetched eth1 blocks", {blockCount: blocks.length, fromBlock, toBlock});
|
||||
this.metrics?.eth1.blocksFetched.inc(blocks.length);
|
||||
this.metrics?.eth1.lastFetchedBlockBlockNumber.set(toBlock);
|
||||
const lastBlock = blocks.at(-1);
|
||||
if (lastBlock) {
|
||||
this.metrics?.eth1.lastFetchedBlockTimestamp.set(lastBlock.timestamp);
|
||||
}
|
||||
|
||||
const eth1Datas = await this.depositsCache.getEth1DataForBlocks(blocks, lastProcessedDepositBlockNumber);
|
||||
await this.eth1DataCache.add(eth1Datas);
|
||||
|
||||
// Note: ETH1_FOLLOW_DISTANCE_SECONDS = ETH1_FOLLOW_DISTANCE * SECONDS_PER_ETH1_BLOCK
|
||||
// Deposit tracker must fetch blocks and deposits up to ETH1_FOLLOW_DISTANCE_SECONDS,
|
||||
// measured in time not blocks. To vote on valid votes it must populate up to the time based follow distance.
|
||||
// If it assumes SECONDS_PER_ETH1_BLOCK but block times are:
|
||||
// - slower: Cache will not contain all blocks
|
||||
// - faster: Cache will contain all required blocks + some ahead of timed follow distance
|
||||
//
|
||||
// For mainnet we must fetch blocks up until block.timestamp < now - 28672 sec. Based on follow distance:
|
||||
// Block times | actual follow distance
|
||||
// 14 | 2048
|
||||
// 20 | 1434
|
||||
// 30 | 956
|
||||
// 60 | 478
|
||||
//
|
||||
// So if after fetching the block at ETH1_FOLLOW_DISTANCE, but it's timestamp is not greater than
|
||||
// ETH1_FOLLOW_DISTANCE_SECONDS, reduce the ETH1_FOLLOW_DISTANCE by a small delta and fetch more blocks.
|
||||
// Otherwise if the last fetched block if above ETH1_FOLLOW_DISTANCE_SECONDS, reduce ETH1_FOLLOW_DISTANCE.
|
||||
|
||||
if (toBlock < remoteFollowBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastBlock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const remoteFollowBlockTimestamp =
|
||||
Math.round(Date.now() / 1000) - this.config.SECONDS_PER_ETH1_BLOCK * this.config.ETH1_FOLLOW_DISTANCE;
|
||||
const blockAfterTargetTimestamp = blocks.find((block) => block.timestamp >= remoteFollowBlockTimestamp);
|
||||
|
||||
if (blockAfterTargetTimestamp) {
|
||||
// Catched up to target timestamp, increase eth1FollowDistance. Limit max config.ETH1_FOLLOW_DISTANCE.
|
||||
// If the block that's right above the timestamp has been fetched now, use it to compute the precise delta.
|
||||
const delta = Math.max(lastBlock.blockNumber - blockAfterTargetTimestamp.blockNumber, 1);
|
||||
this.eth1FollowDistance = Math.min(this.eth1FollowDistance + delta, this.config.ETH1_FOLLOW_DISTANCE);
|
||||
|
||||
return true;
|
||||
}
|
||||
// Blocks are slower than expected, reduce eth1FollowDistance. Limit min CATCHUP_MIN_FOLLOW_DISTANCE
|
||||
const delta =
|
||||
this.eth1FollowDistance -
|
||||
Math.max(this.eth1FollowDistance - ETH1_FOLLOW_DISTANCE_DELTA_IF_SLOW, ETH_MIN_FOLLOW_DISTANCE);
|
||||
this.eth1FollowDistance = this.eth1FollowDistance - delta;
|
||||
|
||||
// Even if the blocks are slow, when we are all caught up as there is no
|
||||
// further possibility to reduce follow distance, we need to call it quits
|
||||
// for now, else it leads to an incessant poll on the EL
|
||||
return delta === 0;
|
||||
}
|
||||
|
||||
private getFromBlockToFetch(lastCachedBlock: number | null): number {
|
||||
if (lastCachedBlock === null) {
|
||||
return this.eth1Provider.deployBlock ?? 0;
|
||||
}
|
||||
return lastCachedBlock + 1;
|
||||
}
|
||||
|
||||
private async getLastProcessedDepositBlockNumber(): Promise<number | null> {
|
||||
if (this.lastProcessedDepositBlockNumber === null) {
|
||||
this.lastProcessedDepositBlockNumber = await this.depositsCache.getHighestDepositEventBlockNumber();
|
||||
}
|
||||
return this.lastProcessedDepositBlockNumber;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import {byteArrayEquals} from "@chainsafe/ssz";
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {FilterOptions} from "@lodestar/db";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
import {Eth1Error, Eth1ErrorCode} from "./errors.js";
|
||||
import {Eth1Block} from "./interface.js";
|
||||
import {getDepositsWithProofs} from "./utils/deposits.js";
|
||||
import {getEth1DataForBlocks} from "./utils/eth1Data.js";
|
||||
import {assertConsecutiveDeposits} from "./utils/eth1DepositEvent.js";
|
||||
|
||||
export class Eth1DepositsCache {
|
||||
unsafeAllowDepositDataOverwrite: boolean;
|
||||
db: IBeaconDb;
|
||||
config: ChainForkConfig;
|
||||
|
||||
constructor(opts: {unsafeAllowDepositDataOverwrite?: boolean}, config: ChainForkConfig, db: IBeaconDb) {
|
||||
this.config = config;
|
||||
this.db = db;
|
||||
this.unsafeAllowDepositDataOverwrite = opts.unsafeAllowDepositDataOverwrite ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of `Deposit` objects, within the given deposit index `range`.
|
||||
*
|
||||
* The `depositCount` is used to generate the proofs for the `Deposits`. For example, if we
|
||||
* have 100 proofs, but the Ethereum Consensus chain only acknowledges 50 of them, we must produce our
|
||||
* proofs with respect to a tree size of 50.
|
||||
*/
|
||||
async get(indexRange: FilterOptions<number>, eth1Data: phase0.Eth1Data): Promise<phase0.Deposit[]> {
|
||||
const depositEvents = await this.db.depositEvent.values(indexRange);
|
||||
const depositRootTree = await this.db.depositDataRoot.getDepositRootTree();
|
||||
return getDepositsWithProofs(depositEvents, depositRootTree, eth1Data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add log to cache
|
||||
* This function enforces that `logs` are imported one-by-one with consecutive indexes
|
||||
*/
|
||||
async add(depositEvents: phase0.DepositEvent[]): Promise<void> {
|
||||
assertConsecutiveDeposits(depositEvents);
|
||||
|
||||
const lastLog = await this.db.depositEvent.lastValue();
|
||||
const firstEvent = depositEvents[0];
|
||||
|
||||
// Check, validate and skip if we got any deposit events already present in DB
|
||||
// This can happen if the remote eth1/EL resets its head in these four scenarios:
|
||||
// 1. Remote eth1/EL resynced/restarted from head behind its previous head pre-merge
|
||||
// 2. In a post merge scenario, Lodestar restarted from finalized state from DB which
|
||||
// generally is a few epochs behind the last synced head. This causes eth1 tracker to reset
|
||||
// and refetch the deposits as the lodestar syncs further along (Post merge there is 1-1
|
||||
// correspondence between EL and CL blocks)
|
||||
// 3. The EL reorged beyond the eth1 follow distance.
|
||||
//
|
||||
// While 1. & 2. are benign and we handle them below by checking if the duplicate log fetched
|
||||
// is same as one written in DB. Refer to this issue for some data dump of how this happens
|
||||
// https://github.com/ChainSafe/lodestar/issues/3674
|
||||
//
|
||||
// If the duplicate log fetched is not same as written in DB then its probablu scenario 3.
|
||||
// which would be a catastrophic event for the network (or we messed up real bad!!!).
|
||||
//
|
||||
// So we provide for a way to overwrite this log without deleting full db via
|
||||
// --unsafeAllowDepositDataOverwrite cli flag which will just overwrite the previous tracker data
|
||||
// if any. This option as indicated by its name is unsafe and to be only used if you know what
|
||||
// you are doing.
|
||||
if (lastLog !== null && firstEvent !== undefined) {
|
||||
const newIndex = firstEvent.index;
|
||||
const lastLogIndex = lastLog.index;
|
||||
|
||||
if (!this.unsafeAllowDepositDataOverwrite && firstEvent.index <= lastLog.index) {
|
||||
// lastLogIndex - newIndex + 1 events are duplicate since this is a consecutive log
|
||||
// as asserted by assertConsecutiveDeposits. Splice those events out from depositEvents.
|
||||
const skipEvents = depositEvents.splice(0, lastLogIndex - newIndex + 1);
|
||||
// After splicing skipEvents will contain duplicate events to be checked and validated
|
||||
// and rest of the remaining events in depositEvents could be safely written to DB and
|
||||
// move the tracker along.
|
||||
for (const depositEvent of skipEvents) {
|
||||
const prevDBSerializedEvent = await this.db.depositEvent.getBinary(depositEvent.index);
|
||||
if (!prevDBSerializedEvent) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.MISSING_DEPOSIT_LOG, newIndex, lastLogIndex});
|
||||
}
|
||||
const serializedEvent = ssz.phase0.DepositEvent.serialize(depositEvent);
|
||||
if (!byteArrayEquals(prevDBSerializedEvent, serializedEvent)) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.DUPLICATE_DISTINCT_LOG, newIndex, lastLogIndex});
|
||||
}
|
||||
}
|
||||
} else if (newIndex > lastLogIndex + 1) {
|
||||
// deposit events need to be consective, the way we fetch our tracker. If the deposit event
|
||||
// is not consecutive it means either our tracker, or the corresponding eth1/EL
|
||||
// node or the database has messed up. All these failures are critical and the tracker
|
||||
// shouldn't proceed without the resolution of this error.
|
||||
throw new Eth1Error({code: Eth1ErrorCode.NON_CONSECUTIVE_LOGS, newIndex, lastLogIndex});
|
||||
}
|
||||
}
|
||||
|
||||
const depositRoots = depositEvents.map((depositEvent) => ({
|
||||
index: depositEvent.index,
|
||||
root: ssz.phase0.DepositData.hashTreeRoot(depositEvent.depositData),
|
||||
}));
|
||||
|
||||
// Store events after verifying that data is consecutive
|
||||
// depositDataRoot will throw if adding non consecutive roots
|
||||
await this.db.depositDataRoot.batchPutValues(depositRoots);
|
||||
await this.db.depositEvent.batchPutValues(depositEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends partial eth1 data (depositRoot, depositCount) in a block range (inclusive)
|
||||
* Returned array is sequential and ascending in blockNumber
|
||||
* @param fromBlock
|
||||
* @param toBlock
|
||||
*/
|
||||
async getEth1DataForBlocks(
|
||||
blocks: Eth1Block[],
|
||||
lastProcessedDepositBlockNumber: number | null
|
||||
): Promise<(phase0.Eth1Data & Eth1Block)[]> {
|
||||
const highestBlock = blocks.at(-1)?.blockNumber;
|
||||
return getEth1DataForBlocks(
|
||||
blocks,
|
||||
this.db.depositEvent.valuesStream({lte: highestBlock, reverse: true}),
|
||||
await this.db.depositDataRoot.getDepositRootTree(),
|
||||
lastProcessedDepositBlockNumber
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest blockNumber stored in DB if any
|
||||
*/
|
||||
async getHighestDepositEventBlockNumber(): Promise<number | null> {
|
||||
const latestEvent = await this.db.depositEvent.lastValue();
|
||||
return latestEvent?.blockNumber || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lowest blockNumber stored in DB if any
|
||||
*/
|
||||
async getLowestDepositEventBlockNumber(): Promise<number | null> {
|
||||
const firstEvent = await this.db.depositEvent.firstValue();
|
||||
return firstEvent?.blockNumber || null;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {Eth1DepositDataTracker, Eth1DepositDataTrackerModules} from "./eth1DepositDataTracker.js";
|
||||
import {Eth1DataAndDeposits, IEth1ForBlockProduction, IEth1Provider} from "./interface.js";
|
||||
import {Eth1Options} from "./options.js";
|
||||
import {Eth1Provider} from "./provider/eth1Provider.js";
|
||||
export {Eth1Provider};
|
||||
export type {IEth1ForBlockProduction, IEth1Provider};
|
||||
|
||||
// This module encapsulates all consumer functionality to the execution node (formerly eth1). The execution client
|
||||
// has to:
|
||||
//
|
||||
// - For genesis, the beacon node must follow the eth1 chain: get all deposit events + blocks within that range.
|
||||
// Once the genesis conditions are met, start the POS chain with the resulting state. The logic is similar to the
|
||||
// two points below, but the implementation is specialized for each scenario.
|
||||
//
|
||||
// - Follow the eth1 block chain to validate eth1Data votes. It needs all consecutive blocks within a specific range
|
||||
// and at a distance from the head.
|
||||
// ETH1_FOLLOW_DISTANCE uint64(2**11) (= 2,048) Eth1 blocks ~8 hours
|
||||
// EPOCHS_PER_ETH1_VOTING_PERIOD uint64(2**6) (= 64) epochs ~6.8 hours
|
||||
//
|
||||
// - Fetch ALL deposit events from the deposit contract to build the deposit tree and validate future merkle proofs.
|
||||
// Then it must follow deposit events at a distance roughly similar to the `ETH1_FOLLOW_DISTANCE` parameter above.
|
||||
|
||||
export function initializeEth1ForBlockProduction(
|
||||
opts: Eth1Options,
|
||||
modules: Pick<Eth1DepositDataTrackerModules, "db" | "config" | "metrics" | "logger" | "signal">
|
||||
): IEth1ForBlockProduction {
|
||||
if (opts.enabled) {
|
||||
return new Eth1ForBlockProduction(opts, {
|
||||
config: modules.config,
|
||||
db: modules.db,
|
||||
metrics: modules.metrics,
|
||||
logger: modules.logger,
|
||||
signal: modules.signal,
|
||||
});
|
||||
}
|
||||
return new Eth1ForBlockProductionDisabled();
|
||||
}
|
||||
|
||||
export class Eth1ForBlockProduction implements IEth1ForBlockProduction {
|
||||
private readonly eth1DepositDataTracker: Eth1DepositDataTracker | null;
|
||||
|
||||
constructor(opts: Eth1Options, modules: Eth1DepositDataTrackerModules & {eth1Provider?: IEth1Provider}) {
|
||||
const eth1Provider =
|
||||
modules.eth1Provider ||
|
||||
new Eth1Provider(
|
||||
modules.config,
|
||||
{...opts, logger: modules.logger},
|
||||
modules.signal,
|
||||
modules.metrics?.eth1HttpClient
|
||||
);
|
||||
|
||||
this.eth1DepositDataTracker = opts.disableEth1DepositDataTracker
|
||||
? null
|
||||
: new Eth1DepositDataTracker(opts, modules, eth1Provider);
|
||||
}
|
||||
|
||||
async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits> {
|
||||
if (this.eth1DepositDataTracker === null) {
|
||||
return {eth1Data: state.eth1Data, deposits: []};
|
||||
}
|
||||
return this.eth1DepositDataTracker.getEth1DataAndDeposits(state);
|
||||
}
|
||||
|
||||
isPollingEth1Data(): boolean {
|
||||
return this.eth1DepositDataTracker?.isPollingEth1Data() ?? false;
|
||||
}
|
||||
|
||||
stopPollingEth1Data(): void {
|
||||
this.eth1DepositDataTracker?.stopPollingEth1Data();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled version of Eth1ForBlockProduction
|
||||
* May produce invalid blocks by not adding new deposits and voting for the same eth1Data
|
||||
*/
|
||||
export class Eth1ForBlockProductionDisabled implements IEth1ForBlockProduction {
|
||||
/**
|
||||
* Returns same eth1Data as in state and no deposits
|
||||
* May produce invalid blocks if deposits have to be added
|
||||
*/
|
||||
async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits> {
|
||||
return {eth1Data: state.eth1Data, deposits: []};
|
||||
}
|
||||
|
||||
isPollingEth1Data(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
stopPollingEth1Data(): void {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import {BeaconConfig} from "@lodestar/config";
|
||||
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {phase0} from "@lodestar/types";
|
||||
|
||||
export type EthJsonRpcBlockRaw = {
|
||||
/** the block number. null when its pending block. `"0x1b4"` */
|
||||
number: string;
|
||||
/** 32 Bytes - hash of the block. null when its pending block. `"0xdc0818cf78f21a8e70579cb46a43643f78291264dda342ae31049421c82d21ae"` */
|
||||
hash: string;
|
||||
/** 32 Bytes - hash of the parent block. `"0xe99e022112df268087ea7eafaf4790497fd21dbeeb6bd7a1721df161a6657a54"` */
|
||||
parentHash: string;
|
||||
/**
|
||||
* integer of the total difficulty of the chain until this block. `"0x78ed983323d"`.
|
||||
* Current mainnet value is 0x684de10dc5c03f006b6, 75 bits so requires a bigint.
|
||||
*/
|
||||
totalDifficulty: string;
|
||||
/** the unix timestamp for when the block was collated. `"0x55ba467c"` */
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export interface IEth1Provider {
|
||||
deployBlock: number;
|
||||
getBlockNumber(): Promise<number>;
|
||||
/** Returns HTTP code 200 + value=null if block is not found */
|
||||
getBlockByNumber(blockNumber: number | "latest"): Promise<EthJsonRpcBlockRaw | null>;
|
||||
/** Returns HTTP code 200 + value=null if block is not found */
|
||||
getBlockByHash(blockHashHex: string): Promise<EthJsonRpcBlockRaw | null>;
|
||||
/** null returns are ignored, may return a different number of blocks than expected */
|
||||
getBlocksByNumber(fromBlock: number, toBlock: number): Promise<EthJsonRpcBlockRaw[]>;
|
||||
getDepositEvents(fromBlock: number, toBlock: number): Promise<phase0.DepositEvent[]>;
|
||||
validateContract(): Promise<void>;
|
||||
getState(): Eth1ProviderState;
|
||||
}
|
||||
|
||||
export enum Eth1ProviderState {
|
||||
ONLINE = "ONLINE",
|
||||
OFFLINE = "OFFLINE",
|
||||
ERROR = "ERROR",
|
||||
AUTH_FAILED = "AUTH_FAILED",
|
||||
}
|
||||
|
||||
export type Eth1DataAndDeposits = {
|
||||
eth1Data: phase0.Eth1Data;
|
||||
deposits: phase0.Deposit[];
|
||||
};
|
||||
|
||||
export interface IEth1ForBlockProduction {
|
||||
getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits>;
|
||||
|
||||
isPollingEth1Data(): boolean;
|
||||
|
||||
/**
|
||||
* Should stop polling eth1Data after a Electra block is finalized AND deposit_requests_start_index is reached
|
||||
*/
|
||||
stopPollingEth1Data(): void;
|
||||
}
|
||||
|
||||
/** Different Eth1Block from phase0.Eth1Block with blockHash */
|
||||
export type Eth1Block = {
|
||||
blockHash: Uint8Array;
|
||||
blockNumber: number;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type BatchDepositEvents = {
|
||||
depositEvents: phase0.DepositEvent[];
|
||||
blockNumber: number;
|
||||
};
|
||||
|
||||
export type Eth1Streamer = {
|
||||
getDepositsStream(fromBlock: number): AsyncGenerator<BatchDepositEvents>;
|
||||
getDepositsAndBlockStreamForGenesis(fromBlock: number): AsyncGenerator<[phase0.DepositEvent[], phase0.Eth1Block]>;
|
||||
};
|
||||
|
||||
export type IEth1StreamParams = Pick<
|
||||
BeaconConfig,
|
||||
"ETH1_FOLLOW_DISTANCE" | "MIN_GENESIS_TIME" | "GENESIS_DELAY" | "SECONDS_PER_ETH1_BLOCK"
|
||||
> & {
|
||||
maxBlocksPerPoll: number;
|
||||
};
|
||||
|
||||
export type IJson = string | number | boolean | undefined | IJson[] | {[key: string]: IJson};
|
||||
|
||||
export interface RpcPayload<P = IJson[]> {
|
||||
method: string;
|
||||
params: P;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export type Eth1Options = {
|
||||
enabled?: boolean;
|
||||
disableEth1DepositDataTracker?: boolean;
|
||||
providerUrls?: string[];
|
||||
/**
|
||||
* jwtSecretHex is the jwt secret if the eth1 modules should ping the jwt auth
|
||||
* protected engine endpoints.
|
||||
*/
|
||||
jwtSecretHex?: string;
|
||||
jwtId?: string;
|
||||
jwtVersion?: string;
|
||||
depositContractDeployBlock?: number;
|
||||
unsafeAllowDepositDataOverwrite?: boolean;
|
||||
/**
|
||||
* Vote for a specific eth1_data regardless of validity and existing votes.
|
||||
* hex encoded ssz serialized Eth1Data type.
|
||||
*/
|
||||
forcedEth1DataVote?: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_PROVIDER_URLS = ["http://localhost:8545"];
|
||||
|
||||
export const defaultEth1Options: Eth1Options = {
|
||||
enabled: true,
|
||||
providerUrls: DEFAULT_PROVIDER_URLS,
|
||||
depositContractDeployBlock: 0,
|
||||
unsafeAllowDepositDataOverwrite: false,
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
import {ChainConfig} from "@lodestar/config";
|
||||
import {Logger} from "@lodestar/logger";
|
||||
import {phase0} from "@lodestar/types";
|
||||
import {
|
||||
FetchError,
|
||||
createElapsedTimeTracker,
|
||||
fromHex,
|
||||
isErrorAborted,
|
||||
isFetchError,
|
||||
toHex,
|
||||
toPrintableUrl,
|
||||
} from "@lodestar/utils";
|
||||
import {HTTP_CONNECTION_ERROR_CODES, HTTP_FATAL_ERROR_CODES} from "../../execution/engine/utils.js";
|
||||
import {isValidAddress} from "../../util/address.js";
|
||||
import {linspace} from "../../util/numpy.js";
|
||||
import {Eth1Block, Eth1ProviderState, EthJsonRpcBlockRaw, IEth1Provider} from "../interface.js";
|
||||
import {DEFAULT_PROVIDER_URLS, Eth1Options} from "../options.js";
|
||||
import {depositEventTopics, parseDepositLog} from "../utils/depositContract.js";
|
||||
import {
|
||||
ErrorJsonRpcResponse,
|
||||
HttpRpcError,
|
||||
JsonRpcHttpClient,
|
||||
JsonRpcHttpClientEvent,
|
||||
JsonRpcHttpClientMetrics,
|
||||
ReqOpts,
|
||||
} from "./jsonRpcHttpClient.js";
|
||||
import {dataToBytes, isJsonRpcTruncatedError, numToQuantity, quantityToNum} from "./utils.js";
|
||||
|
||||
/**
|
||||
* Binds return types to Ethereum JSON RPC methods
|
||||
*/
|
||||
type EthJsonRpcReturnTypes = {
|
||||
eth_getBlockByNumber: EthJsonRpcBlockRaw | null;
|
||||
eth_getBlockByHash: EthJsonRpcBlockRaw | null;
|
||||
eth_blockNumber: string;
|
||||
eth_getCode: string;
|
||||
eth_getLogs: {
|
||||
removed: boolean;
|
||||
logIndex: string;
|
||||
transactionIndex: string;
|
||||
transactionHash: string;
|
||||
blockHash: string;
|
||||
blockNumber: string;
|
||||
address: string;
|
||||
data: string;
|
||||
topics: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
// Define static options once to prevent extra allocations
|
||||
const getBlocksByNumberOpts: ReqOpts = {routeId: "getBlockByNumber_batched"};
|
||||
const getBlockByNumberOpts: ReqOpts = {routeId: "getBlockByNumber"};
|
||||
const getBlockByHashOpts: ReqOpts = {routeId: "getBlockByHash"};
|
||||
const getBlockNumberOpts: ReqOpts = {routeId: "getBlockNumber"};
|
||||
const getLogsOpts: ReqOpts = {routeId: "getLogs"};
|
||||
|
||||
const isOneMinutePassed = createElapsedTimeTracker({minElapsedTime: 60_000});
|
||||
|
||||
export class Eth1Provider implements IEth1Provider {
|
||||
readonly deployBlock: number;
|
||||
private readonly depositContractAddress: string;
|
||||
private readonly rpc: JsonRpcHttpClient;
|
||||
// The default state is ONLINE, it will be updated to offline if we receive a http error
|
||||
private state: Eth1ProviderState = Eth1ProviderState.ONLINE;
|
||||
private logger?: Logger;
|
||||
|
||||
constructor(
|
||||
config: Pick<ChainConfig, "DEPOSIT_CONTRACT_ADDRESS">,
|
||||
opts: Pick<Eth1Options, "depositContractDeployBlock" | "providerUrls" | "jwtSecretHex" | "jwtId" | "jwtVersion"> & {
|
||||
logger?: Logger;
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
metrics?: JsonRpcHttpClientMetrics | null
|
||||
) {
|
||||
this.logger = opts.logger;
|
||||
this.deployBlock = opts.depositContractDeployBlock ?? 0;
|
||||
this.depositContractAddress = toHex(config.DEPOSIT_CONTRACT_ADDRESS);
|
||||
|
||||
const providerUrls = opts.providerUrls ?? DEFAULT_PROVIDER_URLS;
|
||||
this.rpc = new JsonRpcHttpClient(providerUrls, {
|
||||
signal,
|
||||
// Don't fallback with is truncated error. Throw early and let the retry on this class handle it
|
||||
shouldNotFallback: isJsonRpcTruncatedError,
|
||||
jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined,
|
||||
jwtId: opts.jwtId,
|
||||
jwtVersion: opts.jwtVersion,
|
||||
metrics: metrics,
|
||||
});
|
||||
this.logger?.info("Eth1 provider", {urls: providerUrls.map(toPrintableUrl).toString()});
|
||||
|
||||
this.rpc.emitter.on(JsonRpcHttpClientEvent.RESPONSE, () => {
|
||||
const oldState = this.state;
|
||||
this.state = Eth1ProviderState.ONLINE;
|
||||
|
||||
if (oldState !== Eth1ProviderState.ONLINE) {
|
||||
this.logger?.info("Eth1 provider is back online", {oldState, newState: this.state});
|
||||
}
|
||||
});
|
||||
|
||||
this.rpc.emitter.on(JsonRpcHttpClientEvent.ERROR, ({error}) => {
|
||||
if (isErrorAborted(error)) {
|
||||
this.state = Eth1ProviderState.ONLINE;
|
||||
} else if ((error as unknown) instanceof HttpRpcError || (error as unknown) instanceof ErrorJsonRpcResponse) {
|
||||
this.state = Eth1ProviderState.ERROR;
|
||||
} else if (error && isFetchError(error) && HTTP_FATAL_ERROR_CODES.includes((error as FetchError).code)) {
|
||||
this.state = Eth1ProviderState.OFFLINE;
|
||||
} else if (error && isFetchError(error) && HTTP_CONNECTION_ERROR_CODES.includes((error as FetchError).code)) {
|
||||
this.state = Eth1ProviderState.AUTH_FAILED;
|
||||
}
|
||||
|
||||
if (this.state !== Eth1ProviderState.ONLINE && isOneMinutePassed()) {
|
||||
this.logger?.error(
|
||||
"Eth1 provider error",
|
||||
{
|
||||
state: this.state,
|
||||
lastErrorAt: new Date(Date.now() - isOneMinutePassed.msSinceLastCall).toLocaleTimeString(),
|
||||
},
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getState(): Eth1ProviderState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async validateContract(): Promise<void> {
|
||||
if (!isValidAddress(this.depositContractAddress)) {
|
||||
throw Error(`Invalid contract address: ${this.depositContractAddress}`);
|
||||
}
|
||||
|
||||
const code = await this.getCode(this.depositContractAddress);
|
||||
if (!code || code === "0x") {
|
||||
throw new Error(`There is no deposit contract at given address: ${this.depositContractAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getDepositEvents(fromBlock: number, toBlock: number): Promise<phase0.DepositEvent[]> {
|
||||
const logsRawArr = await this.getLogs({
|
||||
fromBlock,
|
||||
toBlock,
|
||||
address: this.depositContractAddress,
|
||||
topics: depositEventTopics,
|
||||
});
|
||||
return logsRawArr.flat(1).map((log) => parseDepositLog(log));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an arbitrary array of block numbers in batch
|
||||
*/
|
||||
async getBlocksByNumber(fromBlock: number, toBlock: number): Promise<EthJsonRpcBlockRaw[]> {
|
||||
const method = "eth_getBlockByNumber";
|
||||
const blocksArr = await this.rpc.fetchBatch<EthJsonRpcReturnTypes[typeof method]>(
|
||||
linspace(fromBlock, toBlock).map((blockNumber) => ({method, params: [numToQuantity(blockNumber), false]})),
|
||||
getBlocksByNumberOpts
|
||||
);
|
||||
const blocks: EthJsonRpcBlockRaw[] = [];
|
||||
for (const block of blocksArr.flat(1)) {
|
||||
if (block) blocks.push(block);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
async getBlockByNumber(blockNumber: number | "latest"): Promise<EthJsonRpcBlockRaw | null> {
|
||||
const method = "eth_getBlockByNumber";
|
||||
const blockNumberHex = typeof blockNumber === "string" ? blockNumber : numToQuantity(blockNumber);
|
||||
return this.rpc.fetch<EthJsonRpcReturnTypes[typeof method]>(
|
||||
// false = include only transaction roots, not full objects
|
||||
{method, params: [blockNumberHex, false]},
|
||||
getBlockByNumberOpts
|
||||
);
|
||||
}
|
||||
|
||||
async getBlockByHash(blockHashHex: string): Promise<EthJsonRpcBlockRaw | null> {
|
||||
const method = "eth_getBlockByHash";
|
||||
return this.rpc.fetch<EthJsonRpcReturnTypes[typeof method]>(
|
||||
// false = include only transaction roots, not full objects
|
||||
{method, params: [blockHashHex, false]},
|
||||
getBlockByHashOpts
|
||||
);
|
||||
}
|
||||
|
||||
async getBlockNumber(): Promise<number> {
|
||||
const method = "eth_blockNumber";
|
||||
const blockNumberRaw = await this.rpc.fetch<EthJsonRpcReturnTypes[typeof method]>(
|
||||
{method, params: []},
|
||||
getBlockNumberOpts
|
||||
);
|
||||
return parseInt(blockNumberRaw, 16);
|
||||
}
|
||||
|
||||
async getCode(address: string): Promise<string> {
|
||||
const method = "eth_getCode";
|
||||
return this.rpc.fetch<EthJsonRpcReturnTypes[typeof method]>({method, params: [address, "latest"]});
|
||||
}
|
||||
|
||||
async getLogs(options: {
|
||||
fromBlock: number;
|
||||
toBlock: number;
|
||||
address: string;
|
||||
topics: string[];
|
||||
}): Promise<{blockNumber: number; data: string; topics: string[]}[]> {
|
||||
const method = "eth_getLogs";
|
||||
const hexOptions = {
|
||||
...options,
|
||||
fromBlock: numToQuantity(options.fromBlock),
|
||||
toBlock: numToQuantity(options.toBlock),
|
||||
};
|
||||
const logsRaw = await this.rpc.fetch<EthJsonRpcReturnTypes[typeof method]>(
|
||||
{method, params: [hexOptions]},
|
||||
getLogsOpts
|
||||
);
|
||||
return logsRaw.map((logRaw) => ({
|
||||
blockNumber: parseInt(logRaw.blockNumber, 16),
|
||||
data: logRaw.data,
|
||||
topics: logRaw.topics,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEth1Block(blockRaw: EthJsonRpcBlockRaw): Eth1Block {
|
||||
if (typeof blockRaw !== "object") throw Error("block is not an object");
|
||||
return {
|
||||
blockHash: dataToBytes(blockRaw.hash, 32),
|
||||
blockNumber: quantityToNum(blockRaw.number, "block.number"),
|
||||
timestamp: quantityToNum(blockRaw.timestamp, "block.timestamp"),
|
||||
};
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {RootHex} from "@lodestar/types";
|
||||
import {bigIntToBytes, bytesToBigInt, fromHex, fromHexInto, toHex} from "@lodestar/utils";
|
||||
import {ErrorParseJson} from "./jsonRpcHttpClient.js";
|
||||
|
||||
/** QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API */
|
||||
export type QUANTITY = string;
|
||||
/** DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API */
|
||||
export type DATA = string;
|
||||
|
||||
export const rootHexRegex = /^0x[a-fA-F0-9]{64}$/;
|
||||
|
||||
export function numberToHex(n: number | bigint): string {
|
||||
return "0x" + n.toString(16);
|
||||
}
|
||||
|
||||
export function isJsonRpcTruncatedError(error: Error): boolean {
|
||||
return (
|
||||
// Truncated responses usually get as 200 but since it's truncated the JSON will be invalid
|
||||
error instanceof ErrorParseJson ||
|
||||
// Otherwise guess Infura error message of too many events
|
||||
(error instanceof Error && error.message.includes("query returned more than 10000 results")) ||
|
||||
// Nethermind enforces limits on JSON RPC batch calls
|
||||
(error instanceof Error && error.message.toLowerCase().includes("batch size limit exceeded"))
|
||||
);
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
// Handle special case in Ethereum hex formating where hex values may include a single letter
|
||||
// 0x0, 0x1 are valid values
|
||||
if (bytes.length === 1 && bytes[0] <= 0xf) {
|
||||
return "0x" + bytes[0].toString(16);
|
||||
}
|
||||
|
||||
return toHex(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*
|
||||
* When encoding QUANTITIES (integers, numbers): encode as hex, prefix with “0x”, the most compact representation (slight exception: zero should be represented as “0x0”). Examples:
|
||||
* - 0x41 (65 in decimal)
|
||||
* - 0x400 (1024 in decimal)
|
||||
* - WRONG: 0x (should always have at least one digit - zero is “0x0”)
|
||||
* - WRONG: 0x0400 (no leading zeroes allowed)
|
||||
* - WRONG: ff (must be prefixed 0x)
|
||||
*/
|
||||
export function numToQuantity(num: number | bigint): QUANTITY {
|
||||
return "0x" + num.toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*/
|
||||
export function quantityToNum(hex: QUANTITY, id = ""): number {
|
||||
const num = parseInt(hex, 16);
|
||||
if (Number.isNaN(num) || num < 0) throw Error(`Invalid hex decimal ${id} '${hex}'`);
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
* Typesafe fn to convert hex string to bigint. The BigInt constructor param is any
|
||||
*/
|
||||
export function quantityToBigint(hex: QUANTITY, id = ""): bigint {
|
||||
try {
|
||||
return BigInt(hex);
|
||||
} catch (e) {
|
||||
throw Error(`Invalid hex bigint ${id} '${hex}': ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
*/
|
||||
export function quantityToBytes(hex: QUANTITY): Uint8Array {
|
||||
const bn = quantityToBigint(hex);
|
||||
return bigIntToBytes(bn, 32, "le");
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
* Compress a 32 ByteVector into a QUANTITY
|
||||
*/
|
||||
export function bytesToQuantity(bytes: Uint8Array): QUANTITY {
|
||||
const bn = bytesToBigInt(bytes, "le");
|
||||
return numToQuantity(bn);
|
||||
}
|
||||
|
||||
/**
|
||||
* DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*
|
||||
* When encoding UNFORMATTED DATA (byte arrays, account addresses, hashes, bytecode arrays): encode as hex, prefix with
|
||||
* “0x”, two hex digits per byte. Examples:
|
||||
*
|
||||
* - 0x41 (size 1, “A”)
|
||||
* - 0x004200 (size 3, “\0B\0”)
|
||||
* - 0x (size 0, “”)
|
||||
* - WRONG: 0xf0f0f (must be even number of digits)
|
||||
* - WRONG: 004200 (must be prefixed 0x)
|
||||
*/
|
||||
export function bytesToData(bytes: Uint8Array): DATA {
|
||||
return toHex(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*/
|
||||
export function dataToBytes(hex: DATA, fixedLength: number | null): Uint8Array {
|
||||
try {
|
||||
const bytes = fromHex(hex);
|
||||
if (fixedLength != null && bytes.length !== fixedLength) {
|
||||
throw Error(`Wrong data length ${bytes.length} expected ${fixedLength}`);
|
||||
}
|
||||
return bytes;
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid hex string: ${(e as Error).message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DATA into a preallocated buffer
|
||||
* fromHexInto will throw if buffer's length is not the same as the decoded hex length
|
||||
*/
|
||||
export function dataIntoBytes(hex: DATA, buffer: Uint8Array): Uint8Array {
|
||||
fromHexInto(hex, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*/
|
||||
export function dataToRootHex(hex: DATA, id = ""): RootHex {
|
||||
if (!rootHexRegex.test(hex)) throw Error(`Invalid hex root ${id} '${hex}'`);
|
||||
return hex;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import {phase0} from "@lodestar/types";
|
||||
import {sleep} from "@lodestar/utils";
|
||||
import {BatchDepositEvents, Eth1Block, IEth1Provider, IEth1StreamParams} from "./interface.js";
|
||||
import {parseEth1Block} from "./provider/eth1Provider.js";
|
||||
import {groupDepositEventsByBlock} from "./utils/groupDepositEventsByBlock.js";
|
||||
import {optimizeNextBlockDiffForGenesis} from "./utils/optimizeNextBlockDiffForGenesis.js";
|
||||
|
||||
/**
|
||||
* Phase 1 of genesis building.
|
||||
* Not enough validators, only stream deposits
|
||||
* @param signal Abort stream returning after a while loop cycle. Aborts internal sleep
|
||||
*/
|
||||
export async function* getDepositsStream(
|
||||
fromBlock: number,
|
||||
provider: IEth1Provider,
|
||||
params: IEth1StreamParams,
|
||||
signal?: AbortSignal
|
||||
): AsyncGenerator<BatchDepositEvents> {
|
||||
fromBlock = Math.max(fromBlock, provider.deployBlock);
|
||||
|
||||
while (true) {
|
||||
const remoteFollowBlock = await getRemoteFollowBlock(provider, params);
|
||||
const toBlock = Math.min(remoteFollowBlock, fromBlock + params.maxBlocksPerPoll);
|
||||
const logs = await provider.getDepositEvents(fromBlock, toBlock);
|
||||
for (const batchedDeposits of groupDepositEventsByBlock(logs)) {
|
||||
yield batchedDeposits;
|
||||
}
|
||||
|
||||
fromBlock = toBlock;
|
||||
|
||||
// If reached head, sleep for an eth1 block. Throws if signal is aborted
|
||||
await sleep(toBlock >= remoteFollowBlock ? params.SECONDS_PER_ETH1_BLOCK * 1000 : 10, signal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2 of genesis building.
|
||||
* There are enough validators, stream deposits and blocks
|
||||
* @param signal Abort stream returning after a while loop cycle. Aborts internal sleep
|
||||
*/
|
||||
export async function* getDepositsAndBlockStreamForGenesis(
|
||||
fromBlock: number,
|
||||
provider: IEth1Provider,
|
||||
params: IEth1StreamParams,
|
||||
signal?: AbortSignal
|
||||
): AsyncGenerator<[phase0.DepositEvent[], Eth1Block]> {
|
||||
fromBlock = Math.max(fromBlock, provider.deployBlock);
|
||||
fromBlock = Math.min(fromBlock, await getRemoteFollowBlock(provider, params));
|
||||
let toBlock = fromBlock; // First, fetch only the first block
|
||||
|
||||
while (true) {
|
||||
const [logs, blockRaw] = await Promise.all([
|
||||
provider.getDepositEvents(fromBlock, toBlock),
|
||||
provider.getBlockByNumber(toBlock),
|
||||
]);
|
||||
|
||||
if (!blockRaw) throw Error(`No block found for number ${toBlock}`);
|
||||
const block = parseEth1Block(blockRaw);
|
||||
|
||||
yield [logs, block];
|
||||
|
||||
const remoteFollowBlock = await getRemoteFollowBlock(provider, params);
|
||||
const nextBlockDiff = optimizeNextBlockDiffForGenesis(block, params);
|
||||
fromBlock = toBlock;
|
||||
toBlock = Math.min(remoteFollowBlock, fromBlock + Math.min(nextBlockDiff, params.maxBlocksPerPoll));
|
||||
|
||||
// If reached head, sleep for an eth1 block. Throws if signal is aborted
|
||||
await sleep(toBlock >= remoteFollowBlock ? params.SECONDS_PER_ETH1_BLOCK * 1000 : 10, signal);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRemoteFollowBlock(provider: IEth1Provider, params: IEth1StreamParams): Promise<number> {
|
||||
const remoteHighestBlock = await provider.getBlockNumber();
|
||||
return Math.max(remoteHighestBlock - params.ETH1_FOLLOW_DISTANCE, 0);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {Interface} from "@ethersproject/abi";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {fromHex} from "@lodestar/utils";
|
||||
|
||||
const depositEventFragment =
|
||||
"event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index)";
|
||||
|
||||
const depositContractInterface = new Interface([depositEventFragment]);
|
||||
|
||||
/**
|
||||
* Precomputed topics of DepositEvent logs
|
||||
*/
|
||||
export const depositEventTopics = [depositContractInterface.getEventTopic("DepositEvent")];
|
||||
|
||||
/**
|
||||
* Parse DepositEvent log
|
||||
*/
|
||||
export function parseDepositLog(log: {blockNumber: number; data: string; topics: string[]}): phase0.DepositEvent {
|
||||
const event = depositContractInterface.parseLog(log);
|
||||
const values = event.args;
|
||||
if (values === undefined) throw Error(`DepositEvent at ${log.blockNumber} has no values`);
|
||||
return {
|
||||
blockNumber: log.blockNumber,
|
||||
index: parseHexNumLittleEndian(values.index),
|
||||
depositData: {
|
||||
pubkey: fromHex(values.pubkey),
|
||||
withdrawalCredentials: fromHex(values.withdrawal_credentials),
|
||||
amount: parseHexNumLittleEndian(values.amount),
|
||||
signature: fromHex(values.signature),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseHexNumLittleEndian(hex: string): number {
|
||||
// Can't use parseInt() because amount is a hex string in little endian
|
||||
return ssz.UintNum64.deserialize(fromHex(hex));
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import {Tree, toGindex} from "@chainsafe/persistent-merkle-tree";
|
||||
import {FilterOptions} from "@lodestar/db";
|
||||
import {CachedBeaconStateAllForks, getEth1DepositCount} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {toRootHex} from "@lodestar/utils";
|
||||
import {DepositTree} from "../../db/repositories/depositDataRoot.js";
|
||||
import {Eth1Error, Eth1ErrorCode} from "../errors.js";
|
||||
|
||||
export type DepositGetter<T> = (indexRange: FilterOptions<number>, eth1Data: phase0.Eth1Data) => Promise<T[]>;
|
||||
|
||||
export async function getDeposits<T>(
|
||||
// eth1_deposit_index represents the next deposit index to be added
|
||||
state: CachedBeaconStateAllForks,
|
||||
eth1Data: phase0.Eth1Data,
|
||||
depositsGetter: DepositGetter<T>
|
||||
): Promise<T[]> {
|
||||
const depositIndex = state.eth1DepositIndex;
|
||||
const depositCount = eth1Data.depositCount;
|
||||
|
||||
if (depositIndex > depositCount) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.DEPOSIT_INDEX_TOO_HIGH, depositIndex, depositCount});
|
||||
}
|
||||
|
||||
const depositsLen = getEth1DepositCount(state, eth1Data);
|
||||
|
||||
if (depositsLen === 0) {
|
||||
return []; // If depositsLen === 0, we can return early since no deposit with be returned from depositsGetter
|
||||
}
|
||||
|
||||
const indexRange = {gte: depositIndex, lt: depositIndex + depositsLen};
|
||||
const deposits = await depositsGetter(indexRange, eth1Data);
|
||||
|
||||
if (deposits.length < depositsLen) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.NOT_ENOUGH_DEPOSITS, len: deposits.length, expectedLen: depositsLen});
|
||||
}
|
||||
|
||||
if (deposits.length > depositsLen) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.TOO_MANY_DEPOSITS, len: deposits.length, expectedLen: depositsLen});
|
||||
}
|
||||
|
||||
return deposits;
|
||||
}
|
||||
|
||||
export function getDepositsWithProofs(
|
||||
depositEvents: phase0.DepositEvent[],
|
||||
depositRootTree: DepositTree,
|
||||
eth1Data: phase0.Eth1Data
|
||||
): phase0.Deposit[] {
|
||||
// Get tree at this particular depositCount to compute correct proofs
|
||||
const viewAtDepositCount = depositRootTree.sliceTo(eth1Data.depositCount - 1);
|
||||
|
||||
const depositRoot = viewAtDepositCount.hashTreeRoot();
|
||||
|
||||
if (!ssz.Root.equals(depositRoot, eth1Data.depositRoot)) {
|
||||
throw new Eth1Error({
|
||||
code: Eth1ErrorCode.WRONG_DEPOSIT_ROOT,
|
||||
root: toRootHex(depositRoot),
|
||||
expectedRoot: toRootHex(eth1Data.depositRoot),
|
||||
});
|
||||
}
|
||||
|
||||
// Already commited for .hashTreeRoot()
|
||||
const treeAtDepositCount = new Tree(viewAtDepositCount.node);
|
||||
const depositTreeDepth = viewAtDepositCount.type.depth;
|
||||
|
||||
return depositEvents.map((log) => ({
|
||||
proof: treeAtDepositCount.getSingleProof(toGindex(depositTreeDepth, BigInt(log.index))),
|
||||
data: log.depositData,
|
||||
}));
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import {Root, phase0} from "@lodestar/types";
|
||||
import {DepositTree} from "../../db/repositories/depositDataRoot.js";
|
||||
import {binarySearchLte} from "../../util/binarySearch.js";
|
||||
import {Eth1Error, Eth1ErrorCode} from "../errors.js";
|
||||
import {Eth1Block} from "../interface.js";
|
||||
|
||||
type BlockNumber = number;
|
||||
|
||||
/**
|
||||
* Appends partial eth1 data (depositRoot, depositCount) in a sequence of blocks
|
||||
* eth1 data deposit is inferred from sparse eth1 data obtained from the deposit logs
|
||||
*/
|
||||
export async function getEth1DataForBlocks(
|
||||
blocks: Eth1Block[],
|
||||
depositDescendingStream: AsyncIterable<phase0.DepositEvent>,
|
||||
depositRootTree: DepositTree,
|
||||
lastProcessedDepositBlockNumber: BlockNumber | null
|
||||
): Promise<(phase0.Eth1Data & Eth1Block)[]> {
|
||||
// Exclude blocks for which there is no valid eth1 data deposit
|
||||
if (lastProcessedDepositBlockNumber !== null) {
|
||||
blocks = blocks.filter((block) => block.blockNumber <= lastProcessedDepositBlockNumber);
|
||||
}
|
||||
|
||||
// A valid block can be constructed using previous `state.eth1Data`, don't throw
|
||||
if (blocks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collect the latest deposit of each blockNumber in a block number range
|
||||
const fromBlock = blocks[0].blockNumber;
|
||||
const toBlock = blocks.at(-1)?.blockNumber as number;
|
||||
const depositsByBlockNumber = await getDepositsByBlockNumber(fromBlock, toBlock, depositDescendingStream);
|
||||
if (depositsByBlockNumber.length === 0) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.NO_DEPOSITS_FOR_BLOCK_RANGE, fromBlock, toBlock});
|
||||
}
|
||||
|
||||
// Precompute a map of depositCount => depositRoot (from depositRootTree)
|
||||
const depositCounts = depositsByBlockNumber.map((event) => event.index + 1);
|
||||
const depositRootByDepositCount = getDepositRootByDepositCount(depositCounts, depositRootTree);
|
||||
|
||||
const eth1Datas: (phase0.Eth1Data & Eth1Block)[] = [];
|
||||
for (const block of blocks) {
|
||||
const deposit = binarySearchLte(depositsByBlockNumber, block.blockNumber, (event) => event.blockNumber);
|
||||
const depositCount = deposit.index + 1;
|
||||
const depositRoot = depositRootByDepositCount.get(depositCount);
|
||||
if (depositRoot === undefined) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.NO_DEPOSIT_ROOT, depositCount});
|
||||
}
|
||||
eth1Datas.push({...block, depositCount, depositRoot});
|
||||
}
|
||||
return eth1Datas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect depositCount by blockNumber from a stream matching a block number range
|
||||
* For a given blockNumber it's depositCount is equal to the index + 1 of the
|
||||
* closest deposit event whose deposit.blockNumber <= blockNumber
|
||||
* @returns array ascending by blockNumber
|
||||
*/
|
||||
export async function getDepositsByBlockNumber(
|
||||
fromBlock: BlockNumber,
|
||||
toBlock: BlockNumber,
|
||||
depositEventDescendingStream: AsyncIterable<phase0.DepositEvent>
|
||||
): Promise<phase0.DepositEvent[]> {
|
||||
const depositCountMap = new Map<BlockNumber, phase0.DepositEvent>();
|
||||
// Take blocks until the block under the range lower bound (included)
|
||||
for await (const deposit of depositEventDescendingStream) {
|
||||
if (deposit.blockNumber <= toBlock && !depositCountMap.has(deposit.blockNumber)) {
|
||||
depositCountMap.set(deposit.blockNumber, deposit);
|
||||
}
|
||||
if (deposit.blockNumber < fromBlock) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(depositCountMap.values()).sort((a, b) => a.blockNumber - b.blockNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Precompute a map of depositCount => depositRoot from a depositRootTree filled beforehand
|
||||
*/
|
||||
export function getDepositRootByDepositCount(depositCounts: number[], depositRootTree: DepositTree): Map<number, Root> {
|
||||
// Unique + sort numerically in descending order
|
||||
depositCounts = [...new Set(depositCounts)].sort((a, b) => b - a);
|
||||
|
||||
if (depositCounts.length > 0) {
|
||||
const maxIndex = depositCounts[0] - 1;
|
||||
const treeLength = depositRootTree.length - 1;
|
||||
if (maxIndex > treeLength) {
|
||||
throw new Eth1Error({code: Eth1ErrorCode.NOT_ENOUGH_DEPOSIT_ROOTS, index: maxIndex, treeLength});
|
||||
}
|
||||
}
|
||||
|
||||
const depositRootByDepositCount = new Map<number, Root>();
|
||||
for (const depositCount of depositCounts) {
|
||||
depositRootTree = depositRootTree.sliceTo(depositCount - 1);
|
||||
depositRootByDepositCount.set(depositCount, depositRootTree.hashTreeRoot());
|
||||
}
|
||||
return depositRootByDepositCount;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Assert that an array of deposits are consecutive and ascending
|
||||
*/
|
||||
export function assertConsecutiveDeposits(depositEvents: {index: number}[]): void {
|
||||
for (let i = 0; i < depositEvents.length - 1; i++) {
|
||||
const indexLeft = depositEvents[i].index;
|
||||
const indexRight = depositEvents[i + 1].index;
|
||||
if (indexLeft !== indexRight - 1) {
|
||||
throw Error(`Non consecutive deposits. deposit[${i}] = ${indexLeft}, deposit[${i + 1}] ${indexRight}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {EPOCHS_PER_ETH1_VOTING_PERIOD, SLOTS_PER_EPOCH, isForkPostElectra} from "@lodestar/params";
|
||||
import {BeaconStateAllForks, BeaconStateElectra, computeTimeAtSlot} from "@lodestar/state-transition";
|
||||
import {RootHex, phase0} from "@lodestar/types";
|
||||
import {toRootHex} from "@lodestar/utils";
|
||||
|
||||
export type Eth1DataGetter = ({
|
||||
timestampRange,
|
||||
}: {
|
||||
timestampRange: {gte: number; lte: number};
|
||||
}) => Promise<phase0.Eth1Data[]>;
|
||||
|
||||
export async function getEth1VotesToConsider(
|
||||
config: ChainForkConfig,
|
||||
state: BeaconStateAllForks,
|
||||
eth1DataGetter: Eth1DataGetter
|
||||
): Promise<phase0.Eth1Data[]> {
|
||||
const fork = config.getForkName(state.slot);
|
||||
if (isForkPostElectra(fork)) {
|
||||
const {eth1DepositIndex, depositRequestsStartIndex} = state as BeaconStateElectra;
|
||||
if (eth1DepositIndex === Number(depositRequestsStartIndex)) {
|
||||
return state.eth1DataVotes.getAllReadonly();
|
||||
}
|
||||
}
|
||||
|
||||
const periodStart = votingPeriodStartTime(config, state);
|
||||
const {SECONDS_PER_ETH1_BLOCK, ETH1_FOLLOW_DISTANCE} = config;
|
||||
|
||||
// Modified version of the spec function to fetch the required range directly from the DB
|
||||
return (
|
||||
await eth1DataGetter({
|
||||
timestampRange: {
|
||||
// Spec v0.12.2
|
||||
// is_candidate_block =
|
||||
// block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= period_start &&
|
||||
// block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2 >= period_start
|
||||
lte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE,
|
||||
gte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2,
|
||||
},
|
||||
})
|
||||
).filter((eth1Data) => eth1Data.depositCount >= state.eth1Data.depositCount);
|
||||
}
|
||||
|
||||
export function pickEth1Vote(state: BeaconStateAllForks, votesToConsider: phase0.Eth1Data[]): phase0.Eth1Data {
|
||||
const votesToConsiderKeys = new Set<string>();
|
||||
for (const eth1Data of votesToConsider) {
|
||||
votesToConsiderKeys.add(getEth1DataKey(eth1Data));
|
||||
}
|
||||
|
||||
const eth1DataHashToEth1Data = new Map<RootHex, phase0.Eth1Data>();
|
||||
const eth1DataVoteCountByRoot = new Map<RootHex, number>();
|
||||
const eth1DataVotesOrder: RootHex[] = [];
|
||||
|
||||
// BeaconStateAllForks is always represented as a tree with a hashing cache.
|
||||
// To check equality its cheaper to use hashTreeRoot as keys.
|
||||
// However `votesToConsider` is an array of values since those are read from DB.
|
||||
// TODO: Optimize cache of known votes, to prevent re-hashing stored values.
|
||||
// Note: for low validator counts it's not very important, since this runs once per proposal
|
||||
const eth1DataVotes = state.eth1DataVotes.getAllReadonly();
|
||||
for (const eth1DataVote of eth1DataVotes) {
|
||||
const rootHex = getEth1DataKey(eth1DataVote);
|
||||
|
||||
if (votesToConsiderKeys.has(rootHex)) {
|
||||
const prevVoteCount = eth1DataVoteCountByRoot.get(rootHex);
|
||||
eth1DataVoteCountByRoot.set(rootHex, 1 + (prevVoteCount ?? 0));
|
||||
|
||||
// Cache eth1DataVote to root Map only once per root
|
||||
if (prevVoteCount === undefined) {
|
||||
eth1DataHashToEth1Data.set(rootHex, eth1DataVote);
|
||||
eth1DataVotesOrder.push(rootHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eth1DataRootsMaxVotes = getKeysWithMaxValue(eth1DataVoteCountByRoot);
|
||||
|
||||
// No votes, vote for the last valid vote
|
||||
if (eth1DataRootsMaxVotes.length === 0) {
|
||||
return votesToConsider.at(-1) ?? state.eth1Data;
|
||||
}
|
||||
|
||||
// If there's a single winning vote with a majority vote that one
|
||||
if (eth1DataRootsMaxVotes.length === 1) {
|
||||
return eth1DataHashToEth1Data.get(eth1DataRootsMaxVotes[0]) ?? state.eth1Data;
|
||||
}
|
||||
|
||||
// If there are multiple winning votes, vote for the latest one
|
||||
const latestMostVotedRoot =
|
||||
eth1DataVotesOrder[Math.max(...eth1DataRootsMaxVotes.map((root) => eth1DataVotesOrder.indexOf(root)))];
|
||||
return eth1DataHashToEth1Data.get(latestMostVotedRoot) ?? state.eth1Data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of keys with max value. May return 0, 1 or more keys
|
||||
*/
|
||||
function getKeysWithMaxValue<T>(map: Map<T, number>): T[] {
|
||||
const entries = Array.from(map.entries());
|
||||
let keysMax: T[] = [];
|
||||
let valueMax = -Infinity;
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
if (value > valueMax) {
|
||||
keysMax = [key];
|
||||
valueMax = value;
|
||||
} else if (value === valueMax) {
|
||||
keysMax.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return keysMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key-ed by fastSerializeEth1Data(). votesToConsider is read from DB as struct and always has a length of 2048.
|
||||
* `state.eth1DataVotes` has a length between 0 and ETH1_FOLLOW_DISTANCE with an equal probability of each value.
|
||||
* So to get the average faster time to key both votesToConsider and state.eth1DataVotes it's better to use
|
||||
* fastSerializeEth1Data(). However, a long term solution is to cache valid votes in memory and prevent having
|
||||
* to recompute their key on every proposal.
|
||||
*
|
||||
* With `fastSerializeEth1Data()`: avg time 20 ms/op
|
||||
* ✓ pickEth1Vote - no votes 233.0587 ops/s 4.290764 ms/op - 121 runs 1.02 s
|
||||
* ✓ pickEth1Vote - max votes 29.21546 ops/s 34.22845 ms/op - 25 runs 1.38 s
|
||||
*
|
||||
* With `toHexString(ssz.phase0.Eth1Data.hashTreeRoot(eth1Data))`: avg time 23 ms/op
|
||||
* ✓ pickEth1Vote - no votes 46.12341 ops/s 21.68096 ms/op - 133 runs 3.40 s
|
||||
* ✓ pickEth1Vote - max votes 37.89912 ops/s 26.38583 ms/op - 29 runs 1.27 s
|
||||
*/
|
||||
function getEth1DataKey(eth1Data: phase0.Eth1Data): string {
|
||||
return fastSerializeEth1Data(eth1Data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize eth1Data types to a unique string ID. It is only used for comparison.
|
||||
*/
|
||||
export function fastSerializeEth1Data(eth1Data: phase0.Eth1Data): string {
|
||||
return toRootHex(eth1Data.blockHash) + eth1Data.depositCount.toString(16) + toRootHex(eth1Data.depositRoot);
|
||||
}
|
||||
|
||||
export function votingPeriodStartTime(config: ChainForkConfig, state: BeaconStateAllForks): number {
|
||||
const eth1VotingPeriodStartSlot = state.slot - (state.slot % (EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH));
|
||||
return computeTimeAtSlot(config, eth1VotingPeriodStartSlot, state.genesisTime);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {phase0} from "@lodestar/types";
|
||||
import {BatchDepositEvents} from "../interface.js";
|
||||
|
||||
/**
|
||||
* Return deposit events of blocks grouped/sorted by block number and deposit index
|
||||
* Blocks without events are omitted
|
||||
* @param depositEvents range deposit events
|
||||
*/
|
||||
export function groupDepositEventsByBlock(depositEvents: phase0.DepositEvent[]): BatchDepositEvents[] {
|
||||
depositEvents.sort((event1, event2) => event1.index - event2.index);
|
||||
const depositsByBlockMap = new Map<number, phase0.DepositEvent[]>();
|
||||
for (const deposit of depositEvents) {
|
||||
depositsByBlockMap.set(deposit.blockNumber, [...(depositsByBlockMap.get(deposit.blockNumber) || []), deposit]);
|
||||
}
|
||||
return Array.from(depositsByBlockMap.entries()).map(([blockNumber, depositEvents]) => ({
|
||||
blockNumber,
|
||||
depositEvents,
|
||||
}));
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {ChainConfig} from "@lodestar/config";
|
||||
|
||||
/**
|
||||
* Utility for fetching genesis min genesis time block
|
||||
* Returns an approximation of the next block diff to fetch to progressively
|
||||
* get closer to the block that satisfies min genesis time condition
|
||||
*/
|
||||
export function optimizeNextBlockDiffForGenesis(
|
||||
lastFetchedBlock: {timestamp: number},
|
||||
params: Pick<ChainConfig, "MIN_GENESIS_TIME" | "GENESIS_DELAY" | "SECONDS_PER_ETH1_BLOCK">
|
||||
): number {
|
||||
const timeToGenesis = params.MIN_GENESIS_TIME - params.GENESIS_DELAY - lastFetchedBlock.timestamp;
|
||||
const numBlocksToGenesis = Math.floor(timeToGenesis / params.SECONDS_PER_ETH1_BLOCK);
|
||||
if (numBlocksToGenesis <= 2) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.floor(numBlocksToGenesis / 2));
|
||||
}
|
||||
@@ -4,14 +4,6 @@ import {BlobsBundle, ExecutionPayload, ExecutionRequests, Root, RootHex, Wei} fr
|
||||
import {BlobAndProof} from "@lodestar/types/deneb";
|
||||
import {BlobAndProofV2} from "@lodestar/types/fulu";
|
||||
import {strip0xPrefix} from "@lodestar/utils";
|
||||
import {
|
||||
ErrorJsonRpcResponse,
|
||||
HttpRpcError,
|
||||
IJsonRpcHttpClient,
|
||||
JsonRpcHttpClientEvent,
|
||||
ReqOpts,
|
||||
} from "../../eth1/provider/jsonRpcHttpClient.js";
|
||||
import {bytesToData, numToQuantity} from "../../eth1/provider/utils.js";
|
||||
import {Metrics} from "../../metrics/index.js";
|
||||
import {EPOCHS_PER_BATCH} from "../../sync/constants.js";
|
||||
import {getLodestarClientVersion} from "../../util/metadata.js";
|
||||
@@ -27,6 +19,13 @@ import {
|
||||
PayloadId,
|
||||
VersionedHashes,
|
||||
} from "./interface.js";
|
||||
import {
|
||||
ErrorJsonRpcResponse,
|
||||
HttpRpcError,
|
||||
IJsonRpcHttpClient,
|
||||
JsonRpcHttpClientEvent,
|
||||
ReqOpts,
|
||||
} from "./jsonRpcHttpClient.js";
|
||||
import {PayloadIdCache} from "./payloadIdCache.js";
|
||||
import {
|
||||
BLOB_AND_PROOF_V2_RPC_BYTES,
|
||||
@@ -45,7 +44,7 @@ import {
|
||||
serializePayloadAttributes,
|
||||
serializeVersionedHashes,
|
||||
} from "./types.js";
|
||||
import {getExecutionEngineState} from "./utils.js";
|
||||
import {bytesToData, getExecutionEngineState, numToQuantity} from "./utils.js";
|
||||
|
||||
export type ExecutionEngineModules = {
|
||||
signal: AbortSignal;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {fromHex, toPrintableUrl} from "@lodestar/utils";
|
||||
import {JsonRpcHttpClient} from "../../eth1/provider/jsonRpcHttpClient.js";
|
||||
import {ExecutionEngineDisabled} from "./disabled.js";
|
||||
import {
|
||||
ExecutionEngineHttp,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
defaultExecutionEngineHttpOpts,
|
||||
} from "./http.js";
|
||||
import {IExecutionEngine} from "./interface.js";
|
||||
import {JsonRpcHttpClient} from "./jsonRpcHttpClient.js";
|
||||
import {ExecutionEngineMockBackend, ExecutionEngineMockOpts} from "./mock.js";
|
||||
import {ExecutionEngineMockJsonRpcClient, JsonRpcBackend} from "./utils.js";
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
import {BlobsBundle, ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, capella} from "@lodestar/types";
|
||||
import {BlobAndProof} from "@lodestar/types/deneb";
|
||||
import {BlobAndProofV2} from "@lodestar/types/fulu";
|
||||
import {DATA} from "../../eth1/provider/utils.js";
|
||||
import {PayloadId, PayloadIdCache, WithdrawalV1} from "./payloadIdCache.js";
|
||||
import {ExecutionPayloadBody} from "./types.js";
|
||||
import {DATA} from "./utils.js";
|
||||
|
||||
export {PayloadIdCache, type PayloadId, type WithdrawalV1};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {EventEmitter} from "node:events";
|
||||
import {StrictEventEmitter} from "strict-event-emitter-types";
|
||||
import {ErrorAborted, Gauge, Histogram, TimeoutError, fetch, isValidHttpUrl, retry} from "@lodestar/utils";
|
||||
import {IJson, RpcPayload} from "../interface.js";
|
||||
import {JwtClaim, encodeJwtToken} from "./jwt.js";
|
||||
import {IJson, RpcPayload} from "./utils.js";
|
||||
|
||||
export enum JsonRpcHttpClientEvent {
|
||||
/**
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import {ExecutionPayload, RootHex, bellatrix, deneb, ssz} from "@lodestar/types";
|
||||
import {fromHex, toRootHex} from "@lodestar/utils";
|
||||
import {ZERO_HASH_HEX} from "../../constants/index.js";
|
||||
import {quantityToNum} from "../../eth1/provider/utils.js";
|
||||
import {INTEROP_BLOCK_HASH} from "../../node/utils/interop/state.js";
|
||||
import {kzgCommitmentToVersionedHash} from "../../util/blobs.js";
|
||||
import {kzg} from "../../util/kzg.js";
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
serializeExecutionPayload,
|
||||
serializeExecutionRequests,
|
||||
} from "./types.js";
|
||||
import {JsonRpcBackend} from "./utils.js";
|
||||
import {JsonRpcBackend, quantityToNum} from "./utils.js";
|
||||
|
||||
const INTEROP_GAS_LIMIT = 30e6;
|
||||
const PRUNE_PAYLOAD_ID_AFTER_MS = 5000;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {SLOTS_PER_EPOCH} from "@lodestar/params";
|
||||
import {pruneSetToMax} from "@lodestar/utils";
|
||||
import {DATA, QUANTITY} from "../../eth1/provider/utils.js";
|
||||
import {PayloadAttributesRpc} from "./types.js";
|
||||
import {DATA, QUANTITY} from "./utils.js";
|
||||
|
||||
// Idealy this only need to be set to the max head reorgs number
|
||||
const MAX_PAYLOAD_IDS = SLOTS_PER_EPOCH;
|
||||
|
||||
@@ -23,6 +23,14 @@ import {
|
||||
} from "@lodestar/types";
|
||||
import {BlobAndProof} from "@lodestar/types/deneb";
|
||||
import {BlobAndProofV2} from "@lodestar/types/fulu";
|
||||
import {
|
||||
ExecutionPayloadStatus,
|
||||
ExecutionRequestType,
|
||||
PayloadAttributes,
|
||||
VersionedHashes,
|
||||
isExecutionRequestType,
|
||||
} from "./interface.js";
|
||||
import {WithdrawalV1} from "./payloadIdCache.js";
|
||||
import {
|
||||
DATA,
|
||||
QUANTITY,
|
||||
@@ -32,15 +40,7 @@ import {
|
||||
numToQuantity,
|
||||
quantityToBigint,
|
||||
quantityToNum,
|
||||
} from "../../eth1/provider/utils.js";
|
||||
import {
|
||||
ExecutionPayloadStatus,
|
||||
ExecutionRequestType,
|
||||
PayloadAttributes,
|
||||
VersionedHashes,
|
||||
isExecutionRequestType,
|
||||
} from "./interface.js";
|
||||
import {WithdrawalV1} from "./payloadIdCache.js";
|
||||
} from "./utils.js";
|
||||
|
||||
export type EngineApiRpcParamTypes = {
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,120 @@
|
||||
import {isErrorAborted, isFetchError} from "@lodestar/utils";
|
||||
import {IJson, RpcPayload} from "../../eth1/interface.js";
|
||||
import {bigIntToBytes, bytesToBigInt, fromHex, fromHexInto, isErrorAborted, isFetchError, toHex} from "@lodestar/utils";
|
||||
import {isQueueErrorAborted} from "../../util/queue/errors.js";
|
||||
import {ExecutionEngineState, ExecutionPayloadStatus} from "./interface.js";
|
||||
import {
|
||||
ErrorJsonRpcResponse,
|
||||
HttpRpcError,
|
||||
IJsonRpcHttpClient,
|
||||
JsonRpcHttpClientEvent,
|
||||
JsonRpcHttpClientEventEmitter,
|
||||
} from "../../eth1/provider/jsonRpcHttpClient.js";
|
||||
import {isQueueErrorAborted} from "../../util/queue/errors.js";
|
||||
import {ExecutionEngineState, ExecutionPayloadStatus} from "./interface.js";
|
||||
} from "./jsonRpcHttpClient.js";
|
||||
|
||||
/** QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API */
|
||||
export type QUANTITY = string;
|
||||
/** DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API */
|
||||
export type DATA = string;
|
||||
|
||||
export const rootHexRegex = /^0x[a-fA-F0-9]{64}$/;
|
||||
|
||||
export type IJson = string | number | boolean | undefined | IJson[] | {[key: string]: IJson};
|
||||
|
||||
export interface RpcPayload<P = IJson[]> {
|
||||
method: string;
|
||||
params: P;
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*
|
||||
* When encoding QUANTITIES (integers, numbers): encode as hex, prefix with “0x”, the most compact representation (slight exception: zero should be represented as “0x0”). Examples:
|
||||
* - 0x41 (65 in decimal)
|
||||
* - 0x400 (1024 in decimal)
|
||||
* - WRONG: 0x (should always have at least one digit - zero is “0x0”)
|
||||
* - WRONG: 0x0400 (no leading zeroes allowed)
|
||||
* - WRONG: ff (must be prefixed 0x)
|
||||
*/
|
||||
export function numToQuantity(num: number | bigint): QUANTITY {
|
||||
return "0x" + num.toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*/
|
||||
export function quantityToNum(hex: QUANTITY, id = ""): number {
|
||||
const num = parseInt(hex, 16);
|
||||
if (Number.isNaN(num) || num < 0) throw Error(`Invalid hex decimal ${id} '${hex}'`);
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
* Typesafe fn to convert hex string to bigint. The BigInt constructor param is any
|
||||
*/
|
||||
export function quantityToBigint(hex: QUANTITY, id = ""): bigint {
|
||||
try {
|
||||
return BigInt(hex);
|
||||
} catch (e) {
|
||||
throw Error(`Invalid hex bigint ${id} '${hex}': ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
*/
|
||||
export function quantityToBytes(hex: QUANTITY): Uint8Array {
|
||||
const bn = quantityToBigint(hex);
|
||||
return bigIntToBytes(bn, 32, "le");
|
||||
}
|
||||
|
||||
/**
|
||||
* QUANTITY as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API.
|
||||
* Compress a 32 ByteVector into a QUANTITY
|
||||
*/
|
||||
export function bytesToQuantity(bytes: Uint8Array): QUANTITY {
|
||||
const bn = bytesToBigInt(bytes, "le");
|
||||
return numToQuantity(bn);
|
||||
}
|
||||
|
||||
/**
|
||||
* DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*
|
||||
* When encoding UNFORMATTED DATA (byte arrays, account addresses, hashes, bytecode arrays): encode as hex, prefix with
|
||||
* “0x”, two hex digits per byte. Examples:
|
||||
*
|
||||
* - 0x41 (size 1, “A”)
|
||||
* - 0x004200 (size 3, “\0B\0”)
|
||||
* - 0x (size 0, “”)
|
||||
* - WRONG: 0xf0f0f (must be even number of digits)
|
||||
* - WRONG: 004200 (must be prefixed 0x)
|
||||
*/
|
||||
export function bytesToData(bytes: Uint8Array): DATA {
|
||||
return toHex(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* DATA as defined in ethereum execution layer JSON RPC https://eth.wiki/json-rpc/API
|
||||
*/
|
||||
export function dataToBytes(hex: DATA, fixedLength: number | null): Uint8Array {
|
||||
try {
|
||||
const bytes = fromHex(hex);
|
||||
if (fixedLength != null && bytes.length !== fixedLength) {
|
||||
throw Error(`Wrong data length ${bytes.length} expected ${fixedLength}`);
|
||||
}
|
||||
return bytes;
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid hex string: ${(e as Error).message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DATA into a preallocated buffer
|
||||
* fromHexInto will throw if buffer's length is not the same as the decoded hex length
|
||||
*/
|
||||
export function dataIntoBytes(hex: DATA, buffer: Uint8Array): Uint8Array {
|
||||
fromHexInto(hex, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export type JsonRpcBackend = {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
export type {RestApiServerMetrics, RestApiServerModules, RestApiServerOpts} from "./api/rest/base.js";
|
||||
export {RestApiServer} from "./api/rest/base.js";
|
||||
export {checkAndPersistAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js";
|
||||
export {checkAndPersistAnchorState, initStateFromDb} from "./chain/index.js";
|
||||
export {DbCPStateDatastore} from "./chain/stateCache/datastore/db.js";
|
||||
export {FileCPStateDatastore} from "./chain/stateCache/datastore/file.js";
|
||||
export {BeaconDb, type IBeaconDb} from "./db/index.js";
|
||||
export {Eth1Provider, type IEth1Provider} from "./eth1/index.js";
|
||||
// Export metrics utilities to de-duplicate validator metrics
|
||||
export {
|
||||
type HttpMetricsServer,
|
||||
|
||||
@@ -1619,98 +1619,6 @@ export function createLodestarMetrics(
|
||||
}),
|
||||
},
|
||||
|
||||
eth1: {
|
||||
depositTrackerIsCaughtup: register.gauge({
|
||||
name: "lodestar_eth1_deposit_tracker_is_caughtup",
|
||||
help: "Eth1 deposit is caught up 0=false 1=true",
|
||||
}),
|
||||
depositTrackerUpdateErrors: register.gauge({
|
||||
name: "lodestar_eth1_deposit_tracker_update_errors_total",
|
||||
help: "Eth1 deposit update loop errors total",
|
||||
}),
|
||||
remoteHighestBlock: register.gauge({
|
||||
name: "lodestar_eth1_remote_highest_block",
|
||||
help: "Eth1 current highest block number",
|
||||
}),
|
||||
depositEventsFetched: register.gauge({
|
||||
name: "lodestar_eth1_deposit_events_fetched_total",
|
||||
help: "Eth1 deposit events fetched total",
|
||||
}),
|
||||
lastProcessedDepositBlockNumber: register.gauge({
|
||||
name: "lodestar_eth1_last_processed_deposit_block_number",
|
||||
help: "Eth1 deposit tracker lastProcessedDepositBlockNumber",
|
||||
}),
|
||||
blocksFetched: register.gauge({
|
||||
name: "lodestar_eth1_blocks_fetched_total",
|
||||
help: "Eth1 blocks fetched total",
|
||||
}),
|
||||
lastFetchedBlockBlockNumber: register.gauge({
|
||||
name: "lodestar_eth1_last_fetched_block_block_number",
|
||||
help: "Eth1 deposit tracker last fetched block's block number",
|
||||
}),
|
||||
lastFetchedBlockTimestamp: register.gauge({
|
||||
name: "lodestar_eth1_last_fetched_block_timestamp",
|
||||
help: "Eth1 deposit tracker last fetched block's timestamp",
|
||||
}),
|
||||
eth1FollowDistanceSecondsConfig: register.gauge({
|
||||
name: "lodestar_eth1_follow_distance_seconds_config",
|
||||
help: "Constant with value = SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE",
|
||||
}),
|
||||
eth1FollowDistanceDynamic: register.gauge({
|
||||
name: "lodestar_eth1_follow_distance_dynamic",
|
||||
help: "Eth1 dynamic follow distance changed by the deposit tracker if blocks are slow",
|
||||
}),
|
||||
eth1GetBlocksBatchSizeDynamic: register.gauge({
|
||||
name: "lodestar_eth1_blocks_batch_size_dynamic",
|
||||
help: "Dynamic batch size to fetch blocks",
|
||||
}),
|
||||
eth1GetLogsBatchSizeDynamic: register.gauge({
|
||||
name: "lodestar_eth1_logs_batch_size_dynamic",
|
||||
help: "Dynamic batch size to fetch deposit logs",
|
||||
}),
|
||||
},
|
||||
|
||||
eth1HttpClient: {
|
||||
requestTime: register.histogram<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_request_time_seconds",
|
||||
help: "eth1 JsonHttpClient - histogram or roundtrip request times",
|
||||
labelNames: ["routeId"],
|
||||
// Provide max resolution on problematic values around 1 second
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 15],
|
||||
}),
|
||||
streamTime: register.histogram<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_stream_time_seconds",
|
||||
help: "eth1 JsonHttpClient - streaming time by routeId",
|
||||
labelNames: ["routeId"],
|
||||
// Provide max resolution on problematic values around 1 second
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 15],
|
||||
}),
|
||||
requestErrors: register.gauge<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_request_errors_total",
|
||||
help: "eth1 JsonHttpClient - total count of request errors",
|
||||
labelNames: ["routeId"],
|
||||
}),
|
||||
retryCount: register.gauge<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_request_retries_total",
|
||||
help: "eth1 JsonHttpClient - total count of request retries",
|
||||
labelNames: ["routeId"],
|
||||
}),
|
||||
requestUsedFallbackUrl: register.gauge<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_request_used_fallback_url_total",
|
||||
help: "eth1 JsonHttpClient - total count of requests on fallback url(s)",
|
||||
labelNames: ["routeId"],
|
||||
}),
|
||||
activeRequests: register.gauge<{routeId: string}>({
|
||||
name: "lodestar_eth1_http_client_active_requests",
|
||||
help: "eth1 JsonHttpClient - current count of active requests",
|
||||
labelNames: ["routeId"],
|
||||
}),
|
||||
configUrlsCount: register.gauge({
|
||||
name: "lodestar_eth1_http_client_config_urls_count",
|
||||
help: "eth1 JsonHttpClient - static config urls count",
|
||||
}),
|
||||
},
|
||||
|
||||
executionEnginerHttpClient: {
|
||||
requestTime: register.histogram<{routeId: string}>({
|
||||
name: "lodestar_execution_engine_http_client_request_time_seconds",
|
||||
|
||||
@@ -13,7 +13,6 @@ import {BeaconRestApiServer, getApi} from "../api/index.js";
|
||||
import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js";
|
||||
import {ValidatorMonitor, createValidatorMonitor} from "../chain/validatorMonitor.js";
|
||||
import {IBeaconDb} from "../db/index.js";
|
||||
import {initializeEth1ForBlockProduction} from "../eth1/index.js";
|
||||
import {initializeExecutionBuilder, initializeExecutionEngine} from "../execution/index.js";
|
||||
import {HttpMetricsServer, Metrics, createMetrics, getHttpMetricsServer} from "../metrics/index.js";
|
||||
import {MonitoringService} from "../monitoring/index.js";
|
||||
@@ -68,7 +67,6 @@ enum LoggerModule {
|
||||
api = "api",
|
||||
backfill = "backfill",
|
||||
chain = "chain",
|
||||
eth1 = "eth1",
|
||||
execution = "execution",
|
||||
metrics = "metrics",
|
||||
monitoring = "monitoring",
|
||||
@@ -220,13 +218,6 @@ export class BeaconNode {
|
||||
validatorMonitor,
|
||||
anchorState,
|
||||
isAnchorStateFinalized,
|
||||
eth1: initializeEth1ForBlockProduction(opts.eth1, {
|
||||
config,
|
||||
db,
|
||||
metrics,
|
||||
logger: logger.child({module: LoggerModule.eth1}),
|
||||
signal,
|
||||
}),
|
||||
executionEngine: initializeExecutionEngine(opts.executionEngine, {
|
||||
metrics,
|
||||
signal,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {ApiOptions, defaultApiOptions} from "../api/options.js";
|
||||
import {ArchiveMode, DEFAULT_ARCHIVE_MODE, IChainOptions, defaultChainOptions} from "../chain/options.js";
|
||||
import {ValidatorMonitorOpts, defaultValidatorMonitorOpts} from "../chain/validatorMonitor.js";
|
||||
import {DatabaseOptions, defaultDbOptions} from "../db/options.js";
|
||||
import {Eth1Options, defaultEth1Options} from "../eth1/options.js";
|
||||
import {
|
||||
ExecutionBuilderOpts,
|
||||
ExecutionEngineOpts,
|
||||
@@ -26,7 +25,6 @@ export interface IBeaconNodeOptions {
|
||||
api: ApiOptions;
|
||||
chain: IChainOptions;
|
||||
db: DatabaseOptions;
|
||||
eth1: Eth1Options;
|
||||
executionEngine: ExecutionEngineOpts;
|
||||
executionBuilder: ExecutionBuilderOpts;
|
||||
metrics: MetricsOptions;
|
||||
@@ -40,7 +38,6 @@ export const defaultOptions: IBeaconNodeOptions = {
|
||||
api: defaultApiOptions,
|
||||
chain: defaultChainOptions,
|
||||
db: defaultDbOptions,
|
||||
eth1: defaultEth1Options,
|
||||
executionEngine: defaultExecutionEngineOpts,
|
||||
executionBuilder: defaultExecutionBuilderOpts,
|
||||
metrics: defaultMetricsOptions,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {digest} from "@chainsafe/as-sha256";
|
||||
import {Tree, toGindex} from "@chainsafe/persistent-merkle-tree";
|
||||
import {ByteVectorType, CompositeViewDU, ListCompositeType} from "@chainsafe/ssz";
|
||||
import {ChainConfig} from "@lodestar/config";
|
||||
import {
|
||||
BLS_WITHDRAWAL_PREFIX,
|
||||
@@ -9,7 +10,8 @@ import {
|
||||
} from "@lodestar/params";
|
||||
import {ZERO_HASH, computeDomain, computeSigningRoot, interopSecretKeys} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {DepositTree} from "../../../db/repositories/depositDataRoot.js";
|
||||
|
||||
export type DepositTree = CompositeViewDU<ListCompositeType<ByteVectorType>>;
|
||||
|
||||
/**
|
||||
* Compute and return deposit data from other validators.
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
initializeBeaconStateFromEth1,
|
||||
} from "@lodestar/state-transition";
|
||||
import {Bytes32, TimeSeconds, phase0, ssz, sszTypesFor} from "@lodestar/types";
|
||||
import {DepositTree} from "../../../db/repositories/depositDataRoot.js";
|
||||
import {DepositTree} from "./deposits.js";
|
||||
|
||||
export const INTEROP_BLOCK_HASH = Buffer.alloc(32, "B");
|
||||
export const INTEROP_TIMESTAMP = Math.pow(2, 40);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {BeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {IBeaconDb} from "../../db/index.js";
|
||||
import {ssz} from "@lodestar/types";
|
||||
import {interopDeposits} from "./interop/deposits.js";
|
||||
import {InteropStateOpts, getInteropState} from "./interop/state.js";
|
||||
|
||||
@@ -12,26 +11,12 @@ export function initDevState(
|
||||
config: ChainForkConfig,
|
||||
validatorCount: number,
|
||||
interopStateOpts: InteropStateOpts
|
||||
): {deposits: phase0.Deposit[]; state: BeaconStateAllForks} {
|
||||
): BeaconStateAllForks {
|
||||
const deposits = interopDeposits(
|
||||
config,
|
||||
ssz.phase0.DepositDataRootList.defaultViewDU(),
|
||||
validatorCount,
|
||||
interopStateOpts
|
||||
);
|
||||
const state = getInteropState(config, interopStateOpts, deposits);
|
||||
return {deposits, state};
|
||||
}
|
||||
|
||||
export async function writeDeposits(db: IBeaconDb, deposits: phase0.Deposit[]): Promise<void> {
|
||||
for (let i = 0; i < deposits.length; i++) {
|
||||
await Promise.all([
|
||||
db.depositEvent.put(i, {
|
||||
blockNumber: i,
|
||||
index: i,
|
||||
depositData: deposits[i].data,
|
||||
}),
|
||||
db.depositDataRoot.put(i, ssz.phase0.DepositData.hashTreeRoot(deposits[i].data)),
|
||||
]);
|
||||
}
|
||||
return getInteropState(config, interopStateOpts, deposits);
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import {afterAll, beforeAll, describe, expect, it} from "vitest";
|
||||
import {fromHexString, toHexString} from "@chainsafe/ssz";
|
||||
import {KeyValue} from "@lodestar/db";
|
||||
import {LevelDbController} from "@lodestar/db/controller/level";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {sleep} from "@lodestar/utils";
|
||||
import {BeaconDb} from "../../../src/db/index.js";
|
||||
import {Eth1ForBlockProduction} from "../../../src/eth1/index.js";
|
||||
import {Eth1Options} from "../../../src/eth1/options.js";
|
||||
import {Eth1Provider} from "../../../src/eth1/provider/eth1Provider.js";
|
||||
import {getGoerliRpcUrl} from "../../testParams.js";
|
||||
import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
|
||||
import {testLogger} from "../../utils/logger.js";
|
||||
import {generateState} from "../../utils/state.js";
|
||||
import {getTestnetConfig, medallaTestnetConfig} from "../../utils/testnet.js";
|
||||
|
||||
const dbLocation = "./.__testdb";
|
||||
|
||||
// First Pyrmont deposits deposit_data_root field
|
||||
const pyrmontDepositsDataRoot = [
|
||||
// https://goerli.etherscan.io/tx/0x342d3551439a13555c62f95d27b2fbabc816e4c23a6e58c28e69af6fae6d0159
|
||||
"0x8976a7deec59f3ebcdcbd67f512fdd07a9a7cab72b63e85bc7a22bb689c2a40c",
|
||||
// https://goerli.etherscan.io/tx/0x6bab2263e1801ae3ffd14a31c08602c17f0e105e8ab849855adbd661d8b87bfd
|
||||
"0x61cef7d8a3f7c590a2dc066ae1c95def5ce769b3e9471fdb34f36f7a7246965e",
|
||||
];
|
||||
|
||||
// https://github.com/ChainSafe/lodestar/issues/5967
|
||||
describe.skip("eth1 / Eth1Provider", () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const config = getTestnetConfig();
|
||||
const logger = testLogger();
|
||||
|
||||
let db: BeaconDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Nuke DB to make sure it's empty
|
||||
await LevelDbController.destroy(dbLocation);
|
||||
|
||||
db = new BeaconDb(config, await LevelDbController.create({name: dbLocation}, {logger}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
controller.abort();
|
||||
await db.close();
|
||||
await LevelDbController.destroy(dbLocation);
|
||||
});
|
||||
|
||||
it("Should fetch real Pyrmont eth1 data for block proposing", async () => {
|
||||
const eth1Options: Eth1Options = {
|
||||
enabled: true,
|
||||
providerUrls: [getGoerliRpcUrl()],
|
||||
depositContractDeployBlock: medallaTestnetConfig.depositBlock,
|
||||
unsafeAllowDepositDataOverwrite: false,
|
||||
};
|
||||
const eth1Provider = new Eth1Provider(config, eth1Options, controller.signal);
|
||||
|
||||
const eth1ForBlockProduction = new Eth1ForBlockProduction(eth1Options, {
|
||||
config,
|
||||
db,
|
||||
metrics: null,
|
||||
logger,
|
||||
signal: controller.signal,
|
||||
eth1Provider,
|
||||
});
|
||||
|
||||
// Resolves when Eth1ForBlockProduction has fetched both blocks and deposits
|
||||
const {eth1Datas, deposits} = await (async function resolveWithEth1DataAndDeposits() {
|
||||
while (true) {
|
||||
const eth1Datas = await db.eth1Data.entries();
|
||||
const deposits = await db.depositEvent.values();
|
||||
if (eth1Datas.length > 0 && deposits.length > 0) {
|
||||
return {eth1Datas, deposits};
|
||||
}
|
||||
await sleep(1000, controller.signal);
|
||||
}
|
||||
})();
|
||||
|
||||
// Generate mock state to query eth1 data for block proposing
|
||||
if (eth1Datas.length === 0) throw Error("No eth1Datas");
|
||||
const {key: maxTimestamp, value: latestEth1Data} = eth1Datas.at(-1) as KeyValue<number, phase0.Eth1DataOrdered>;
|
||||
|
||||
const {SECONDS_PER_ETH1_BLOCK, ETH1_FOLLOW_DISTANCE} = config;
|
||||
// block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= period_start && ...
|
||||
const periodStart = maxTimestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE;
|
||||
|
||||
// Compute correct deposit root tree
|
||||
const depositRootTree = ssz.phase0.DepositDataRootList.toViewDU(
|
||||
pyrmontDepositsDataRoot.map((root) => fromHexString(root))
|
||||
);
|
||||
|
||||
const tbState = generateState(
|
||||
{
|
||||
// Set genesis time and slot so latestEth1Data is considered
|
||||
slot: 0,
|
||||
genesisTime: periodStart,
|
||||
// No deposits processed yet
|
||||
// eth1_deposit_index represents the next deposit index to be added
|
||||
eth1DepositIndex: 0,
|
||||
// Set eth1Data with deposit length to return them
|
||||
eth1Data: {
|
||||
depositCount: deposits.length,
|
||||
depositRoot: depositRootTree.hashTreeRoot(),
|
||||
blockHash: Buffer.alloc(32),
|
||||
},
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
const state = createCachedBeaconStateTest(tbState, config);
|
||||
|
||||
const result = await eth1ForBlockProduction.getEth1DataAndDeposits(state);
|
||||
expect(result.eth1Data).toEqual(latestEth1Data);
|
||||
expect(result.deposits.map((deposit) => toHexString(ssz.phase0.DepositData.hashTreeRoot(deposit.data)))).toEqual(
|
||||
pyrmontDepositsDataRoot
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import {afterEach, beforeEach, describe, expect, it} from "vitest";
|
||||
import {fromHexString} from "@chainsafe/ssz";
|
||||
import {Eth1Block} from "../../../src/eth1/interface.js";
|
||||
import {Eth1Options} from "../../../src/eth1/options.js";
|
||||
import {Eth1Provider, parseEth1Block} from "../../../src/eth1/provider/eth1Provider.js";
|
||||
import {getGoerliRpcUrl} from "../../testParams.js";
|
||||
import {getTestnetConfig, goerliTestnetDepositEvents} from "../../utils/testnet.js";
|
||||
|
||||
// https://github.com/ChainSafe/lodestar/issues/5967
|
||||
describe.skip("eth1 / Eth1Provider", () => {
|
||||
let controller: AbortController;
|
||||
beforeEach(() => {
|
||||
controller = new AbortController();
|
||||
});
|
||||
afterEach(() => controller.abort());
|
||||
|
||||
const config = getTestnetConfig();
|
||||
|
||||
// Compute lazily since getGoerliRpcUrl() throws if GOERLI_RPC_URL is not set
|
||||
function getEth1Provider(): Eth1Provider {
|
||||
const eth1Options: Eth1Options = {
|
||||
enabled: true,
|
||||
providerUrls: [getGoerliRpcUrl()],
|
||||
depositContractDeployBlock: 0,
|
||||
unsafeAllowDepositDataOverwrite: false,
|
||||
};
|
||||
return new Eth1Provider(config, eth1Options, controller.signal);
|
||||
}
|
||||
|
||||
it("Should validate contract", async () => {
|
||||
await getEth1Provider().validateContract();
|
||||
});
|
||||
|
||||
it("Should get latest block number", async () => {
|
||||
const blockNumber = await getEth1Provider().getBlockNumber();
|
||||
expect(blockNumber).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("Should get a specific block by number", async () => {
|
||||
const goerliGenesisBlock: Eth1Block = {
|
||||
blockHash: fromHexString("0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a"),
|
||||
blockNumber: 0,
|
||||
timestamp: 1548854791,
|
||||
};
|
||||
const block = await getEth1Provider().getBlockByNumber(goerliGenesisBlock.blockNumber);
|
||||
expect(block && parseEth1Block(block)).toEqual(goerliGenesisBlock);
|
||||
});
|
||||
|
||||
it("Should get deposits events for a block range", async () => {
|
||||
const blockNumbers = goerliTestnetDepositEvents.map((log) => log.blockNumber);
|
||||
const fromBlock = Math.min(...blockNumbers);
|
||||
const toBlock = Math.min(...blockNumbers);
|
||||
const depositEvents = await getEth1Provider().getDepositEvents(fromBlock, toBlock);
|
||||
expect(depositEvents).toEqual(goerliTestnetDepositEvents);
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
const firstGoerliBlocks: Eth1Block[] = [
|
||||
[0, 1548854791, "0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a"],
|
||||
[1, 1548947453, "0x8f5bab218b6bb34476f51ca588e9f4553a3a7ce5e13a66c660a5283e97e9a85a"],
|
||||
[2, 1548947468, "0xe675f1362d82cdd1ec260b16fb046c17f61d8a84808150f5d715ccce775f575e"],
|
||||
[3, 1548947483, "0xd5daa825732729bb0d2fd187a1b888e6bfc890f1fc5333984740d9052afb2920"],
|
||||
[4, 1548947498, "0xfe43c87178f0f87c2be161389aa2d35f3065d330bb596a6d9e01529706bf040d"],
|
||||
].map(([number, timestamp, hash]) => ({
|
||||
blockHash: fromHexString(hash as string),
|
||||
blockNumber: number as number,
|
||||
timestamp: timestamp as number,
|
||||
}));
|
||||
|
||||
const goerliSampleContract = {
|
||||
address: "0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC",
|
||||
code: "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a",
|
||||
};
|
||||
|
||||
it("getBlocksByNumber: Should fetch a block range", async () => {
|
||||
const fromBlock = firstGoerliBlocks[0].blockNumber;
|
||||
const toBlock = firstGoerliBlocks.at(-1)?.blockNumber as number;
|
||||
const blocks = await getEth1Provider().getBlocksByNumber(fromBlock, toBlock);
|
||||
expect(blocks.map(parseEth1Block)).toEqual(firstGoerliBlocks);
|
||||
});
|
||||
|
||||
it("getBlockByNumber: Should fetch a single block", async () => {
|
||||
const firstGoerliBlock = firstGoerliBlocks[0];
|
||||
const block = await getEth1Provider().getBlockByNumber(firstGoerliBlock.blockNumber);
|
||||
expect(block && parseEth1Block(block)).toEqual(firstGoerliBlock);
|
||||
});
|
||||
|
||||
it("getBlockNumber: Should fetch latest block number", async () => {
|
||||
const blockNumber = await getEth1Provider().getBlockNumber();
|
||||
expect(blockNumber).toBeInstanceOf(Number);
|
||||
expect(blockNumber).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("getCode: Should fetch code for a contract", async () => {
|
||||
const code = await getEth1Provider().getCode(goerliSampleContract.address);
|
||||
expect(code).toEqual(expect.arrayContaining([goerliSampleContract.code]));
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import {afterEach, beforeEach, describe, expect, it} from "vitest";
|
||||
import {Eth1Options} from "../../../src/eth1/options.js";
|
||||
import {Eth1Provider} from "../../../src/eth1/provider/eth1Provider.js";
|
||||
import {getDepositsAndBlockStreamForGenesis, getDepositsStream} from "../../../src/eth1/stream.js";
|
||||
import {getGoerliRpcUrl} from "../../testParams.js";
|
||||
import {getTestnetConfig, medallaTestnetConfig} from "../../utils/testnet.js";
|
||||
|
||||
// https://github.com/ChainSafe/lodestar/issues/5967
|
||||
describe.skip("Eth1 streams", () => {
|
||||
let controller: AbortController;
|
||||
beforeEach(() => {
|
||||
controller = new AbortController();
|
||||
});
|
||||
afterEach(() => controller.abort());
|
||||
|
||||
const config = getTestnetConfig();
|
||||
|
||||
// Compute lazily since getGoerliRpcUrl() throws if GOERLI_RPC_URL is not set
|
||||
function getEth1Provider(): Eth1Provider {
|
||||
const eth1Options: Eth1Options = {
|
||||
enabled: true,
|
||||
providerUrls: [getGoerliRpcUrl()],
|
||||
depositContractDeployBlock: 0,
|
||||
unsafeAllowDepositDataOverwrite: false,
|
||||
};
|
||||
return new Eth1Provider(config, eth1Options, controller.signal);
|
||||
}
|
||||
|
||||
const maxBlocksPerPoll = 1000;
|
||||
const depositsToFetch = 1000;
|
||||
const eth1Params = {...config, maxBlocksPerPoll};
|
||||
|
||||
it(`Should fetch ${depositsToFetch} deposits with getDepositsStream`, async () => {
|
||||
const depositsStream = getDepositsStream(
|
||||
medallaTestnetConfig.blockWithDepositActivity,
|
||||
getEth1Provider(),
|
||||
eth1Params,
|
||||
controller.signal
|
||||
);
|
||||
|
||||
let depositCount = 0;
|
||||
for await (const {depositEvents} of depositsStream) {
|
||||
depositCount += depositEvents.length;
|
||||
if (depositCount > depositsToFetch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(depositCount).toBeGreaterThan(depositsToFetch);
|
||||
});
|
||||
|
||||
it(`Should fetch ${depositsToFetch} deposits with getDepositsAndBlockStreamForGenesis`, async () => {
|
||||
const stream = getDepositsAndBlockStreamForGenesis(
|
||||
medallaTestnetConfig.blockWithDepositActivity,
|
||||
getEth1Provider(),
|
||||
eth1Params,
|
||||
controller.signal
|
||||
);
|
||||
|
||||
let depositCount = 0;
|
||||
for await (const [deposit] of stream) {
|
||||
depositCount += deposit.length;
|
||||
if (depositCount > depositsToFetch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(depositCount).toBeGreaterThan(depositsToFetch);
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,11 @@ import crypto from "node:crypto";
|
||||
import http from "node:http";
|
||||
import {afterEach, describe, expect, it, vi} from "vitest";
|
||||
import {FetchError, sleep} from "@lodestar/utils";
|
||||
import {RpcPayload} from "../../../src/eth1/interface.js";
|
||||
import {JsonRpcHttpClient} from "../../../src/eth1/provider/jsonRpcHttpClient.js";
|
||||
import {getGoerliRpcUrl} from "../../testParams.js";
|
||||
import {JsonRpcHttpClient} from "../../../../src/execution/engine/jsonRpcHttpClient.js";
|
||||
import {RpcPayload} from "../../../../src/execution/engine/utils.js";
|
||||
import {getGoerliRpcUrl} from "../../../testParams.js";
|
||||
|
||||
describe("eth1 / jsonRpcHttpClient", () => {
|
||||
describe("execution / engine / jsonRpcHttpClient", () => {
|
||||
vi.setConfig({testTimeout: 10_000});
|
||||
|
||||
const port = 36421;
|
||||
@@ -145,10 +145,10 @@ describe("eth1 / jsonRpcHttpClient", () => {
|
||||
|
||||
const controller = new AbortController();
|
||||
if (abort) setTimeout(() => controller.abort(), 50);
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
|
||||
try {
|
||||
await eth1JsonRpcClient.fetch(payload, {timeout});
|
||||
await jsonRpcClient.fetch(payload, {timeout});
|
||||
} catch (error) {
|
||||
if (testCase.errorCode) {
|
||||
expect((error as FetchError).code).toBe(testCase.errorCode);
|
||||
@@ -161,7 +161,7 @@ describe("eth1 / jsonRpcHttpClient", () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
describe("execution / engine / jsonRpcHttpClient - with retries", () => {
|
||||
vi.setConfig({testTimeout: 10_000});
|
||||
|
||||
const port = 36421;
|
||||
@@ -186,9 +186,9 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
const retries = 2;
|
||||
|
||||
const controller = new AbortController();
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(
|
||||
eth1JsonRpcClient.fetchWithRetries(payload, {
|
||||
jsonRpcClient.fetchWithRetries(payload, {
|
||||
retries,
|
||||
shouldRetry: () => {
|
||||
// using the shouldRetry function to keep tab of the retried requests
|
||||
@@ -208,9 +208,9 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
const retries = 2;
|
||||
|
||||
const controller = new AbortController();
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(
|
||||
eth1JsonRpcClient.fetchWithRetries(payload, {
|
||||
jsonRpcClient.fetchWithRetries(payload, {
|
||||
retries,
|
||||
shouldRetry: () => {
|
||||
// using the shouldRetry function to keep tab of the retried requests
|
||||
@@ -247,8 +247,8 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
const retries = 2;
|
||||
|
||||
const controller = new AbortController();
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retries})).rejects.toThrow("Not Found");
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(jsonRpcClient.fetchWithRetries(payload, {retries})).rejects.toThrow("Not Found");
|
||||
expect(requestCount).toBeWithMessage(retries + 1, "404 responses should be retried before failing");
|
||||
});
|
||||
|
||||
@@ -278,8 +278,8 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
const timeout = 200;
|
||||
|
||||
const controller = new AbortController();
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retries, timeout})).rejects.toThrow("Timeout request");
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(jsonRpcClient.fetchWithRetries(payload, {retries, timeout})).rejects.toThrow("Timeout request");
|
||||
expect(requestCount).toBeWithMessage(retries + 1, "Timeout request should be retried before failing");
|
||||
});
|
||||
|
||||
@@ -308,8 +308,8 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 50);
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retries, timeout})).rejects.toThrow("Aborted");
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(jsonRpcClient.fetchWithRetries(payload, {retries, timeout})).rejects.toThrow("Aborted");
|
||||
expect(requestCount).toBeWithMessage(1, "Aborted request should not be retried");
|
||||
});
|
||||
|
||||
@@ -338,8 +338,8 @@ describe("eth1 / jsonRpcHttpClient - with retries", () => {
|
||||
const retries = 2;
|
||||
|
||||
const controller = new AbortController();
|
||||
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retries})).rejects.toThrow("Method not found");
|
||||
const jsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
|
||||
await expect(jsonRpcClient.fetchWithRetries(payload, {retries})).rejects.toThrow("Method not found");
|
||||
expect(requestCount).toBeWithMessage(1, "Payload error (non-network error) should not be retried");
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe("interop / initDevState", () => {
|
||||
|
||||
it("Create correct genesisState", () => {
|
||||
const validatorCount = 8;
|
||||
const {state} = initDevState(config, validatorCount, {
|
||||
const state = initDevState(config, validatorCount, {
|
||||
genesisTime: 1644000000,
|
||||
eth1BlockHash: Buffer.alloc(32, 0xaa),
|
||||
eth1Timestamp: 1644000000,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {AggregatedAttestationPool, OpPool, SyncContributionAndProofPool} from ".
|
||||
import {QueuedStateRegenerator} from "../../src/chain/regen/index.js";
|
||||
import {SeenBlockInput} from "../../src/chain/seenCache/seenGossipBlockInput.js";
|
||||
import {ShufflingCache} from "../../src/chain/shufflingCache.js";
|
||||
import {Eth1ForBlockProduction} from "../../src/eth1/index.js";
|
||||
import {ExecutionBuilderHttp} from "../../src/execution/builder/http.js";
|
||||
import {ExecutionEngineHttp} from "../../src/execution/engine/index.js";
|
||||
import {Clock} from "../../src/util/clock.js";
|
||||
@@ -24,7 +23,6 @@ export type MockedBeaconChain = Mocked<BeaconChain> & {
|
||||
forkChoice: MockedForkChoice;
|
||||
executionEngine: Mocked<ExecutionEngineHttp>;
|
||||
executionBuilder: Mocked<ExecutionBuilderHttp>;
|
||||
eth1: Mocked<Eth1ForBlockProduction>;
|
||||
opPool: Mocked<OpPool>;
|
||||
aggregatedAttestationPool: Mocked<AggregatedAttestationPool>;
|
||||
syncContributionAndProofPool: Mocked<SyncContributionAndProofPool>;
|
||||
@@ -73,7 +71,6 @@ vi.mock("@lodestar/fork-choice", async (importActual) => {
|
||||
});
|
||||
|
||||
vi.mock("../../src/chain/regen/index.js");
|
||||
vi.mock("../../src/eth1/index.js");
|
||||
vi.mock("../../src/chain/beaconProposerCache.js");
|
||||
vi.mock("../../src/chain/seenCache/seenGossipBlockInput.js");
|
||||
vi.mock("../../src/chain/shufflingCache.js");
|
||||
@@ -136,8 +133,6 @@ vi.mock("../../src/chain/chain.js", async (importActual) => {
|
||||
getClientVersion: vi.fn(),
|
||||
},
|
||||
executionBuilder: {},
|
||||
// @ts-expect-error
|
||||
eth1: new Eth1ForBlockProduction(),
|
||||
opPool: new OpPool(),
|
||||
aggregatedAttestationPool: new AggregatedAttestationPool(config),
|
||||
syncContributionAndProofPool: new SyncContributionAndProofPool(config, clock),
|
||||
|
||||
@@ -10,9 +10,6 @@ import {
|
||||
BlockRepository,
|
||||
DataColumnSidecarArchiveRepository,
|
||||
DataColumnSidecarRepository,
|
||||
DepositDataRootRepository,
|
||||
DepositEventRepository,
|
||||
Eth1DataRepository,
|
||||
ProposerSlashingRepository,
|
||||
StateArchiveRepository,
|
||||
VoluntaryExitRepository,
|
||||
@@ -34,10 +31,6 @@ export type MockedBeaconDb = Mocked<BeaconDb> & {
|
||||
blsToExecutionChange: Mocked<BLSToExecutionChangeRepository>;
|
||||
proposerSlashing: Mocked<ProposerSlashingRepository>;
|
||||
attesterSlashing: Mocked<AttesterSlashingRepository>;
|
||||
depositEvent: Mocked<DepositEventRepository>;
|
||||
|
||||
depositDataRoot: Mocked<DepositDataRootRepository>;
|
||||
eth1Data: Mocked<Eth1DataRepository>;
|
||||
};
|
||||
|
||||
vi.mock("../../src/db/repositories/index.js");
|
||||
@@ -55,10 +48,6 @@ vi.mock("../../src/db/index.js", async (importActual) => {
|
||||
blsToExecutionChange: vi.mocked(new BLSToExecutionChangeRepository({} as any, {} as any)),
|
||||
proposerSlashing: vi.mocked(new ProposerSlashingRepository({} as any, {} as any)),
|
||||
attesterSlashing: vi.mocked(new AttesterSlashingRepository({} as any, {} as any)),
|
||||
depositEvent: vi.mocked(new DepositEventRepository({} as any, {} as any)),
|
||||
|
||||
depositDataRoot: vi.mocked(new DepositDataRootRepository({} as any, {} as any)),
|
||||
eth1Data: vi.mocked(new Eth1DataRepository({} as any, {} as any)),
|
||||
|
||||
blobSidecars: vi.mocked(new BlobSidecarsRepository({} as any, {} as any)),
|
||||
blobSidecarsArchive: vi.mocked(new BlobSidecarsArchiveRepository({} as any, {} as any)),
|
||||
|
||||
@@ -8,7 +8,6 @@ import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator";
|
||||
import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js";
|
||||
import {BeaconChain} from "../../../../src/chain/index.js";
|
||||
import {BlockType, produceBlockBody} from "../../../../src/chain/produceBlock/produceBlockBody.js";
|
||||
import {Eth1ForBlockProductionDisabled} from "../../../../src/eth1/index.js";
|
||||
import {ExecutionEngineDisabled} from "../../../../src/execution/engine/index.js";
|
||||
import {ArchiveMode, BeaconDb} from "../../../../src/index.js";
|
||||
import {testLogger} from "../../../utils/logger.js";
|
||||
@@ -49,7 +48,6 @@ describe("produceBlockBody", () => {
|
||||
validatorMonitor: null,
|
||||
anchorState: state,
|
||||
isAnchorStateFinalized: true,
|
||||
eth1: new Eth1ForBlockProductionDisabled(),
|
||||
executionEngine: new ExecutionEngineDisabled(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {BlockInputPreData} from "../../../src/chain/blocks/blockInput/blockInput
|
||||
import {BlockInputSource} from "../../../src/chain/blocks/blockInput/types.js";
|
||||
import {AttestationImportOpt} from "../../../src/chain/blocks/types.js";
|
||||
import {BeaconChain} from "../../../src/chain/index.js";
|
||||
import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js";
|
||||
import {ExecutionEngineDisabled} from "../../../src/execution/engine/index.js";
|
||||
import {ArchiveMode, BeaconDb} from "../../../src/index.js";
|
||||
import {linspace} from "../../../src/util/numpy.js";
|
||||
@@ -101,7 +100,6 @@ describe.skip("verify+import blocks - range sync perf test", () => {
|
||||
validatorMonitor: null,
|
||||
anchorState: state,
|
||||
isAnchorStateFinalized: true,
|
||||
eth1: new Eth1ForBlockProductionDisabled(),
|
||||
executionEngine: new ExecutionEngineDisabled(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import {bench, describe, setBenchOpts} from "@chainsafe/benchmark";
|
||||
import {ContainerType, ListCompositeType} from "@chainsafe/ssz";
|
||||
import {BeaconStateAllForks, newFilledArray} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {fastSerializeEth1Data, pickEth1Vote} from "../../../src/eth1/utils/eth1Vote.js";
|
||||
|
||||
describe("eth1 / pickEth1Vote", () => {
|
||||
const ETH1_FOLLOW_DISTANCE_MAINNET = 2048;
|
||||
const EPOCHS_PER_ETH1_VOTING_PERIOD_MAINNET = 64;
|
||||
const SLOTS_PER_EPOCH_MAINNET = 32;
|
||||
const eth1DataVotesLimit = EPOCHS_PER_ETH1_VOTING_PERIOD_MAINNET * SLOTS_PER_EPOCH_MAINNET;
|
||||
|
||||
const stateMainnetType = new ContainerType({
|
||||
eth1DataVotes: new ListCompositeType(ssz.phase0.Eth1Data, eth1DataVotesLimit),
|
||||
});
|
||||
|
||||
const stateNoVotes = stateMainnetType.defaultViewDU();
|
||||
const stateMaxVotes = stateMainnetType.defaultViewDU();
|
||||
|
||||
// Must convert all instances to create a cache
|
||||
stateMaxVotes.eth1DataVotes = ssz.phase0.Eth1DataVotes.toViewDU(
|
||||
newFilledArray(eth1DataVotesLimit, {
|
||||
depositRoot: Buffer.alloc(32, 0xdd),
|
||||
// All votes are the same
|
||||
depositCount: 1e6,
|
||||
blockHash: Buffer.alloc(32, 0xdd),
|
||||
})
|
||||
);
|
||||
stateMaxVotes.commit();
|
||||
|
||||
// votesToConsider range:
|
||||
// lte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE,
|
||||
// gte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2,
|
||||
const votesToConsider = Array.from({length: ETH1_FOLLOW_DISTANCE_MAINNET}, (_, i) => ({
|
||||
depositRoot: Buffer.alloc(32, 0xdd),
|
||||
// Each eth1Data is different
|
||||
depositCount: 1e6 + i,
|
||||
blockHash: Buffer.alloc(32, 0xdd),
|
||||
}));
|
||||
|
||||
bench("pickEth1Vote - no votes", () => {
|
||||
pickEth1Vote(stateNoVotes as unknown as BeaconStateAllForks, votesToConsider);
|
||||
});
|
||||
|
||||
bench("pickEth1Vote - max votes", () => {
|
||||
pickEth1Vote(stateMaxVotes as unknown as BeaconStateAllForks, votesToConsider);
|
||||
});
|
||||
});
|
||||
|
||||
// Results in Linux Feb 2022
|
||||
//
|
||||
// eth1 / pickEth1Vote serializers
|
||||
// ✓ pickEth1Vote - Eth1Data hashTreeRoot value x2048 58.45559 ops/s 17.10700 ms/op - 45 runs 1.27 s
|
||||
// ✓ pickEth1Vote - Eth1Data hashTreeRoot tree x2048 122.1150 ops/s 8.189003 ms/op - 65 runs 1.75 s
|
||||
// ✓ pickEth1Vote - Eth1Data fastSerialize value x2048 533.9807 ops/s 1.872727 ms/op - 272 runs 1.01 s
|
||||
// ✓ pickEth1Vote - Eth1Data fastSerialize tree x2048 59.49406 ops/s 16.80840 ms/op - 60 runs 1.51 s
|
||||
|
||||
describe("eth1 / pickEth1Vote serializers", () => {
|
||||
setBenchOpts({noThreshold: true});
|
||||
|
||||
const ETH1_FOLLOW_DISTANCE_MAINNET = 2048;
|
||||
const eth1DataValue: phase0.Eth1Data = {
|
||||
depositRoot: Buffer.alloc(32, 0xdd),
|
||||
depositCount: 1e6,
|
||||
blockHash: Buffer.alloc(32, 0xdd),
|
||||
};
|
||||
const eth1DataTree = ssz.phase0.Eth1Data.toViewDU(eth1DataValue);
|
||||
|
||||
bench(`pickEth1Vote - Eth1Data hashTreeRoot value x${ETH1_FOLLOW_DISTANCE_MAINNET}`, () => {
|
||||
for (let i = 0; i < ETH1_FOLLOW_DISTANCE_MAINNET; i++) {
|
||||
ssz.phase0.Eth1Data.hashTreeRoot(eth1DataValue);
|
||||
}
|
||||
});
|
||||
|
||||
// Create new copies of eth1DataTree to drop the hashing cache
|
||||
bench({
|
||||
id: `pickEth1Vote - Eth1Data hashTreeRoot tree x${ETH1_FOLLOW_DISTANCE_MAINNET}`,
|
||||
beforeEach: () =>
|
||||
Array.from({length: ETH1_FOLLOW_DISTANCE_MAINNET}, () => ssz.phase0.Eth1Data.toViewDU(eth1DataValue)),
|
||||
fn: (eth1DataTrees) => {
|
||||
for (let i = 0; i < eth1DataTrees.length; i++) {
|
||||
ssz.phase0.Eth1Data.hashTreeRoot(eth1DataTrees[i]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
bench(`pickEth1Vote - Eth1Data fastSerialize value x${ETH1_FOLLOW_DISTANCE_MAINNET}`, () => {
|
||||
for (let i = 0; i < ETH1_FOLLOW_DISTANCE_MAINNET; i++) {
|
||||
fastSerializeEth1Data(eth1DataValue);
|
||||
}
|
||||
});
|
||||
|
||||
bench(`pickEth1Vote - Eth1Data fastSerialize tree x${ETH1_FOLLOW_DISTANCE_MAINNET}`, () => {
|
||||
for (let i = 0; i < ETH1_FOLLOW_DISTANCE_MAINNET; i++) {
|
||||
fastSerializeEth1Data(eth1DataTree);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,11 +8,10 @@ import {CachedBeaconStateElectra} from "@lodestar/state-transition";
|
||||
import {Epoch, Slot, electra} from "@lodestar/types";
|
||||
import {LogLevel, sleep} from "@lodestar/utils";
|
||||
import {ValidatorProposerConfig} from "@lodestar/validator";
|
||||
import {bytesToData} from "../../lib/eth1/provider/utils.js";
|
||||
import {BeaconRestApiServerOpts} from "../../src/api/index.js";
|
||||
import {dataToBytes} from "../../src/eth1/provider/utils.js";
|
||||
import {defaultExecutionEngineHttpOpts} from "../../src/execution/engine/http.js";
|
||||
import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js";
|
||||
import {bytesToData, dataToBytes} from "../../src/execution/engine/utils.js";
|
||||
import {initializeExecutionEngine} from "../../src/execution/index.js";
|
||||
import {BeaconNode} from "../../src/index.js";
|
||||
import {ClockEvent} from "../../src/util/clock.js";
|
||||
@@ -311,8 +310,6 @@ describe("executionEngine / ExecutionEngineHttp", () => {
|
||||
api: {rest: {enabled: true} as BeaconRestApiServerOpts},
|
||||
sync: {isSingleNode: true},
|
||||
network: {allowPublishToZeroPeers: true, discv5: null},
|
||||
// Now eth deposit/merge tracker methods directly available on engine endpoints
|
||||
eth1: {enabled: false, providerUrls: [engineRpcUrl], jwtSecretHex},
|
||||
executionEngine: {urls: [engineRpcUrl], jwtSecretHex},
|
||||
chain: {suggestedFeeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
|
||||
},
|
||||
|
||||
@@ -38,7 +38,6 @@ import {BeaconChain, ChainEvent} from "../../../src/chain/index.js";
|
||||
import {defaultChainOptions} from "../../../src/chain/options.js";
|
||||
import {validateBlockDataColumnSidecars} from "../../../src/chain/validation/dataColumnSidecar.js";
|
||||
import {ZERO_HASH_HEX} from "../../../src/constants/constants.js";
|
||||
import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js";
|
||||
import {ExecutionPayloadStatus} from "../../../src/execution/engine/interface.js";
|
||||
import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.js";
|
||||
import {getExecutionEngineFromBackend} from "../../../src/execution/index.js";
|
||||
@@ -77,7 +76,6 @@ const forkChoiceTest =
|
||||
/** This is to track test's tickTime to be used in proposer boost */
|
||||
let tickTime = 0;
|
||||
const clock = new ClockStopped(currentSlot);
|
||||
const eth1 = new Eth1ForBlockProductionDisabled();
|
||||
const executionEngineBackend = new ExecutionEngineMockBackend({
|
||||
onlyPredefinedResponses: opts.onlyPredefinedResponses,
|
||||
genesisBlockHash: isExecutionStateType(anchorState)
|
||||
@@ -124,7 +122,6 @@ const forkChoiceTest =
|
||||
validatorMonitor: null,
|
||||
anchorState,
|
||||
isAnchorStateFinalized: true,
|
||||
eth1,
|
||||
executionEngine,
|
||||
executionBuilder: undefined,
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {PublicKey, SecretKey} from "@chainsafe/blst";
|
||||
import {toHexString} from "@chainsafe/ssz";
|
||||
import {config} from "@lodestar/config/default";
|
||||
import {DOMAIN_DEPOSIT, MAX_EFFECTIVE_BALANCE} from "@lodestar/params";
|
||||
import {ZERO_HASH, computeDomain, computeSigningRoot, interopSecretKey} from "@lodestar/state-transition";
|
||||
import {ValidatorIndex, phase0, ssz} from "@lodestar/types";
|
||||
import {ErrorAborted} from "@lodestar/utils";
|
||||
import {GenesisBuilder} from "../../../../src/chain/genesis/genesis.js";
|
||||
import {ZERO_HASH_HEX} from "../../../../src/constants/index.js";
|
||||
import {Eth1ProviderState, EthJsonRpcBlockRaw, IEth1Provider} from "../../../../src/eth1/interface.js";
|
||||
import {testLogger} from "../../../utils/logger.js";
|
||||
|
||||
describe("genesis builder", () => {
|
||||
const logger = testLogger();
|
||||
const schlesiConfig = Object.assign({}, config, {
|
||||
MIN_GENESIS_TIME: 1587755000,
|
||||
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 4,
|
||||
MIN_GENESIS_DELAY: 3600,
|
||||
});
|
||||
|
||||
type MockData = {events: phase0.DepositEvent[]; blocks: EthJsonRpcBlockRaw[]};
|
||||
|
||||
function generateGenesisBuilderMockData(): MockData {
|
||||
const events: phase0.DepositEvent[] = [];
|
||||
const blocks: EthJsonRpcBlockRaw[] = [];
|
||||
|
||||
for (let i = 0; i < schlesiConfig.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT; i++) {
|
||||
const secretKey = interopSecretKey(i);
|
||||
const publicKey = secretKey.toPublicKey();
|
||||
const event: phase0.DepositEvent = {
|
||||
depositData: generateDeposit(i, secretKey, publicKey),
|
||||
index: i,
|
||||
blockNumber: i,
|
||||
};
|
||||
events.push(event);
|
||||
// All blocks satisfy MIN_GENESIS_TIME, so genesis will happen when the min validator count is reached
|
||||
blocks.push({
|
||||
number: i.toString(16),
|
||||
hash: ZERO_HASH_HEX,
|
||||
timestamp: schlesiConfig.MIN_GENESIS_TIME + i.toString(16),
|
||||
// Extra un-used data for this test
|
||||
parentHash: "0x0",
|
||||
totalDifficulty: "0x0",
|
||||
});
|
||||
}
|
||||
|
||||
return {events, blocks};
|
||||
}
|
||||
|
||||
function getMockEth1Provider({events, blocks}: MockData, eth1Provider?: Partial<IEth1Provider>): IEth1Provider {
|
||||
return {
|
||||
deployBlock: events[0].blockNumber,
|
||||
getBlockNumber: async () => 2000,
|
||||
getBlockByNumber: async (number) => blocks[number as number],
|
||||
getBlocksByNumber: async (fromBlock, toBlock) =>
|
||||
blocks.filter((b) => parseInt(b.number) >= fromBlock && parseInt(b.number) <= toBlock),
|
||||
getBlockByHash: async () => null,
|
||||
getDepositEvents: async (fromBlock, toBlock) =>
|
||||
events.filter((e) => e.blockNumber >= fromBlock && e.blockNumber <= toBlock),
|
||||
validateContract: async () => {
|
||||
return;
|
||||
},
|
||||
getState: () => Eth1ProviderState.ONLINE,
|
||||
...eth1Provider,
|
||||
};
|
||||
}
|
||||
|
||||
it("should build genesis state", async () => {
|
||||
const mockData = generateGenesisBuilderMockData();
|
||||
const eth1Provider = getMockEth1Provider(mockData);
|
||||
|
||||
const genesisBuilder = new GenesisBuilder({
|
||||
config: schlesiConfig,
|
||||
eth1Provider,
|
||||
logger,
|
||||
maxBlocksPerPoll: 1,
|
||||
});
|
||||
|
||||
const {state} = await genesisBuilder.waitForGenesis();
|
||||
|
||||
expect(state.validators.length).toBe(schlesiConfig.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT);
|
||||
expect(toHexString(state.eth1Data.blockHash)).toBe(
|
||||
mockData.blocks[schlesiConfig.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT - 1].hash
|
||||
);
|
||||
});
|
||||
|
||||
it("should abort building genesis state", async () => {
|
||||
const mockData = generateGenesisBuilderMockData();
|
||||
const controller = new AbortController();
|
||||
const eth1Provider = getMockEth1Provider(mockData, {
|
||||
getDepositEvents: async (fromBlock, toBlock) => {
|
||||
controller.abort();
|
||||
return mockData.events.filter((e) => e.blockNumber >= fromBlock && e.blockNumber <= toBlock);
|
||||
},
|
||||
});
|
||||
|
||||
const genesisBuilder = new GenesisBuilder({
|
||||
config: schlesiConfig,
|
||||
eth1Provider,
|
||||
logger,
|
||||
signal: controller.signal,
|
||||
maxBlocksPerPoll: 1,
|
||||
});
|
||||
|
||||
await expect(genesisBuilder.waitForGenesis()).rejects.toThrow(ErrorAborted);
|
||||
});
|
||||
});
|
||||
|
||||
function generateDeposit(index: ValidatorIndex, secretKey: SecretKey, publicKey: PublicKey): phase0.DepositData {
|
||||
const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH);
|
||||
const depositMessage = {
|
||||
pubkey: publicKey.toBytes(),
|
||||
withdrawalCredentials: Buffer.alloc(32, index),
|
||||
amount: MAX_EFFECTIVE_BALANCE,
|
||||
};
|
||||
const signingRoot = computeSigningRoot(ssz.phase0.DepositMessage, depositMessage, domain);
|
||||
const signature = secretKey.sign(signingRoot);
|
||||
return {...depositMessage, signature: signature.toBytes()};
|
||||
}
|
||||
@@ -245,10 +245,6 @@ describe("api/validator - produceBlockV3", () => {
|
||||
modules.chain.recomputeForkChoiceHead.mockReturnValue(generateProtoBlock({slot: headSlot}));
|
||||
modules.chain["opPool"].getSlashingsAndExits.mockReturnValue([[], [], [], []]);
|
||||
modules.chain["aggregatedAttestationPool"].getAttestationsForBlock.mockReturnValue([]);
|
||||
modules.chain["eth1"].getEth1DataAndDeposits.mockResolvedValue({
|
||||
eth1Data: ssz.phase0.Eth1Data.defaultValue(),
|
||||
deposits: [],
|
||||
});
|
||||
modules.chain["syncContributionAndProofPool"].getAggregate.mockReturnValue({
|
||||
syncCommitteeBits: ssz.altair.SyncCommitteeBits.defaultValue(),
|
||||
syncCommitteeSignature: G2_POINT_AT_INFINITY,
|
||||
|
||||
@@ -38,7 +38,7 @@ const TestSSZType = new ContainerType({
|
||||
|
||||
class TestRepository extends Repository<string, TestType> {
|
||||
constructor(db: Db) {
|
||||
super(config, db, Bucket.phase0_depositEvent, TestSSZType, "phase0_depositEvent");
|
||||
super(config, db, Bucket.phase0_exit, TestSSZType, "phase0_exit");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import {MockInstance, afterEach, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import {config} from "@lodestar/config/default";
|
||||
import {TimeoutError} from "@lodestar/utils";
|
||||
import {BeaconDb} from "../../../src/db/beacon.js";
|
||||
import {Eth1DepositDataTracker} from "../../../src/eth1/eth1DepositDataTracker.js";
|
||||
import {defaultEth1Options} from "../../../src/eth1/options.js";
|
||||
import {Eth1Provider} from "../../../src/eth1/provider/eth1Provider.js";
|
||||
import {getMockedBeaconDb} from "../../mocks/mockedBeaconDb.js";
|
||||
import {testLogger} from "../../utils/logger.js";
|
||||
|
||||
describe("Eth1DepositDataTracker", () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const logger = testLogger();
|
||||
const opts = {...defaultEth1Options, enabled: false};
|
||||
const signal = controller.signal;
|
||||
const eth1Provider = new Eth1Provider(config, opts, signal, null);
|
||||
let db: BeaconDb;
|
||||
let eth1DepositDataTracker: Eth1DepositDataTracker;
|
||||
let getBlocksByNumberStub: MockInstance;
|
||||
let getDepositEventsStub: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
db = getMockedBeaconDb();
|
||||
eth1DepositDataTracker = new Eth1DepositDataTracker(
|
||||
opts,
|
||||
{config, db, logger, signal, metrics: null},
|
||||
eth1Provider
|
||||
);
|
||||
vi.spyOn(Eth1DepositDataTracker.prototype as any, "getLastProcessedDepositBlockNumber").mockResolvedValue(0);
|
||||
vi.spyOn(eth1DepositDataTracker["eth1DataCache"], "getHighestCachedBlockNumber").mockResolvedValue(0);
|
||||
vi.spyOn(eth1DepositDataTracker["eth1DataCache"], "add").mockResolvedValue(void 0);
|
||||
|
||||
vi.spyOn(eth1DepositDataTracker["depositsCache"], "getEth1DataForBlocks").mockResolvedValue([]);
|
||||
vi.spyOn(eth1DepositDataTracker["depositsCache"], "add").mockResolvedValue(void 0);
|
||||
vi.spyOn(eth1DepositDataTracker["depositsCache"], "getLowestDepositEventBlockNumber").mockResolvedValue(0);
|
||||
|
||||
getBlocksByNumberStub = vi.spyOn(eth1Provider, "getBlocksByNumber");
|
||||
getDepositEventsStub = vi.spyOn(eth1Provider, "getDepositEvents");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("Should dynamically adjust blocks batch size", async () => {
|
||||
let expectedSize = 1000;
|
||||
expect(eth1DepositDataTracker["eth1GetBlocksBatchSizeDynamic"]).toBe(expectedSize);
|
||||
|
||||
// If there are timeerrors or parse errors then batch size should reduce
|
||||
getBlocksByNumberStub.mockRejectedValue(new TimeoutError("timeout error"));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expectedSize = Math.max(Math.floor(expectedSize / 2), 10);
|
||||
await eth1DepositDataTracker["updateBlockCache"](3000).catch((_e) => void 0);
|
||||
expect(eth1DepositDataTracker["eth1GetBlocksBatchSizeDynamic"]).toBe(expectedSize);
|
||||
}
|
||||
expect(expectedSize).toBe(10);
|
||||
|
||||
getBlocksByNumberStub.mockResolvedValue([]);
|
||||
// Should take a whole longer to get back to the orignal batch size
|
||||
for (let i = 0; i < 100; i++) {
|
||||
expectedSize = Math.min(expectedSize + 10, 1000);
|
||||
await eth1DepositDataTracker["updateBlockCache"](3000);
|
||||
expect(eth1DepositDataTracker["eth1GetBlocksBatchSizeDynamic"]).toBe(expectedSize);
|
||||
}
|
||||
expect(expectedSize).toBe(1000);
|
||||
});
|
||||
|
||||
it("Should dynamically adjust logs batch size", async () => {
|
||||
let expectedSize = 1000;
|
||||
expect(eth1DepositDataTracker["eth1GetLogsBatchSizeDynamic"]).toBe(expectedSize);
|
||||
|
||||
// If there are timeerrors or parse errors then batch size should reduce
|
||||
getDepositEventsStub.mockRejectedValue(new TimeoutError("timeout error"));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expectedSize = Math.max(Math.floor(expectedSize / 2), 10);
|
||||
await eth1DepositDataTracker["updateDepositCache"](3000).catch((_e) => void 0);
|
||||
expect(eth1DepositDataTracker["eth1GetLogsBatchSizeDynamic"]).toBe(expectedSize);
|
||||
}
|
||||
expect(expectedSize).toBe(10);
|
||||
|
||||
getDepositEventsStub.mockResolvedValue([]);
|
||||
// Should take a whole longer to get back to the orignal batch size
|
||||
for (let i = 0; i < 100; i++) {
|
||||
expectedSize = Math.min(expectedSize + 10, 1000);
|
||||
await eth1DepositDataTracker["updateDepositCache"](3000);
|
||||
expect(eth1DepositDataTracker["eth1GetLogsBatchSizeDynamic"]).toBe(expectedSize);
|
||||
}
|
||||
expect(expectedSize).toBe(1000);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {parseDepositLog} from "../../../../src/eth1/utils/depositContract.js";
|
||||
import {goerliTestnetDepositEvents, goerliTestnetLogs} from "../../../utils/testnet.js";
|
||||
|
||||
describe("eth1 / util / depositContract", () => {
|
||||
it("Should parse a raw deposit log", () => {
|
||||
const depositEvents = goerliTestnetLogs.map((log) => parseDepositLog(log));
|
||||
expect(depositEvents).toEqual(goerliTestnetDepositEvents);
|
||||
});
|
||||
});
|
||||
@@ -1,208 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {createChainForkConfig} from "@lodestar/config";
|
||||
import {MAX_DEPOSITS, SLOTS_PER_EPOCH} from "@lodestar/params";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {verifyMerkleBranch} from "@lodestar/utils";
|
||||
import {DepositTree} from "../../../../src/db/repositories/depositDataRoot.js";
|
||||
import {Eth1ErrorCode} from "../../../../src/eth1/errors.js";
|
||||
import {DepositGetter, getDeposits, getDepositsWithProofs} from "../../../../src/eth1/utils/deposits.js";
|
||||
import {createCachedBeaconStateTest} from "../../../utils/cachedBeaconState.js";
|
||||
import {filterBy} from "../../../utils/db.js";
|
||||
import {expectRejectedWithLodestarError} from "../../../utils/errors.js";
|
||||
import {generateState} from "../../../utils/state.js";
|
||||
|
||||
describe("eth1 / util / deposits", () => {
|
||||
describe("getDeposits", () => {
|
||||
type TestCase = {
|
||||
id: string;
|
||||
depositCount: number;
|
||||
eth1DepositIndex: number;
|
||||
depositIndexes: number[];
|
||||
expectedReturnedIndexes?: number[];
|
||||
error?: Eth1ErrorCode;
|
||||
postElectra?: boolean;
|
||||
};
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
id: "Return first deposit",
|
||||
depositCount: 1,
|
||||
eth1DepositIndex: 0,
|
||||
depositIndexes: [0, 1, 2, 3],
|
||||
expectedReturnedIndexes: [0],
|
||||
},
|
||||
{
|
||||
id: "Return second and third deposit",
|
||||
depositCount: 3,
|
||||
eth1DepositIndex: 1,
|
||||
depositIndexes: [0, 1, 2, 3],
|
||||
expectedReturnedIndexes: [1, 2],
|
||||
},
|
||||
{
|
||||
id: "No deposits to be included",
|
||||
depositCount: 3,
|
||||
eth1DepositIndex: 3,
|
||||
depositIndexes: [0, 1, 2, 3],
|
||||
expectedReturnedIndexes: [],
|
||||
},
|
||||
{
|
||||
id: "Limit deposits to MAX_DEPOSITS",
|
||||
depositCount: 10 * MAX_DEPOSITS,
|
||||
eth1DepositIndex: 0,
|
||||
depositIndexes: Array.from({length: 10 * MAX_DEPOSITS}, (_, i) => i),
|
||||
expectedReturnedIndexes: Array.from({length: MAX_DEPOSITS}, (_, i) => i),
|
||||
},
|
||||
{
|
||||
id: "Should throw if depositIndex > depositCount",
|
||||
depositCount: 0,
|
||||
eth1DepositIndex: 1,
|
||||
depositIndexes: [],
|
||||
error: Eth1ErrorCode.DEPOSIT_INDEX_TOO_HIGH,
|
||||
},
|
||||
{
|
||||
id: "Should throw if DB returns less deposits than expected",
|
||||
depositCount: 1,
|
||||
eth1DepositIndex: 0,
|
||||
depositIndexes: [],
|
||||
error: Eth1ErrorCode.NOT_ENOUGH_DEPOSITS,
|
||||
},
|
||||
{
|
||||
id: "Empty case",
|
||||
depositCount: 0,
|
||||
eth1DepositIndex: 0,
|
||||
depositIndexes: [],
|
||||
expectedReturnedIndexes: [],
|
||||
},
|
||||
{
|
||||
id: "No deposits to be included post Electra after deposit_requests_start_index",
|
||||
depositCount: 2030,
|
||||
eth1DepositIndex: 2025,
|
||||
depositIndexes: Array.from({length: 2030}, (_, i) => i),
|
||||
expectedReturnedIndexes: [],
|
||||
postElectra: true,
|
||||
},
|
||||
{
|
||||
id: "Should return deposits post Electra before deposit_requests_start_index",
|
||||
depositCount: 2022,
|
||||
eth1DepositIndex: 2018,
|
||||
depositIndexes: Array.from({length: 2022}, (_, i) => i),
|
||||
expectedReturnedIndexes: [2018, 2019, 2020, 2021],
|
||||
postElectra: true,
|
||||
},
|
||||
{
|
||||
id: "Should return deposits less than MAX_DEPOSITS post Electra before deposit_requests_start_index",
|
||||
depositCount: 10 * MAX_DEPOSITS,
|
||||
eth1DepositIndex: 0,
|
||||
depositIndexes: Array.from({length: 10 * MAX_DEPOSITS}, (_, i) => i),
|
||||
expectedReturnedIndexes: Array.from({length: MAX_DEPOSITS}, (_, i) => i),
|
||||
postElectra: true,
|
||||
},
|
||||
];
|
||||
|
||||
const postElectraConfig = createChainForkConfig({
|
||||
ALTAIR_FORK_EPOCH: 1,
|
||||
BELLATRIX_FORK_EPOCH: 2,
|
||||
CAPELLA_FORK_EPOCH: 3,
|
||||
DENEB_FORK_EPOCH: 4,
|
||||
ELECTRA_FORK_EPOCH: 5,
|
||||
});
|
||||
const postElectraSlot = postElectraConfig.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH + 1;
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, depositIndexes, eth1DepositIndex, depositCount, expectedReturnedIndexes, error, postElectra} =
|
||||
testCase;
|
||||
it(id, async () => {
|
||||
const state = postElectra
|
||||
? generateState({slot: postElectraSlot, eth1DepositIndex}, postElectraConfig)
|
||||
: generateState({eth1DepositIndex});
|
||||
const cachedState = createCachedBeaconStateTest(
|
||||
state,
|
||||
postElectra ? postElectraConfig : createChainForkConfig({})
|
||||
);
|
||||
const eth1Data = generateEth1Data(depositCount);
|
||||
const deposits = depositIndexes.map((index) => generateDepositEvent(index));
|
||||
const depositsGetter: DepositGetter<phase0.DepositEvent> = async (indexRange) =>
|
||||
filterBy(deposits, indexRange, (deposit) => deposit.index);
|
||||
|
||||
const resultPromise = getDeposits(cachedState, eth1Data, depositsGetter);
|
||||
|
||||
if (expectedReturnedIndexes) {
|
||||
const result = await resultPromise;
|
||||
expect(result.map((deposit) => deposit.index)).toEqual(expectedReturnedIndexes);
|
||||
} else if (error != null) {
|
||||
await expectRejectedWithLodestarError(resultPromise, error);
|
||||
} else {
|
||||
throw Error("Test case must have 'result' or 'error'");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("getDepositsWithProofs", () => {
|
||||
it("return empty array if no pending deposits", () => {
|
||||
const initialValues = [Buffer.alloc(32)];
|
||||
const depositRootTree = ssz.phase0.DepositDataRootList.toViewDU(initialValues);
|
||||
const depositCount = 0;
|
||||
const eth1Data = generateEth1Data(depositCount, depositRootTree);
|
||||
|
||||
const deposits = getDepositsWithProofs([], depositRootTree, eth1Data);
|
||||
expect(deposits).toEqual([]);
|
||||
});
|
||||
|
||||
it("return deposits with valid proofs", () => {
|
||||
const depositEvents = Array.from(
|
||||
{length: 2},
|
||||
(_, index): phase0.DepositEvent => ({
|
||||
depositData: ssz.phase0.DepositData.defaultValue(),
|
||||
blockNumber: index,
|
||||
index,
|
||||
})
|
||||
);
|
||||
|
||||
const depositRootTree = ssz.phase0.DepositDataRootList.defaultViewDU();
|
||||
for (const depositEvent of depositEvents) {
|
||||
depositRootTree.push(ssz.phase0.DepositData.hashTreeRoot(depositEvent.depositData));
|
||||
}
|
||||
const depositCount = depositEvents.length;
|
||||
const eth1Data = generateEth1Data(depositCount, depositRootTree);
|
||||
|
||||
const deposits = getDepositsWithProofs(depositEvents, depositRootTree, eth1Data);
|
||||
|
||||
// Should not return all deposits
|
||||
expect(deposits.length).toBe(2);
|
||||
|
||||
// Verify each individual merkle root
|
||||
for (const [index, deposit] of deposits.entries()) {
|
||||
// Wrong merkle proof on deposit ${index}
|
||||
expect(
|
||||
verifyMerkleBranch(
|
||||
ssz.phase0.DepositData.hashTreeRoot(deposit.data),
|
||||
Array.from(deposit.proof).map((p) => p),
|
||||
33,
|
||||
index,
|
||||
eth1Data.depositRoot
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function generateEth1Data(depositCount: number, depositRootTree?: DepositTree): phase0.Eth1Data {
|
||||
return {
|
||||
blockHash: Buffer.alloc(32),
|
||||
depositRoot: depositRootTree ? depositRootTree.sliceTo(depositCount - 1).hashTreeRoot() : Buffer.alloc(32),
|
||||
depositCount,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDepositEvent(index: number, blockNumber = 0): phase0.DepositEvent {
|
||||
const depositData = ssz.phase0.DepositData.defaultValue();
|
||||
depositData.amount = 32 * 10 * 9;
|
||||
|
||||
return {
|
||||
index,
|
||||
blockNumber,
|
||||
depositData,
|
||||
};
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {Root, phase0, ssz} from "@lodestar/types";
|
||||
import {toHex} from "@lodestar/utils";
|
||||
import {DepositTree} from "../../../../src/db/repositories/depositDataRoot.js";
|
||||
import {Eth1ErrorCode} from "../../../../src/eth1/errors.js";
|
||||
import {Eth1Block} from "../../../../src/eth1/interface.js";
|
||||
import {
|
||||
getDepositRootByDepositCount,
|
||||
getDepositsByBlockNumber,
|
||||
getEth1DataForBlocks,
|
||||
} from "../../../../src/eth1/utils/eth1Data.js";
|
||||
import {expectRejectedWithLodestarError} from "../../../utils/errors.js";
|
||||
import {iteratorFromArray} from "../../../utils/interator.js";
|
||||
|
||||
describe("eth1 / util / getEth1DataForBlocks", () => {
|
||||
type TestCase = {
|
||||
id: string;
|
||||
blocks: Eth1Block[];
|
||||
deposits: phase0.DepositEvent[];
|
||||
depositRootTree: DepositTree;
|
||||
lastProcessedDepositBlockNumber: number;
|
||||
expectedEth1Data?: Partial<phase0.Eth1Data & Eth1Block>[];
|
||||
error?: Eth1ErrorCode;
|
||||
};
|
||||
|
||||
const testCases: (() => TestCase)[] = [
|
||||
() => {
|
||||
// Result must contain all blocks from eth1Blocks, with backfilled eth1Data
|
||||
const expectedEth1Data = [
|
||||
{blockNumber: 5, depositCount: 13},
|
||||
{blockNumber: 6, depositCount: 13},
|
||||
{blockNumber: 7, depositCount: 17},
|
||||
{blockNumber: 8, depositCount: 17},
|
||||
{blockNumber: 9, depositCount: 17},
|
||||
];
|
||||
|
||||
// Consecutive block headers to be filled with eth1Data
|
||||
const blocks = expectedEth1Data.map(({blockNumber}) => getMockBlock({blockNumber}));
|
||||
|
||||
// Arbitrary list of consecutive non-uniform (blockNumber-wise) deposit roots
|
||||
const deposits: phase0.DepositEvent[] = expectedEth1Data.map(({blockNumber, depositCount}) =>
|
||||
getMockDeposit({blockNumber, index: depositCount - 1})
|
||||
);
|
||||
const lastProcessedDepositBlockNumber = expectedEth1Data.at(-1)?.blockNumber as number;
|
||||
|
||||
// Pre-fill the depositTree with roots for all deposits
|
||||
const depositRootTree = ssz.phase0.DepositDataRootList.toViewDU(
|
||||
Array.from({length: (deposits.at(-1)?.index as number) + 1}, (_, i) => Buffer.alloc(32, i))
|
||||
);
|
||||
|
||||
return {
|
||||
id: "Normal case",
|
||||
blocks,
|
||||
deposits,
|
||||
depositRootTree,
|
||||
lastProcessedDepositBlockNumber,
|
||||
expectedEth1Data,
|
||||
};
|
||||
},
|
||||
|
||||
() => {
|
||||
return {
|
||||
id: "No deposits yet, should throw with NoDepositsForBlockRange",
|
||||
blocks: [getMockBlock({blockNumber: 0})],
|
||||
deposits: [],
|
||||
depositRootTree: ssz.phase0.DepositDataRootList.defaultViewDU(),
|
||||
lastProcessedDepositBlockNumber: 0,
|
||||
error: Eth1ErrorCode.NO_DEPOSITS_FOR_BLOCK_RANGE,
|
||||
};
|
||||
},
|
||||
|
||||
() => {
|
||||
return {
|
||||
id: "With deposits and no deposit roots, should throw with NotEnoughDepositRoots",
|
||||
blocks: [getMockBlock({blockNumber: 0})],
|
||||
deposits: [getMockDeposit({blockNumber: 0, index: 0})],
|
||||
depositRootTree: ssz.phase0.DepositDataRootList.defaultViewDU(),
|
||||
lastProcessedDepositBlockNumber: 0,
|
||||
error: Eth1ErrorCode.NOT_ENOUGH_DEPOSIT_ROOTS,
|
||||
};
|
||||
},
|
||||
|
||||
() => {
|
||||
return {
|
||||
id: "Empty case",
|
||||
blocks: [],
|
||||
deposits: [],
|
||||
depositRootTree: ssz.phase0.DepositDataRootList.defaultViewDU(),
|
||||
lastProcessedDepositBlockNumber: 0,
|
||||
expectedEth1Data: [],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, blocks, deposits, depositRootTree, lastProcessedDepositBlockNumber, expectedEth1Data, error} =
|
||||
testCase();
|
||||
it(id, async () => {
|
||||
const eth1DatasPromise = getEth1DataForBlocks(
|
||||
blocks,
|
||||
// Simulate a descending stream reading from DB
|
||||
iteratorFromArray(deposits.reverse()),
|
||||
depositRootTree,
|
||||
lastProcessedDepositBlockNumber
|
||||
);
|
||||
|
||||
if (expectedEth1Data) {
|
||||
const eth1Datas = await eth1DatasPromise;
|
||||
const eth1DatasPartial = eth1Datas.map(({blockNumber, depositCount}) => ({blockNumber, depositCount}));
|
||||
expect(eth1DatasPartial).toEqual(expectedEth1Data);
|
||||
} else if (error != null) {
|
||||
await expectRejectedWithLodestarError(eth1DatasPromise, error);
|
||||
} else {
|
||||
throw Error("Test case must have 'expectedEth1Data' or 'error'");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("eth1 / util / getDepositsByBlockNumber", () => {
|
||||
type TestCase = {
|
||||
id: string;
|
||||
fromBlock: number;
|
||||
toBlock: number;
|
||||
deposits: phase0.DepositEvent[];
|
||||
expectedResult: phase0.DepositEvent[];
|
||||
};
|
||||
|
||||
const testCases: (() => TestCase)[] = [
|
||||
() => {
|
||||
const deposit0 = getMockDeposit({blockNumber: 0, index: 0});
|
||||
return {
|
||||
id: "Collect deposit at block 0 in range [1,2]",
|
||||
fromBlock: 1,
|
||||
toBlock: 2,
|
||||
deposits: [deposit0],
|
||||
expectedResult: [deposit0],
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const deposit1 = getMockDeposit({blockNumber: 1, index: 0});
|
||||
return {
|
||||
id: "Collect deposit at block 1 in range [1,2]",
|
||||
fromBlock: 1,
|
||||
toBlock: 2,
|
||||
deposits: [deposit1],
|
||||
expectedResult: [deposit1],
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const deposit3 = getMockDeposit({blockNumber: 3, index: 0});
|
||||
return {
|
||||
id: "Don't collect deposit at block 3 in range [1,2]",
|
||||
fromBlock: 1,
|
||||
toBlock: 2,
|
||||
deposits: [deposit3],
|
||||
expectedResult: [],
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const deposit0 = getMockDeposit({blockNumber: 0, index: 0});
|
||||
const deposit3 = getMockDeposit({blockNumber: 3, index: 4});
|
||||
return {
|
||||
id: "Collect multiple deposits",
|
||||
fromBlock: 1,
|
||||
toBlock: 4,
|
||||
deposits: [deposit0, deposit3],
|
||||
expectedResult: [deposit0, deposit3],
|
||||
};
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
id: "Empty case",
|
||||
fromBlock: 0,
|
||||
toBlock: 0,
|
||||
deposits: [],
|
||||
expectedResult: [],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, fromBlock, toBlock, deposits, expectedResult} = testCase();
|
||||
it(id, async () => {
|
||||
const result = await getDepositsByBlockNumber(
|
||||
fromBlock,
|
||||
toBlock, // Simulate a descending stream reading from DB
|
||||
iteratorFromArray(deposits.reverse())
|
||||
);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("eth1 / util / getDepositRootByDepositCount", () => {
|
||||
type TestCase = {
|
||||
id: string;
|
||||
depositCounts: number[];
|
||||
depositRootTree: DepositTree;
|
||||
expectedMap: Map<number, Root>;
|
||||
};
|
||||
|
||||
const fullRootMap = new Map<number, Root>();
|
||||
const fullDepositRootTree = ssz.phase0.DepositDataRootList.defaultViewDU();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
fullDepositRootTree.push(Buffer.alloc(32, i));
|
||||
fullRootMap.set(fullDepositRootTree.length, fullDepositRootTree.hashTreeRoot());
|
||||
}
|
||||
|
||||
const testCases: (() => TestCase)[] = [
|
||||
() => {
|
||||
return {
|
||||
id: "Roots are computed correctly, all values match",
|
||||
depositCounts: Array.from(fullRootMap.keys()),
|
||||
depositRootTree: fullDepositRootTree,
|
||||
expectedMap: fullRootMap,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const depositCounts = Array.from(fullRootMap.keys()).filter((n) => n % 2);
|
||||
const expectedMap = new Map<number, Root>();
|
||||
for (const depositCount of depositCounts) {
|
||||
const depositRoot = fullRootMap.get(depositCount);
|
||||
if (depositRoot) expectedMap.set(depositCount, depositRoot);
|
||||
}
|
||||
return {
|
||||
id: "Roots are computed correctly, sparse values match",
|
||||
depositCounts,
|
||||
depositRootTree: fullDepositRootTree,
|
||||
expectedMap,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const emptyTree = ssz.phase0.DepositDataRootList.defaultViewDU();
|
||||
return {
|
||||
id: "Empty case",
|
||||
depositCounts: [],
|
||||
depositRootTree: emptyTree,
|
||||
expectedMap: new Map<number, Root>(),
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, depositCounts, depositRootTree, expectedMap} = testCase();
|
||||
it(id, () => {
|
||||
const map = getDepositRootByDepositCount(depositCounts, depositRootTree);
|
||||
expect(renderDepositRootByDepositCount(map)).toEqual(renderDepositRootByDepositCount(expectedMap));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function renderDepositRootByDepositCount(map: Map<number, Uint8Array>): Record<string, string> {
|
||||
const data: Record<string, string> = {};
|
||||
for (const [key, root] of Object.entries(map)) {
|
||||
data[key] = toHex(root);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function getMockBlock({blockNumber}: {blockNumber: number}): Eth1Block {
|
||||
return {
|
||||
blockNumber,
|
||||
blockHash: Buffer.alloc(32, blockNumber),
|
||||
timestamp: blockNumber,
|
||||
};
|
||||
}
|
||||
|
||||
function getMockDeposit({blockNumber, index}: {blockNumber: number; index: number}): phase0.DepositEvent {
|
||||
return {
|
||||
blockNumber,
|
||||
index,
|
||||
depositData: {} as phase0.DepositData, // Not used
|
||||
};
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {assertConsecutiveDeposits} from "../../../../src/eth1/utils/eth1DepositEvent.js";
|
||||
|
||||
describe("eth1 / util / assertConsecutiveDeposits", () => {
|
||||
const testCases: {
|
||||
id: string;
|
||||
ok: boolean;
|
||||
depositEvents: {index: number}[];
|
||||
}[] = [
|
||||
{
|
||||
id: "sequential deposits",
|
||||
ok: true,
|
||||
depositEvents: [{index: 4}, {index: 5}, {index: 6}],
|
||||
},
|
||||
{
|
||||
id: "non sequential deposits",
|
||||
ok: false,
|
||||
depositEvents: [{index: 4}, {index: 7}, {index: 9}],
|
||||
},
|
||||
{
|
||||
id: "sequential descending deposits",
|
||||
ok: false,
|
||||
depositEvents: [{index: 6}, {index: 5}, {index: 4}],
|
||||
},
|
||||
{
|
||||
id: "single deposit",
|
||||
ok: true,
|
||||
depositEvents: [{index: 4}],
|
||||
},
|
||||
{
|
||||
id: "empty array",
|
||||
ok: true,
|
||||
depositEvents: [],
|
||||
},
|
||||
];
|
||||
|
||||
for (const {id, ok, depositEvents} of testCases) {
|
||||
it(id, () => {
|
||||
if (ok) {
|
||||
assertConsecutiveDeposits(depositEvents);
|
||||
} else {
|
||||
expect(() => assertConsecutiveDeposits(depositEvents)).toThrow();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {ChainForkConfig} from "@lodestar/config";
|
||||
import {config} from "@lodestar/config/default";
|
||||
import {BeaconStateAllForks} from "@lodestar/state-transition";
|
||||
import {phase0, ssz} from "@lodestar/types";
|
||||
import {
|
||||
Eth1DataGetter,
|
||||
getEth1VotesToConsider,
|
||||
pickEth1Vote,
|
||||
votingPeriodStartTime,
|
||||
} from "../../../../src/eth1/utils/eth1Vote.js";
|
||||
import {filterBy} from "../../../utils/db.js";
|
||||
import {generateState} from "../../../utils/state.js";
|
||||
|
||||
describe("eth1 / util / eth1Vote", () => {
|
||||
function generateEth1Vote(i: number): phase0.Eth1Data {
|
||||
return {
|
||||
blockHash: Buffer.alloc(32, i),
|
||||
depositRoot: Buffer.alloc(32, i),
|
||||
depositCount: i,
|
||||
};
|
||||
}
|
||||
|
||||
describe("pickEth1Vote", () => {
|
||||
// Function array to scope votes in each test case defintion
|
||||
const testCases: (() => {
|
||||
id: string;
|
||||
eth1DataVotesInState: phase0.Eth1Data[];
|
||||
votesToConsider: phase0.Eth1Data[];
|
||||
expectedEth1Vote: phase0.Eth1Data;
|
||||
})[] = [
|
||||
() => {
|
||||
const vote = generateEth1Vote(0);
|
||||
return {
|
||||
id: "basic case, pick the only valid vote",
|
||||
eth1DataVotesInState: [vote],
|
||||
votesToConsider: [vote],
|
||||
expectedEth1Vote: vote,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const vote = generateEth1Vote(0);
|
||||
const voteDefault = generateEth1Vote(1);
|
||||
return {
|
||||
id: "no valid votes in state, pick the default first from votesToConsider",
|
||||
eth1DataVotesInState: [vote],
|
||||
votesToConsider: [voteDefault],
|
||||
expectedEth1Vote: voteDefault,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const vote = generateEth1Vote(0);
|
||||
return {
|
||||
id: "no votes in state",
|
||||
eth1DataVotesInState: [],
|
||||
votesToConsider: [vote],
|
||||
expectedEth1Vote: vote,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const vote1 = generateEth1Vote(0);
|
||||
const vote2 = generateEth1Vote(1);
|
||||
const vote3 = generateEth1Vote(2);
|
||||
return {
|
||||
id: "pick most frequent vote",
|
||||
eth1DataVotesInState: [vote1, vote2, vote2, vote2, vote3],
|
||||
votesToConsider: [vote1, vote2, vote3],
|
||||
expectedEth1Vote: vote2,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const vote1 = generateEth1Vote(0);
|
||||
const vote2 = generateEth1Vote(0);
|
||||
return {
|
||||
id: "tiebreak",
|
||||
eth1DataVotesInState: [vote1, vote2],
|
||||
votesToConsider: [vote1, vote2],
|
||||
expectedEth1Vote: vote1,
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, eth1DataVotesInState, votesToConsider, expectedEth1Vote} = testCase();
|
||||
it(id, async () => {
|
||||
const state = generateState({slot: 5, eth1DataVotes: eth1DataVotesInState});
|
||||
const eth1Vote = pickEth1Vote(state, votesToConsider);
|
||||
expect(ssz.phase0.Eth1Data.toJson(eth1Vote)).toEqual(ssz.phase0.Eth1Data.toJson(expectedEth1Vote));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("getEth1VotesToConsider", () => {
|
||||
// Function array to scope votes in each test case defintion
|
||||
const testCases: (() => {
|
||||
id: string;
|
||||
state: BeaconStateAllForks;
|
||||
eth1Datas: Eth1DataWithTimestamp[];
|
||||
expectedVotesToConsider: phase0.Eth1Data[];
|
||||
})[] = [
|
||||
() => {
|
||||
const state = generateState({eth1Data: generateEth1Vote(0)});
|
||||
const timestampInRange = getTimestampInRange(config, state);
|
||||
const vote1 = getEth1DataBlock({depositCount: 1, timestamp: 0});
|
||||
const vote2 = getEth1DataBlock({depositCount: 1, timestamp: timestampInRange});
|
||||
const vote3 = getEth1DataBlock({depositCount: 1, timestamp: Infinity});
|
||||
return {
|
||||
id: "Only consider blocks with a timestamp in range",
|
||||
state,
|
||||
eth1Datas: [vote1, vote2, vote3].map(getEth1DataBlock),
|
||||
expectedVotesToConsider: [vote2],
|
||||
};
|
||||
},
|
||||
() => {
|
||||
const state = generateState({eth1Data: generateEth1Vote(11)});
|
||||
const timestampInRange = getTimestampInRange(config, state);
|
||||
const vote1 = getEth1DataBlock({depositCount: 10, timestamp: timestampInRange});
|
||||
const vote2 = getEth1DataBlock({depositCount: 12, timestamp: timestampInRange});
|
||||
return {
|
||||
id: "Ensure first vote is depositCount < current state is not considered",
|
||||
state,
|
||||
eth1Datas: [vote1, vote2].map(getEth1DataBlock),
|
||||
expectedVotesToConsider: [vote2],
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const {id, state, eth1Datas, expectedVotesToConsider} = testCase();
|
||||
it(`get votesToConsider: ${id}`, async () => {
|
||||
const eth1DataGetter: Eth1DataGetter = async ({timestampRange}) =>
|
||||
filterBy(eth1Datas, timestampRange, (eth1Data) => eth1Data.timestamp);
|
||||
|
||||
const votesToConsider = await getEth1VotesToConsider(config, state, eth1DataGetter);
|
||||
|
||||
expect(votesToConsider.map((eth1Data) => ssz.phase0.Eth1Data.toJson(eth1Data))).toEqual(
|
||||
expectedVotesToConsider.map((eth1Data) => ssz.phase0.Eth1Data.toJson(eth1Data))
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Eth1DataWithTimestamp extends phase0.Eth1Data {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Util: Fill partial eth1DataBlock with mock data
|
||||
* @param eth1DataBlock
|
||||
*/
|
||||
function getEth1DataBlock(eth1DataBlock: Partial<Eth1DataWithTimestamp>): Eth1DataWithTimestamp {
|
||||
return {
|
||||
blockHash: Buffer.alloc(32),
|
||||
depositRoot: Buffer.alloc(32),
|
||||
depositCount: 0,
|
||||
timestamp: 0,
|
||||
...eth1DataBlock,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Util: Get a mock timestamp that passes isCandidateBlock validation
|
||||
* @param config
|
||||
* @param state
|
||||
*/
|
||||
function getTimestampInRange(config: ChainForkConfig, state: BeaconStateAllForks): number {
|
||||
const {SECONDS_PER_ETH1_BLOCK, ETH1_FOLLOW_DISTANCE} = config;
|
||||
const periodStart = votingPeriodStartTime(config, state);
|
||||
return periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {phase0} from "@lodestar/types";
|
||||
import {groupDepositEventsByBlock} from "../../../../src/eth1/utils/groupDepositEventsByBlock.js";
|
||||
|
||||
describe("eth1 / util / groupDepositEventsByBlock", () => {
|
||||
it("should return deposit events by block sorted by index", () => {
|
||||
const depositData = {
|
||||
amount: 0,
|
||||
signature: Buffer.alloc(96),
|
||||
withdrawalCredentials: Buffer.alloc(32),
|
||||
pubkey: Buffer.alloc(48),
|
||||
};
|
||||
const depositEvents: phase0.DepositEvent[] = [
|
||||
{blockNumber: 1, index: 0, depositData},
|
||||
{blockNumber: 2, index: 2, depositData},
|
||||
{blockNumber: 2, index: 1, depositData},
|
||||
{blockNumber: 3, index: 4, depositData},
|
||||
{blockNumber: 3, index: 3, depositData},
|
||||
];
|
||||
const blockEvents = groupDepositEventsByBlock(depositEvents);
|
||||
|
||||
// Keep only the relevant info of the result
|
||||
const blockEventsIndexOnly = blockEvents.map((blockEvent) => ({
|
||||
blockNumber: blockEvent.blockNumber,
|
||||
deposits: blockEvent.depositEvents.map((deposit) => deposit.index),
|
||||
}));
|
||||
|
||||
expect(blockEventsIndexOnly).toEqual([
|
||||
{blockNumber: 1, deposits: [0]},
|
||||
{blockNumber: 2, deposits: [1, 2]},
|
||||
{blockNumber: 3, deposits: [3, 4]},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {Eth1Block} from "../../../../src/eth1/interface.js";
|
||||
import {optimizeNextBlockDiffForGenesis} from "../../../../src/eth1/utils/optimizeNextBlockDiffForGenesis.js";
|
||||
|
||||
describe("eth1 / utils / optimizeNextBlockDiffForGenesis", () => {
|
||||
it("should return optimized block diff to find genesis time", () => {
|
||||
const params = {
|
||||
MIN_GENESIS_TIME: 1578009600,
|
||||
GENESIS_DELAY: 172800,
|
||||
SECONDS_PER_ETH1_BLOCK: 14,
|
||||
};
|
||||
const initialTimeDiff = params.GENESIS_DELAY * 2;
|
||||
let lastFetchedBlock: Eth1Block = {
|
||||
blockHash: Buffer.alloc(32, 0),
|
||||
blockNumber: 100000,
|
||||
timestamp: params.MIN_GENESIS_TIME - initialTimeDiff,
|
||||
};
|
||||
|
||||
const diffRecord: {blockDiff: number; number: number}[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const blockDiff = optimizeNextBlockDiffForGenesis(lastFetchedBlock, params);
|
||||
|
||||
// Simulate fetching the next block
|
||||
lastFetchedBlock = {
|
||||
blockHash: Buffer.alloc(32, 0),
|
||||
blockNumber: lastFetchedBlock.blockNumber + blockDiff,
|
||||
timestamp: lastFetchedBlock.timestamp + blockDiff * params.SECONDS_PER_ETH1_BLOCK,
|
||||
};
|
||||
|
||||
if (lastFetchedBlock.timestamp > params.MIN_GENESIS_TIME - params.GENESIS_DELAY) {
|
||||
break;
|
||||
}
|
||||
diffRecord.push({number: lastFetchedBlock.blockNumber, blockDiff});
|
||||
}
|
||||
|
||||
// Make sure the returned diffs converge to genesis time fast
|
||||
expect(diffRecord).toEqual([
|
||||
{number: 106171, blockDiff: 6171},
|
||||
{number: 109256, blockDiff: 3085},
|
||||
{number: 110799, blockDiff: 1543},
|
||||
{number: 111570, blockDiff: 771},
|
||||
{number: 111956, blockDiff: 386},
|
||||
{number: 112149, blockDiff: 193},
|
||||
{number: 112245, blockDiff: 96},
|
||||
{number: 112293, blockDiff: 48},
|
||||
{number: 112317, blockDiff: 24},
|
||||
{number: 112329, blockDiff: 12},
|
||||
{number: 112335, blockDiff: 6},
|
||||
{number: 112338, blockDiff: 3},
|
||||
{number: 112340, blockDiff: 2},
|
||||
{number: 112341, blockDiff: 1},
|
||||
{number: 112342, blockDiff: 1},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
quantityToBigint,
|
||||
quantityToBytes,
|
||||
quantityToNum,
|
||||
} from "../../../src/eth1/provider/utils.js";
|
||||
} from "../../../../src/execution/engine/utils.js";
|
||||
|
||||
describe("eth1 / hex encoding", () => {
|
||||
describe("execution / engine / hex encoding", () => {
|
||||
describe("QUANTITY", () => {
|
||||
const testCases: {
|
||||
quantity: QUANTITY;
|
||||
@@ -1,7 +1,7 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {decodeJwtToken, encodeJwtToken} from "../../../src/eth1/provider/jwt.js";
|
||||
import {decodeJwtToken, encodeJwtToken} from "../../../../src/execution/engine/jwt.js";
|
||||
|
||||
describe("ExecutionEngine / jwt", () => {
|
||||
describe("execution / engine / jwt", () => {
|
||||
it("encode/decode correctly", () => {
|
||||
const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255)));
|
||||
const claim = {iat: Math.floor(new Date().getTime() / 1000)};
|
||||
@@ -1,6 +1,6 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {ErrorAborted, FetchError} from "@lodestar/utils";
|
||||
import {ErrorJsonRpcResponse, HttpRpcError} from "../../../../src/eth1/provider/jsonRpcHttpClient.js";
|
||||
import {ErrorJsonRpcResponse, HttpRpcError} from "../../../../src/execution/engine/jsonRpcHttpClient.js";
|
||||
import {
|
||||
HTTP_CONNECTION_ERROR_CODES,
|
||||
HTTP_FATAL_ERROR_CODES,
|
||||
|
||||
@@ -2,14 +2,13 @@ import {fastify} from "fastify";
|
||||
import {afterAll, beforeAll, describe, expect, it} from "vitest";
|
||||
import {Logger} from "@lodestar/logger";
|
||||
import {ForkName} from "@lodestar/params";
|
||||
import {RpcPayload} from "../../../src/eth1/interface.js";
|
||||
import {numToQuantity} from "../../../src/eth1/provider/utils.js";
|
||||
import {defaultExecutionEngineHttpOpts} from "../../../src/execution/engine/http.js";
|
||||
import {
|
||||
parseExecutionPayload,
|
||||
serializeExecutionPayload,
|
||||
serializeExecutionPayloadBody,
|
||||
} from "../../../src/execution/engine/types.js";
|
||||
import {RpcPayload, numToQuantity} from "../../../src/execution/engine/utils.js";
|
||||
import {IExecutionEngine, initializeExecutionEngine} from "../../../src/execution/index.js";
|
||||
|
||||
describe("ExecutionEngine / http", () => {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {afterAll, beforeAll, describe, expect, it} from "vitest";
|
||||
import {fromHexString} from "@chainsafe/ssz";
|
||||
import {Logger} from "@lodestar/logger";
|
||||
import {ForkName} from "@lodestar/params";
|
||||
import {bytesToData, numToQuantity} from "../../../src/eth1/provider/utils.js";
|
||||
import {defaultExecutionEngineHttpOpts} from "../../../src/execution/engine/http.js";
|
||||
import {bytesToData, numToQuantity} from "../../../src/execution/engine/utils.js";
|
||||
import {IExecutionEngine, PayloadAttributes, initializeExecutionEngine} from "../../../src/execution/index.js";
|
||||
|
||||
describe("ExecutionEngine / http ", () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {ChainForkConfig, createBeaconConfig} from "@lodestar/config";
|
||||
import {ssz} from "@lodestar/types";
|
||||
import {sleep} from "@lodestar/utils";
|
||||
import {BeaconChain} from "../../src/chain/chain.js";
|
||||
import {Eth1ForBlockProductionDisabled} from "../../src/eth1/index.js";
|
||||
import {ExecutionEngineDisabled} from "../../src/execution/index.js";
|
||||
import {ArchiveMode} from "../../src/index.js";
|
||||
import {GossipHandlers, Network, NetworkInitModules, getReqRespHandlers} from "../../src/network/index.js";
|
||||
@@ -76,7 +75,6 @@ export async function getNetworkForTest(
|
||||
validatorMonitor: null,
|
||||
anchorState: createCachedBeaconStateTest(state, beaconConfig),
|
||||
isAnchorStateFinalized: true,
|
||||
eth1: new Eth1ForBlockProductionDisabled(),
|
||||
executionEngine: new ExecutionEngineDisabled(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {BeaconNode} from "../../../src/index.js";
|
||||
import {defaultNetworkOptions} from "../../../src/network/options.js";
|
||||
import {IBeaconNodeOptions, defaultOptions} from "../../../src/node/options.js";
|
||||
import {InteropStateOpts} from "../../../src/node/utils/interop/state.js";
|
||||
import {initDevState, writeDeposits} from "../../../src/node/utils/state.js";
|
||||
import {initDevState} from "../../../src/node/utils/state.js";
|
||||
import {testLogger} from "../logger.js";
|
||||
|
||||
export async function getDevBeaconNode(
|
||||
@@ -45,13 +45,10 @@ export async function getDevBeaconNode(
|
||||
|
||||
let anchorState = opts.anchorState;
|
||||
if (!anchorState) {
|
||||
const {state, deposits} = initDevState(config, validatorCount, opts);
|
||||
anchorState = state;
|
||||
anchorState = initDevState(config, validatorCount, opts);
|
||||
|
||||
// Is it necessary to persist deposits and genesis block?
|
||||
await writeDeposits(db, deposits);
|
||||
const block = config.getForkTypes(GENESIS_SLOT).SignedBeaconBlock.defaultValue();
|
||||
block.message.stateRoot = state.hashTreeRoot();
|
||||
block.message.stateRoot = anchorState.hashTreeRoot();
|
||||
await db.blockArchive.add(block);
|
||||
|
||||
if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.deneb) {
|
||||
@@ -69,7 +66,6 @@ export async function getDevBeaconNode(
|
||||
// dev defaults that we wish, especially for the api options
|
||||
{
|
||||
db: {name: tmpDir.name},
|
||||
eth1: {enabled: false},
|
||||
api: {rest: {api: ["beacon", "config", "events", "node", "validator"], port: 19596}},
|
||||
metrics: {enabled: false},
|
||||
network: {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {spawn} from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import {ChainConfig} from "@lodestar/config";
|
||||
import {sleep} from "@lodestar/utils";
|
||||
import {ZERO_HASH} from "../../src/constants/index.js";
|
||||
import {Eth1Provider} from "../../src/index.js";
|
||||
import {fromHex, sleep} from "@lodestar/utils";
|
||||
import {JsonRpcHttpClient} from "../../src/execution/engine/jsonRpcHttpClient.js";
|
||||
import {shell} from "../sim/shell.js";
|
||||
|
||||
let txRpcId = 1;
|
||||
@@ -87,18 +85,20 @@ async function getGenesisBlockHash(
|
||||
{providerUrl, jwtSecretHex}: {providerUrl: string; jwtSecretHex?: string},
|
||||
signal: AbortSignal
|
||||
): Promise<string> {
|
||||
const eth1Provider = new Eth1Provider(
|
||||
{DEPOSIT_CONTRACT_ADDRESS: ZERO_HASH} as Partial<ChainConfig> as ChainConfig,
|
||||
{providerUrls: [providerUrl], jwtSecretHex},
|
||||
signal
|
||||
);
|
||||
const rpc = new JsonRpcHttpClient([providerUrl], {
|
||||
signal,
|
||||
jwtSecret: jwtSecretHex ? fromHex(jwtSecretHex) : undefined,
|
||||
});
|
||||
|
||||
// Need to run multiple tries because nethermind sometimes is not yet ready and throws error
|
||||
// of connection refused while fetching genesis block
|
||||
for (let i = 1; i <= 60; i++) {
|
||||
console.log(`fetching genesisBlock hash, try: ${i}`);
|
||||
try {
|
||||
const genesisBlock = await eth1Provider.getBlockByNumber(0);
|
||||
const genesisBlock = await rpc.fetch<{hash: string}>({
|
||||
method: "eth_getBlockByNumber",
|
||||
params: ["0x0", false],
|
||||
});
|
||||
console.log({genesisBlock});
|
||||
if (!genesisBlock) {
|
||||
throw Error("No genesis block available");
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {fromHexString} from "@chainsafe/ssz";
|
||||
import {ChainForkConfig, createChainForkConfig} from "@lodestar/config";
|
||||
import {chainConfig} from "@lodestar/config/default";
|
||||
import {phase0} from "@lodestar/types";
|
||||
|
||||
/** Generic testnet data taken from the Medalla testnet */
|
||||
export const medallaTestnetConfig = {
|
||||
depositBlock: 3085928,
|
||||
// Optimized blocks for quick testing
|
||||
blockWithDepositActivity: 3124889,
|
||||
};
|
||||
|
||||
/** Testnet specs for the Medalla testnet */
|
||||
export function getTestnetConfig(): ChainForkConfig {
|
||||
const config = createChainForkConfig(chainConfig);
|
||||
config.DEPOSIT_NETWORK_ID = 5;
|
||||
config.DEPOSIT_CONTRACT_ADDRESS = Buffer.from("07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC", "hex");
|
||||
config.MIN_GENESIS_TIME = 1596546000;
|
||||
config.GENESIS_DELAY = 172800;
|
||||
config.GENESIS_FORK_VERSION = Buffer.from("00000001", "hex");
|
||||
return config;
|
||||
}
|
||||
|
||||
/** Goerli deposit log for the Medalla testnet */
|
||||
export const goerliTestnetLogs = [
|
||||
{
|
||||
// Raw unparsed log index 6833
|
||||
blockNumber: 3124930,
|
||||
txHash: "0x9662b35ea4128fafe8185f8b4b0b890f72009d31e9d65a8f2ad5712f74910644",
|
||||
topics: ["0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"],
|
||||
data: "0x00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000308214eabc827a4deaed78c0bf3f91d81b57968041b5d7c975c716641ccfac7aa4e11e3354a357b1f40637e282fd66403500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000bb991061d2545c75e788b93f3425b03b05f0d2aae8e97da30d7d04886b9eb700000000000000000000000000000000000000000000000000000000000000080040597307000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006099cb82bc69b4111d1a828963f0316ec9aa38c4e9e041a8afec86cd20dfe9a590999845bf01d4689f3bbe3df54e48695e081f1216027b577c7fccf6ab0a4fcc75faf8009c6b55e518478139f604f542d138ae3bc34bad01ee6002006d64c4ff820000000000000000000000000000000000000000000000000000000000000008b11a000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
];
|
||||
|
||||
/** Goerli parsed deposit event for the Medalla testnet */
|
||||
export const goerliTestnetDepositEvents: phase0.DepositEvent[] = [
|
||||
{
|
||||
blockNumber: 3124930,
|
||||
index: 6833,
|
||||
depositData: {
|
||||
pubkey: fromHexString(
|
||||
"8214EABC827A4DEAED78C0BF3F91D81B57968041B5D7C975C716641CCFAC7AA4E11E3354A357B1F40637E282FD664035"
|
||||
),
|
||||
withdrawalCredentials: fromHexString("00BB991061D2545C75E788B93F3425B03B05F0D2AAE8E97DA30D7D04886B9EB7"),
|
||||
amount: 32e9,
|
||||
signature: fromHexString(
|
||||
"99CB82BC69B4111D1A828963F0316EC9AA38C4E9E041A8AFEC86CD20DFE9A590999845BF01D4689F3BBE3DF54E48695E081F1216027B577C7FCCF6AB0A4FCC75FAF8009C6B55E518478139F604F542D138AE3BC34BAD01EE6002006D64C4FF82"
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -10,7 +10,7 @@ import {ACTIVE_PRESET, PresetName} from "@lodestar/params";
|
||||
import {ErrorAborted, bytesToInt, formatBytes} from "@lodestar/utils";
|
||||
import {ProcessShutdownCallback} from "@lodestar/validator";
|
||||
import {BeaconNodeOptions, getBeaconConfigFromArgs} from "../../config/index.js";
|
||||
import {getNetworkBootnodes, getNetworkData, isKnownNetworkName, readBootnodes} from "../../networks/index.js";
|
||||
import {getNetworkBootnodes, isKnownNetworkName, readBootnodes} from "../../networks/index.js";
|
||||
import {GlobalArgs, parseBeaconNodeArgs} from "../../options/index.js";
|
||||
import {LogArgs} from "../../options/logOptions.js";
|
||||
import {
|
||||
@@ -71,13 +71,11 @@ export async function beaconHandler(args: BeaconArgs & GlobalArgs): Promise<void
|
||||
// BeaconNode setup
|
||||
try {
|
||||
const {anchorState, isFinalized, wsCheckpoint} = await initBeaconState(
|
||||
options,
|
||||
args,
|
||||
beaconPaths.dataDir,
|
||||
config,
|
||||
db,
|
||||
logger,
|
||||
abortController.signal
|
||||
logger
|
||||
);
|
||||
const beaconConfig = createBeaconConfig(config, anchorState.genesisValidatorsRoot);
|
||||
const node = await BeaconNode.init({
|
||||
@@ -189,12 +187,6 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
|
||||
// Add detailed version string for API node/version endpoint
|
||||
beaconNodeOptions.set({api: {commit, version}});
|
||||
|
||||
// Set known depositContractDeployBlock
|
||||
if (isKnownNetworkName(network)) {
|
||||
const {depositContractDeployBlock} = getNetworkData(network);
|
||||
beaconNodeOptions.set({eth1: {depositContractDeployBlock}});
|
||||
}
|
||||
|
||||
const logger = initLogger(args, beaconPaths.dataDir, config);
|
||||
const {privateKey, enr} = await initPrivateKeyAndEnr(args, beaconPaths.beaconDir, logger);
|
||||
|
||||
@@ -226,7 +218,7 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
|
||||
// Add User-Agent header to all builder requests
|
||||
beaconNodeOptions.set({executionBuilder: {userAgent: versionStr}});
|
||||
// Set jwt version with version string
|
||||
beaconNodeOptions.set({executionEngine: {jwtVersion: versionStr}, eth1: {jwtVersion: versionStr}});
|
||||
beaconNodeOptions.set({executionEngine: {jwtVersion: versionStr}});
|
||||
// Set commit and version for ClientVersion
|
||||
beaconNodeOptions.set({executionEngine: {commit, version}});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import {
|
||||
DbCPStateDatastore,
|
||||
FileCPStateDatastore,
|
||||
IBeaconDb,
|
||||
IBeaconNodeOptions,
|
||||
checkAndPersistAnchorState,
|
||||
getStateTypeFromBytes,
|
||||
initStateFromEth1,
|
||||
} from "@lodestar/beacon-node";
|
||||
import {BeaconConfig, ChainForkConfig, createBeaconConfig} from "@lodestar/config";
|
||||
import {
|
||||
@@ -94,20 +92,17 @@ async function initAndVerifyWeakSubjectivityState(
|
||||
* 1. restore from weak subjectivity state (possibly downloaded from a remote beacon node)
|
||||
* 2. restore from db
|
||||
* 3. restore from genesis state (possibly downloaded via URL)
|
||||
* 4. create genesis state from eth1
|
||||
*
|
||||
* The returned anchorState could be finalized or not.
|
||||
* - if we load from checkpointState, checkpointSyncUrl, genesisStateFile or archived db, it is finalized
|
||||
* - it's not finalized if we load from unsafeCheckpointState or lastPersistedCheckpointState
|
||||
*/
|
||||
export async function initBeaconState(
|
||||
options: IBeaconNodeOptions,
|
||||
args: BeaconArgs & GlobalArgs,
|
||||
dataDir: string,
|
||||
chainForkConfig: ChainForkConfig,
|
||||
db: IBeaconDb,
|
||||
logger: Logger,
|
||||
signal: AbortSignal
|
||||
logger: Logger
|
||||
): Promise<{anchorState: BeaconStateAllForks; isFinalized: boolean; wsCheckpoint?: Checkpoint}> {
|
||||
if (args.forceCheckpointSync && !(args.checkpointState || args.checkpointSyncUrl || args.unsafeCheckpointState)) {
|
||||
throw new Error("Forced checkpoint sync without specifying a checkpointState or checkpointSyncUrl");
|
||||
@@ -333,9 +328,7 @@ export async function initBeaconState(
|
||||
return {anchorState, isFinalized};
|
||||
}
|
||||
|
||||
// Only place we will not bother checking isWithinWeakSubjectivityPeriod as forceGenesis passed by user
|
||||
const anchorState = await initStateFromEth1({config: chainForkConfig, db, logger, opts: options.eth1, signal});
|
||||
return {anchorState, isFinalized: true};
|
||||
throw Error("Failed to initialize beacon state, please provide a genesis state file or use checkpoint sync");
|
||||
}
|
||||
|
||||
async function readWSState(
|
||||
|
||||
@@ -150,7 +150,7 @@ export const beaconExtraOptions: CliCommandOptions<BeaconExtraArgs> = {
|
||||
|
||||
private: {
|
||||
description:
|
||||
"Do not send implementation details over p2p identify protocol and in builder, execution engine and eth1 requests",
|
||||
"Do not send implementation details over p2p identify protocol, and in builder and execution engine requests",
|
||||
type: "boolean",
|
||||
},
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function writeTestnetFiles(
|
||||
const genesisTime = Math.floor(Date.now() / 1000);
|
||||
const eth1BlockHash = Buffer.alloc(32, 0);
|
||||
|
||||
const {state} = nodeUtils.initDevState(config, genesisValidators, {genesisTime, eth1BlockHash});
|
||||
const state = nodeUtils.initDevState(config, genesisValidators, {genesisTime, eth1BlockHash});
|
||||
|
||||
// Write testnet data
|
||||
fs.mkdirSync(targetDir, {recursive: true});
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function devHandler(args: IDevArgs & GlobalArgs): Promise<void> {
|
||||
const genesisTime = args.genesisTime ?? Math.floor(Date.now() / 1000) + 5;
|
||||
const eth1BlockHash = fromHex(args.genesisEth1Hash ?? toHex(Buffer.alloc(32, 0x0b)));
|
||||
|
||||
const {state} = nodeUtils.initDevState(config, validatorCount, {genesisTime, eth1BlockHash});
|
||||
const state = nodeUtils.initDevState(config, validatorCount, {genesisTime, eth1BlockHash});
|
||||
|
||||
args.genesisStateFile = "genesis.ssz";
|
||||
fs.writeFileSync(args.genesisStateFile, state.serialize());
|
||||
|
||||
@@ -80,11 +80,6 @@ const externalOptionsOverrides: Partial<Record<"network" | keyof typeof beaconNo
|
||||
defaultDescription: undefined,
|
||||
default: true,
|
||||
},
|
||||
eth1: {
|
||||
...beaconNodeOptions.eth1,
|
||||
defaultDescription: undefined,
|
||||
default: false,
|
||||
},
|
||||
rest: {
|
||||
...beaconNodeOptions.rest,
|
||||
defaultDescription: undefined,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export {chiadoChainConfig as chainConfig} from "@lodestar/config/networks";
|
||||
|
||||
// eth1.providerUrls suggestion: https://rpc.chiado.gnosis.gateway.fm
|
||||
export const depositContractDeployBlock = 155435;
|
||||
export const genesisFileUrl = "https://raw.githubusercontent.com/gnosischain/configs/main/chiado/genesis.ssz";
|
||||
export const genesisStateRoot = "0xa48419160f8f146ecaa53d12a5d6e1e6af414a328afdc56b60d5002bb472a077";
|
||||
export const bootnodesFileUrl = "https://raw.githubusercontent.com/gnosischain/configs/main/chiado/bootnodes.yaml";
|
||||
|
||||
@@ -20,7 +20,6 @@ switch (ACTIVE_PRESET) {
|
||||
|
||||
export {chainConfig};
|
||||
|
||||
export const depositContractDeployBlock = 0;
|
||||
export const genesisFileUrl = null;
|
||||
export const genesisStateRoot = null;
|
||||
export const bootnodesFileUrl = null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export {ephemeryChainConfig as chainConfig} from "@lodestar/config/networks";
|
||||
|
||||
export const depositContractDeployBlock = 0;
|
||||
export const genesisFileUrl = "https://ephemery.dev/latest/genesis.ssz";
|
||||
export const genesisStateRoot = null;
|
||||
export const bootnodesFileUrl = "https://ephemery.dev/latest/bootstrap_nodes.txt";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export {gnosisChainConfig as chainConfig} from "@lodestar/config/networks";
|
||||
|
||||
// eth1.providerUrls suggestion: https://rpc.gnosischain.com
|
||||
export const depositContractDeployBlock = 19469077;
|
||||
export const genesisFileUrl = "https://raw.githubusercontent.com/gnosischain/configs/main/mainnet/genesis.ssz";
|
||||
export const genesisStateRoot = "0x1511578d6de70428bf3529ab92102f21070694cb205443437fae359a7f220537";
|
||||
export const bootnodesFileUrl = "https://raw.githubusercontent.com/gnosischain/configs/main/mainnet/bootnodes.yaml";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export {hoodiChainConfig as chainConfig} from "@lodestar/config/networks";
|
||||
|
||||
export const depositContractDeployBlock = 0;
|
||||
export const genesisFileUrl = "https://media.githubusercontent.com/media/eth-clients/hoodi/main/metadata/genesis.ssz";
|
||||
export const genesisStateRoot = "0x2683ebc120f91f740c7bed4c866672d01e1ba51b4cc360297138465ee5df40f0";
|
||||
export const bootnodesFileUrl =
|
||||
|
||||
@@ -49,7 +49,6 @@ const GET_STATE_LOG_INTERVAL = 30 * 1000;
|
||||
|
||||
export function getNetworkData(network: NetworkName): {
|
||||
chainConfig: ChainConfig;
|
||||
depositContractDeployBlock: number;
|
||||
genesisFileUrl: string | null;
|
||||
genesisStateRoot: string | null;
|
||||
bootnodesFileUrl: string | null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user